kirby's dead land is a mod that puts kirby in dead cells. he is not redrawn: the real kirby super star ultra runs hidden in an emulator, and dead cells borrows him one frame at a time. the guest was read from its cartridge in an earlier article. this one reads the host. kirby made dead cells slower than its own hero does, and the place to find out why was inside the game: how a frame of it runs, what it spends the time on, and what it charges a mod for every question the mod asks it.
everything below was read from the game's own files without running them, or timed on one machine, in a copy of the game kept apart from the real one, with saves of its own.1
dead cells is written in haxe, a language made to be translated into other languages. on a computer it becomes bytecode for hashlink, a small virtual machine, and ships as a single file of 17.7 megabytes. nothing in that file is ready to run. each time the game starts, hashlink's JIT turns its 37,448 functions into the processor's own instructions, and only then does the title appear. heaps, the engine, is in the same file, so the code that draws a sprite and the code that decides a zombie's next step can be read the same way.
and they can be read without running anything. crashlink, a disassembler for hashlink, lists every function with its instructions, every class with its fields, and every call the game makes into native code: the 503 doors through which it reaches the screen, the sound and the disk. from those lists an index was built of who calls whom, so that a question like “what happens in a frame?” could be followed down from the top.
a mod does not change that file, and this one never touched it. it goes in through dead cells core modding, DCCM, which starts the game with its own build of hashlink. that build compiles every function with 32 empty bytes at its start, room to write a detour into later, and it hands .NET, where mods are written, a stand-in for each of the game's 4,855 classes. through the stand-ins a mod reads and writes the game's memory and calls its functions. none of the game's own logic runs in .NET.
a frame of dead cells is one turn of the engine's loop: read the window's events, update the game, draw it. updating means asking every process, a tree of them with the game, the level, the hero and the interface in it, to update, and when every one has, asking every one to postUpdate. DCCM raises an event of its own for mods when that is over, while the frame's state is final and nothing is drawn yet. that is where the mod runs kirby's game.
the loop waits for nothing but the screen. with the screen's pace switched off, as it is in the lab, a frame starts the moment the last one ends, so the time of a frame is simply what the game costs.
| update | drawing | the rest | |
|---|---|---|---|
| the beheaded | 0.93 | 6.72 | 0.12 |
| kirby | 1.82 | 6.85 | 0.12 |
| kirby among 31 zombies | 3.47 | 7.88 | 0.13 |
78 in 100 parts of kirby's frame go to drawing it.
that is a processor doing a graphics card's work. the lab draws in software, so that the numbers do not depend on a driver. drawn on the card, the same frames take under a millisecond: 0.54 ms for dead cells with the mod asleep, 0.79 ms for the beheaded with the mod awake, 0.92 ms for kirby. there, what the mod adds is no longer hidden behind the drawing.
the level is one of those processes, and every creature lives in it. each frame Level.update asks a quadtree which part of the level is near the screen, calls preUpdate on every creature and pushes apart those that overlap. what a creature does, its physics and its mind, is in fixedUpdate, which 342 classes write in a way of their own, and that runs on a clock of its own: thirty times a second, however fast the frames come.
| lane | frames | what happens |
|---|---|---|
preUpdate | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 | every creature, every frame |
fixedUpdate | 1, 5, 9 | physics and decisions, thirty times a second |
postUpdate | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 | then spriteUpdate, which moves each sprite to its creature |
| kirby's game | 1, 3, 5, 7, 9, 11 | a frame of the hidden console, sixty times a second |
update is over before any postUpdate starts.a creature takes part only while canUpdate says it may: when it is in the game and not frozen, when it is one of the few that always update, and, for an enemy, whenever it has a target. at the start of the prisoners' quarters the game asks that question 370 to 590 times a frame. it is a cheap question, until a mod listens to it.
a level is made when it is entered, on the same thread that draws:
a load that never leaves the drawing thread should freeze the screen. it does not, because the generator stops in seventeen places to ask whether a frame of the loading screen is due, and draws one if it is. how long a load takes was not measured. kirby plays inside a level, not between them.
a program that makes objects needs something to throw them away. dead cells' objects live in hashlink's heap, and hashlink's garbage collector looks for what is still in use on up to sixteen threads at once. inside a level it has little to do: the game made 5 to 17 kilobytes of new objects a frame there.
the mod's objects, and DCCM's, live in .NET's heap, which has a collector of its own, and the two are tied together twice. before every full collection of hashlink's, DCCM stops everything and runs one of .NET's, so that neither throws away what the other still holds. and every call the game makes into a mod leaves an object with a finalizer, a last thing to do before it may go. such an object cannot die in the first collection that finds it. it is moved, with everything it holds, to an older generation, where collecting stops the game for longer.
| pause | |
|---|---|
| dead cells, the mod asleep | 1.8 |
| the beheaded, the mod awake | 7.0 |
| kirby | 7.4 |
to change what the game does, a mod hooks one of its functions: it hands DCCM a handler that runs instead of the original and may call the original from inside. the first time any mod hooks a function, DCCM writes a detour into the empty bytes at its start, to a callback in .NET. from then on every call of that function, from anywhere in the game, goes this way:
| from | to | when |
|---|---|---|
| the game calls it | the detour | |
| the detour | a callback | |
| a callback | arguments boxed | always |
| a callback | the original | never |
| arguments boxed | handlers rebuilt | |
| handlers rebuilt | the mod's handler | |
| the mod's handler | the original | if it asks |
the arguments are copied into a list, each in a box of its own. the chain of handlers is put together again, from nothing, on every call: a stand-in for the original, one for the handler, a closure, and a callback with a finalizer for each handler. then the handler is called by name. all of that is paid whatever the handler does, even if it does nothing.
| a call | |
|---|---|
| never hooked | 0.38 |
| hooked once, its handlers gone | 0.77 |
| hooked twice, as the mod did | 4.4 |
the middle bar is the strange one. removing a handler takes it off the list and does nothing else: the detour stays, and the function keeps crossing into .NET for nothing, for the rest of the session. the mod hooks Mob.canUpdate while kirby holds an enemy in his own game, and lets go of the hook when he lets go of the enemy. after the first one, every enemy in every later level paid the toll anyway.
| what happens | its first bytes | handlers | a call costs |
|---|---|---|---|
| the game starts | empty | 0 | 0.38 µs |
| kirby holds an enemy | a detour | 1 | about 2.2 µs |
| he lets go, before the fix | a detour | 0 | 0.77 µs |
| he lets go, with it | empty | 0 | 0.38 µs |
| he holds another | a detour | 1 | about 2.2 µs |
so the first finding was not about dead cells at all. it was about how much the mod was asking. this is how many calls a frame crossed into .NET:
| calls | |
|---|---|
| dead cells, the mod asleep | 6.6 |
| the beheaded, the mod awake | 103.8 |
| kirby | 108.7 |
| kirby among 31 zombies | 482.4 |
HSprite.draw, which draws every sprite on the screen. in the crowd, 280 a frame were the toll of the middle bar: calls with no handler at all, 259 of them Mob.canUpdate.with that in hand, the comparison. the same level from the same seed, the same path through it, once as the beheaded with the mod awake and once as kirby:
| the beheaded | kirby | |
|---|---|---|
| walking and jumping | 125.8 | 108.1 |
| 31 zombies | 119.5 | 84.5 |
| wading in the greenhouse | 54.5 | 47.3 |
moving, kirby cost 1.3 ms a frame more than the beheaded. among the zombies he cost 3.5 ms more. an average hides the part a player feels, though, which is the frame that comes late:
| the beheaded | kirby | |
|---|---|---|
| walking and jumping, 50 s | 3 and 1 | 14 and 1 |
| 31 zombies, 50 s | 7 and 2 | 46 and 2 |
| wading in the greenhouse, 50 s | 204 and 6 | 927 and 11 |
20 ms is a frame at 50 a second, and 33 ms one at 30: the stutters an eye can catch. most of kirby's were not his game, or his drawing. they were collections.
35 of 57 of kirby's slowest frames among the zombies held a pause of .NET's collector.
and the collections came from the toll. kirby's frame made 126.5 kilobytes of garbage moving and 251.3 among the zombies, most of it on the way into and out of hooked functions, and nearly every collection of it was the slow kind, because of the finalizers.
all four are changes to the mod. the game was never touched. each was timed alone against the build before it, and then all four together.
kirby wears a real dead cells outfit. the game needs one to dress its hero, so the outfit has a picture for every animation the beheaded has, all of them one still kirby. the real kirby is drawn from his own game, so those pictures must never show, and the mod made sure by hooking HSprite.draw, the function that draws every sprite in the game, to skip the outfit's. that put a toll on every sprite on the screen, kirby's or not, 79 times a frame. the fix gives the outfit a blank page instead: the game draws its pictures of kirby, and they are empty. the hook is gone.
kirby's arrival shows the blank page at work. the game plays its hero's arrival as it always does: the cells gather on the floor, and the head pops in a flash. every picture it draws of that hero is now empty, so the first body on the floor is kirby's own, drawn by his game:
| lane | pictures | what happens |
|---|---|---|
| the game's arrival | 1–16 | the cells gather |
| the game's arrival | 17–34 | the head pops, in a flash |
| kirby drawn | 23–36 | |
| kirby's game | 35–36 | running |
the strange middle bar of figure 5. when kirby lets go of the last enemy he holds, the mod now takes DCCM's detour off the function, so it runs as if it had never been hooked, and writes it back before the next hold. the first try did something gentler: it switched the direct route back on. the game crashed, three times in four, after the enemies were let go. a route DCCM never takes was a route nobody had tested, so that try was dropped rather than debugged, and the detour is now taken off whole.
twice a frame the mod reads the controls. for every key it asked the game whether that key was down, through a function that, called from .NET, makes a new object every time. now it reads the game's own table of pressed keys once, and looks the keys up there.
so that the game's attacks find kirby where he is drawn, the mod moves the hero's hurtbox onto kirby's body while the level updates, and puts it back when the update returns. four hooks kept the hero's own work on his real feet in the meantime. two of them ran after the update had returned, as figure 2 shows postUpdate does: they guarded something that was already over, and one of them ran for every creature in the level. those two are gone, and the other two now share the mod's own handler on the same function, since every handler on a hooked function is paid for again on each call.
each fix was timed alone, two runs against three of the build before. walking, this is what each took off the garbage of kirby's frame:
| fix | change |
|---|---|
| before the fixes | 126.3 kb |
| the sprite hook | −72.6 kb |
| the keyboard | −3.9 kb |
| two hooks too late | −6.0 kb |
| the three, added up | 43.8 kb |
with all four, the crossings into .NET nearly stopped:
| before | with the fixes | |
|---|---|---|
| walking | 108.6 | 24.7 |
| 31 zombies | 481.7 | 32.0 |
| wading in the greenhouse | 257.9 | 23.1 |
less garbage meant fewer collections, and shorter ones, with fewer finalizers waiting in each. walking, .NET collected 21 times a minute instead of 51, among the zombies 28 instead of 96, and the middle pause fell from about 7 ms to 5 or 6. the frames show it:
| milliseconds | before | with the fixes |
|---|---|---|
| 6 | 0.0% | 0.0% |
| 7 | 1.6% | 22.4% |
| 8 | 12.8% | 15.5% |
| 9 | 12.7% | 15.6% |
| 10 | 14.0% | 31.0% |
| 11 | 26.6% | 8.0% |
| 12 | 13.3% | 4.3% |
| 13 | 7.5% | 1.7% |
| 14 | 4.8% | 0.6% |
| 15 | 2.6% | 0.3% |
| 16 | 1.3% | 0.2% |
| 17 | 0.6% | 0.1% |
| 18 | 0.7% | 0.0% |
| 19 | 0.4% | 0.1% |
| 20 | 0.2% | 0.0% |
in software, the drawing is most of the frame, and it stays. on the card, where the mod's share showed, the fixes took a third off kirby's walk and nearly half off his wade:
| before | with the fixes | |
|---|---|---|
| walking | 9.11 ms | 8.37 ms |
| 31 zombies | 11.69 ms | 9.77 ms |
| before | with the fixes | |
|---|---|---|
| walking | 0.90 ms | 0.61 ms |
| wading in the greenhouse | 1.76 ms | 0.96 ms |
the other half of the mod is the hidden console: melonDS, a DS emulator, and a bridge around it. it had already been through rounds of its own, told in the earlier article: it runs kirby's game through a recompiler, at twice the console's clock, and does not paint the screen nobody looks at. driven alone, as fast as it will go, one frame of kirby's game takes 0.94 ms, and 1.01 ms at the ninety-fifth percentile.
| share | |
|---|---|
| kirby's game, recompiled | 50.0% |
| timers and the schedule of events | 16.7% |
| the sound chip | 12.5% |
| the rest of the console | 11.5% |
| drawing in 2D | 7.3% |
| drawing in 3D | 2.1% |
one saving was left out on purpose. a recompiler can map the console's memory straight onto the computer's, and catch the reads that miss through the computer's own crash handler. but the hidden console lives inside dead cells, which has crash handlers of its own, so that door stays shut. what the bridge spends now is the console itself, so the fixes were all on the host's side.
five more functions are hooked for the whole session and called a few times a frame: the one that walks the hero by the stick, the healing flask's test, the collector's test for unlocked items, the weapon slots and the beheaded's head. together, about fifteen tolls a frame. each guards a rule of the mod that needs a live check of its own when it changes, so they wait for their own turn.
the first frame kirby is on screen takes 47 to 64 ms: the mod's own code being compiled the first time it runs, and a first frame of his game that is slower than the rest. it happens once, during his arrival. the frames of a level being built were not measured, and neither was play in a window on a graphics card, with the screen's pace on, which is how the game is meant to be played.
hlboot.dat into lists of functions, classes and native calls, and a script turned those into an index of who calls whom. the game, its folder and its saves were only ever read