Skip to content

LHVM scheduler and @noyield

Daniele Lombardi edited this page Aug 20, 2024 · 2 revisions

If you wonder what //@noyield means, here is the explanation.

The LHVM is a single-thread multi-task system. It can handle the execution of multiple tasks in parallel, each with its own context, but they share the same execution unit, so each task must suspend its execution from time to time in order to let other tasks run.

We are used to modern systems where the time partitioning is handled by the hardware+OS, but the LHVM is not so smart.

In LHVM there are specific instructions which are used to suspend the execution of a task:

  • the start keyword, which must be coded right after local variables declaration, results in an explicit yield instruction;
  • the fixed and conditional jumps instructions, but only if the BACKWARD flag is set. This flag is set by the compiler only for jumps to previous locations, which are used to implement while loops.

This means that a task is always suspended at least once (right after the local variables are initialized), and at every loop cycle.
If you have a task with 2 nested loops, each repeating 10 times, that task will be suspended 101 times during its execution. Considering that the task scheduler runs at 10 Hz, that task would take more than 10 seconds to complete!

Luckily for us, the BACKWARD flag is not required to jump to a previous location, so we can alter the compiler behaviour to avoid setting the flag for some specific loops under our control, and we can also remove the explicit yield which would be coded on start keyword. This exploit allows to code entire scripts that are executed in a single turn, for example to run scanlines over land, or to solve non-linear equations for geometric computations.

But all that glitters is not gold. Since the VM turns are executed on the same thread used by the game engine, locking a turn on a long running loop results in a complete freeze of the game.

That said, you can prevent yielding as long as your loops task a finite number of cycles.

Use cases

Scanlines

You can run two nested loops to scan an area to locate all individual objects in that area.

Here is an example to set on fire all the buildings within a squared area:

begin script BurnArea(Position, Radius, Speed)
	Cx = SCRIPT_OBJECT_PROPERTY_TYPE_XPOS of Position
	Cz = SCRIPT_OBJECT_PROPERTY_TYPE_ZPOS of Position
	XStep = 10
	ZStep = XStep * 0.707
	CellRadius = XStep / 2
	CurrOffset = 0
	x0 = Cx - Radius + CellRadius
	z0 = Cz - Radius + CellRadius
	x1 = Cx + Radius - CellRadius
	z1 = Cz + Radius - CellRadius
	X = x0
	Z = z0
	Building = 0
start
	//@noyield
	while X < x1
		Z = z0 + CurrOffset
		//@noyield
		while Z < z1
			Building = get SCRIPT_OBJECT_TYPE_ABODE at [X, 0, Z] radius CellRadius
			if Building exists
				enable Building on fire Speed
			end if
			Z += ZStep
		end while
		X += XStep
		CurrOffset = CellRadius - CurrOffset
	end while
end script BurnArea

Call it as:

run background script BurnArea(marker at [get town with id 0], 300, 0.2)

Maths

BW1 lacks any math function beside the 4 operations. Luckily for us, almost any function can be computed using iterative methods which can rely just on the 4 basic operations. Without the ability to disable yielding, these methods would take hours to be executed.
The major limitation is that scripts cannot return values, so you have to use a global variable to store the result; this may cause concurrency problems when you have multiple tasks calling the same function, but hopefully you don't need it.

Here is an example to compute the square root of a number using the bisection method:

global Root

begin script SquareRoot(Number)
	Low = 0
	High = 0
	Estimate = 0
	Test = 0
	Delta = 0
	ToleranceSquared = Number * Number / 10000.0
//@noyield
start
	Root = 0
	if Number >= 0
		if Number >= 1
			Low = 1
			High = Number
		else
			Low = Number
			High = 1
		end if
		Estimate = (Low + High) / 2.0
		Test = Estimate * Estimate
		Delta = Number - Estimate
		//@noyield
		while Delta * Delta > ToleranceSquared
			if Test > Number
				High = Estimate
			else
				Low = Estimate
			end if
			Estimate = (Low + High) / 2.0
			Test = Estimate * Estimate
			Delta = Number - Test
		end while
		//
		Root = Estimate
	end if
end script SquareRoot

Call it as:

run script SquareRoot(900)
// global variable Root is now 30
Clone this wiki locally