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.

37,448 functions · 4,855 classes · 342 ways to take a step · 2 garbage collectors · 4 fixes

  1. the window's events
  2. every process updates
  3. the mod runs a frame of kirby's game
  4. every sprite is drawn
  5. the picture is shown

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

a game compiled every time it starts

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.

the shape of a frame

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.

milliseconds of one frame drawn in software, at the start of the prisoners' quarters
updatedrawingthe rest
the beheaded0.936.720.12
kirby1.826.850.12
kirby among 31 zombies3.477.880.13
one run each, with every phase of the frame timed. kirby's update holds the frames of his own game, which the mod runs in about every second frame of dead cells', and everything the mod does to the zombies it has taken over.

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.

thirty times a second

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.

a tenth of a second at 120 frames a second
laneframeswhat happens
preUpdate1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12every creature, every frame
fixedUpdate1, 5, 9physics and decisions, thirty times a second
postUpdate1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12then spriteUpdate, which moves each sprite to its creature
kirby's game1, 3, 5, 7, 9, 11a frame of the hidden console, sixty times a second
the lanes in the order they run inside a frame. every 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 built while you wait

a level is made when it is entered, on the same thread that draws:

  1. a graph of roomsthe run's seed starts a random generator, and the biome's plan asks for rooms: the main path, side rooms, doors that open on a trigger, on a timer or on a challenge, elites, teleports and lore
  2. a map that fits the graphwhen no map fits, the game writes an error to its log and starts again from a new seed
  3. tilesone of eleven builders lays the rooms out
  4. a living levellit layers, a map, its sound, and then the enemies, loot, merchants, doors and things to break
  5. a full collectiononce the new level runs, the game asks hashlink to clear out everything the old one left: the only collection it ever asks for

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.

two collectors in one house

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.

the middle pause of a .NET collection, in milliseconds, moving through the prisoners' quarters
pause
dead cells, the mod asleep1.8
the beheaded, the mod awake7.0
kirby7.4
one run each, drawn in software. with the mod awake, nearly every collection moved 5.5 to 6.3 megabytes to the older generation, against 1 to 2 with it asleep. a frame here lasts about 8 ms, so a pause of 7 nearly doubles the frame it falls in.

a toll on every call

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:

one call of a hooked function
fromtowhen
the game calls itthe detour
the detoura callback
a callbackarguments boxedalways
a callbackthe originalnever
arguments boxedhandlers rebuilt
handlers rebuiltthe mod's handler
the mod's handlerthe originalif it asks
the callback has a direct route back to the original, which leaves the arguments as they are. DCCM opens it when it writes the detour and closes it when it adds the first handler, a moment later, so it never takes it.

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.

millionths of a second for one call of Mob.canUpdate, measured in the game
a call
never hooked0.38
hooked once, its handlers gone0.77
hooked twice, as the mod did4.4
the same three leave 0, 56 and 1,840 bytes of garbage a call. hooked twice, because an enemy is asked both as an enemy and as an entity: about 2.2 millionths and 920 bytes for each hook.

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.

one host function through a run, with the fix for the middle bar
what happensits first byteshandlersa call costs
the game startsempty00.38 µs
kirby holds an enemya detour1about 2.2 µs
he lets go, before the fixa detour00.77 µs
he lets go, with itempty00.38 µs
he holds anothera detour1about 2.2 µs
what one call of the function costs at each step. with the fix, letting go of the last enemy puts the function back as it was when the game started.

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:

hooked calls a frame, at the start of the prisoners' quarters
calls
dead cells, the mod asleep6.6
the beheaded, the mod awake103.8
kirby108.7
kirby among 31 zombies482.4
counted by a probe that listens to DCCM's callback. 78.9 of kirby's 108.7 were one function, 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.

kirby against the beheaded

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, a body in a red scarf with a flame for a head, standing on a stone floor beside a tree stump with a spear stuck in it kirby, pink and round, standing on the same floor beside the same stump
the start of the prisoners' quarters, in two runs from the same seed. drag the line between them.
frames a second, drawn in software
the beheadedkirby
walking and jumping125.8108.1
31 zombies119.584.5
wading in the greenhouse54.547.3
one run each. the greenhouse is the heaviest place to draw in software, about two hundred sprites a frame.

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:

frames that took longer than 20 and 33 ms
the beheadedkirby
walking and jumping, 50 s3 and 114 and 1
31 zombies, 50 s7 and 246 and 2
wading in the greenhouse, 50 s204 and 6927 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.

four fixes

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.

the sprite hook

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:

a stone cell beside a tree stump with a spear in it. a few green cells rise from the floor and gather in the air, blue lightning runs along the floor, a yellow flash fills the room, and kirby stands where it met with his eyes shut, then opens them
kirby's arrival with the fix, ten pictures a second
lanepictureswhat happens
the game's arrival1–16the cells gather
the game's arrival17–34the head pops, in a flash
kirby drawn23–36
kirby's game35–36running
the game dresses its hero in kirby's outfit and plays the arrival it always plays, and kirby shows as the head pops. his own game stands still on one frame through the arrival and runs again when play begins.

the toll after the last handler

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.

the keyboard

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.

two hooks too late

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.

what they came to

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:

kilobytes of garbage a frame, walking through the prisoners' quarters
fixchange
before the fixes126.3 kb
the sprite hook−72.6 kb
the keyboard−3.9 kb
two hooks too late−6.0 kb
the three, added up43.8 kb
drawn in software. walking, kirby holds no enemy, so the toll after the last handler has nothing to take off. with all four at once the same walk made 43.9 kb a frame. the lab walks kirby without touching the keyboard: with keys pressed the way a player presses them, the keyboard fix took 28.6 kb a frame off, not 3.9.

with all four, the crossings into .NET nearly stopped:

hooked calls a frame, before the fixes and with all four
beforewith the fixes
walking108.624.7
31 zombies481.732.0
wading in the greenhouse257.923.1
what is left is the mod's own: eight functions it hooks to do its work once a frame, five more that are told at the end, and among the zombies, the few enemies kirby holds.
kirby in a green cap holds up a sword beside three piles of glowing green zombies, a red health bar over each pile, in a stone cell of the prisoners' quarters
the crowd test, with the four fixes: thirty-one zombies in three piles, and kirby with a sword. every crowd run played the same fight, before the fixes and after: thirty zombies hit, 66 times, for 2,046 damage.

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:

frames among the 31 zombies, by how long they took
millisecondsbeforewith the fixes
60.0%0.0%
71.6%22.4%
812.8%15.5%
912.7%15.6%
1014.0%31.0%
1126.6%8.0%
1213.3%4.3%
137.5%1.7%
144.8%0.6%
152.6%0.3%
161.3%0.2%
170.6%0.1%
180.7%0.0%
190.4%0.1%
200.2%0.0%
the share of frames in each millisecond, so that 7 is a frame of 7 to 8 ms. drawn in software, three runs before the fixes and two with them. frames over 20 ms: 152 in 153 seconds before the fixes, 18 in 97 seconds with them.

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:

drawn in software
beforewith the fixes
walking9.11 ms8.37 ms
31 zombies11.69 ms9.77 ms
drawn on the card
beforewith the fixes
walking0.90 ms0.61 ms
wading in the greenhouse1.76 ms0.96 ms
the average frame, of two or three runs. on the card kirby walked at 1,111 frames a second before the fixes and 1,628 with them, and waded at 569 and 1,037.

the hidden console's share

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.

where a frame of the hidden console goes
share
kirby's game, recompiled50.0%
timers and the schedule of events16.7%
the sound chip12.5%
the rest of the console11.5%
drawing in 2D7.3%
drawing in 3D2.1%
96 samples of the bridge's stack, taken while it ran alone for two and a half minutes. the game's own code is half the bill, and every other line is a part of the console the mod needs: kirby's sound effects come from its sound chip, and the background of his effects from its picture.

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.

what is left

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.

how it was read

  1. the program, taken apartcrashlink turned 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
  2. the mod's door, readDCCM's libraries were read as they are compiled, instruction by instruction, and its plumbing was timed on its own, without the game
  3. every crossing, counteda probe in the mod counted each call that crossed into .NET, and each one that crossed with no handler waiting
  4. runs, one at a timea copy of the game in a profile of its own, the same seed each time so every run plays the same level, and the same input. no two runs at once, and none unless the machine was idle
  5. each fix alone, then all togethertwo runs of each, against three of the build before

words

bytecode
a program written for a machine that does not exist, which a virtual machine runs or translates.
detour
a jump written over the start of a function, so that calling it runs something else first.
finalizer
a last task an object asks to run before it is thrown away. the collector has to keep it alive until that task is done.
garbage collector
the part of a runtime that finds the objects nothing points at any more and frees their memory. it may stop the program while it looks.
generation
how old an object is to the collector. young objects are checked often and cheaply. objects that survive are moved to an older generation, checked less often and at greater cost.
hook
a handler a mod puts in front of one of the game's functions.
JIT
a compiler that translates a program into the processor's instructions while it starts or runs, instead of beforehand.

notes

  1. dead cells version 35 on linux, run through DCCM 35.13.6 in a copy with its own profile and saves. the timings come from the mod's lab: the game runs with no window on screen and no frame cap, drawn by the processor unless the graphics card is named, an amd radeon r9 380. every run plays the same seed, so the same level, with the same input. the frames over 20 and 33 ms leave out the ones in which the lab wrote its reports.