-
Notifications
You must be signed in to change notification settings - Fork 2
LHVM scheduler and @noyield
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 implementwhile
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.
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)
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