onsoku advance is sonic advance, the game boy advance game from 2001, built again for a browser, with everything on the screen and in the speakers read from the player's own copy of the game. since the first article it has grown from one act to all fourteen of the game's levels, with the special stage, both endings and a game of chao hunt for two. it grew heavier on the way. this is how it was made lighter and smoother, measured before and after, without a pixel or a sample of sound changing.

4 changes · 15 levels measured · 25,200 frames timed in each version · 0 pixels changed

the same pixels, every one

the game is checked against the original a frame at a time, so a faster version is only worth having if it draws exactly what the slower one drew. every change here was held to that. both versions played the same scripted runs and took a hash of every frame, and the hashes had to match: the fourteen levels for 1,800 frames each, the widescreen view, the extras on their own and all together, the normal ending, the staff credits and two players in chao hunt. the sound engine's output was hashed too, over two minutes of music. all of it came out the same.

so what changed is how the work is done, never what comes out of it. that includes the original game's own bugs, and one of them, in secret base, turned out to be the slowest thing in the game.

the runs were scripted, so both versions saw exactly the same game: the first 1,800 frames of every act, half a minute each.1 a fast processor hides a lot, so the loading and the frames were also timed with the processor slowed to a quarter of its speed.

a picture for every refresh

a screen refreshes about sixty times a second, and the game steps exactly sixty times a second. the old loop asked the browser how long each refresh had taken, added the times up, and ran as many steps as they paid for: almost always one, sometimes none, sometimes two. on a refresh that ran none, the picture stayed as it was. the next one ran two, and a picture was never shown. the game lost nothing, but the eye sees a stutter.

firefox reports those times in whole milliseconds, so a refresh of 16.68 ms comes back as 16 or as 17. on a screen at 59.97 Hz, a hair slower than the game, the added-up time sits right on the edge of a step, and the rounding tips it one way and then the other. in one recorded run of 1,800 refreshes the old loop showed 663 pictures twice and skipped 664, and for long stretches the game moved at thirty pictures a second.

sonic rolling down a sandy slope past a red beach umbrella, the timer in the corner jumping two hundredths at a time and every second picture the same as the one before, a red or amber bar under most of them
the same roll down the slope past the umbrella, the timer counting up one frame at a time, every picture new, a grey bar under each
twenty refreshes of that run, at a sixth of their speed. the times firefox gave were played through each loop, and each refresh shows the frame the game draws for the step it reached. a the old loop: a red bar under a picture shown again, an amber one where a picture was skipped. b the new clock: one step a refresh, and every picture once.

the new clock learns how long the screen's refreshes really are. when they are within 1% of 60 Hz, it runs exactly one step for each, whatever the rounding says, and within 1% of 120 Hz one for every second refresh. on any other screen the steps follow the clock as before. and a frame is drawn only after a step, so a fast screen no longer draws the same picture two or three times over.

pictures shown twice and pictures skipped, out of 1,800 refreshes, or a minute when simulated
beforeafter
twiceskippedtwiceskipped
firefox, 59.97 Hz66366400
firefox, 59.83 Hz0000
firefox, 59.80 Hz0101
simulated, 59.94 Hz2600
simulated, 59.96 Hz, uneven414300
simulated, 60 Hz, 1% late2863035
the one skip at 59.80 Hz follows an 84 ms stall, which no loop can hide. the simulated minutes are the mean of 40 starting points; uneven means each refresh off by up to 0.6 ms, and a late refresh misses its turn, so its step goes on the next.

on a 120 Hz screen the old loop drew 7,200 pictures a minute for 3,600 steps, and 39 times a minute a step came one refresh or three after the last instead of two. the new one draws 3,600, two refreshes apart. at 144 Hz it drew 8,640, and draws 3,600.

loading in less than half the time

opening a level means reading it out of the ROM: its tiles, its maps, its collision, and every sprite it can show in every palette it can show it in. the sprites are laid out in one big picture, an atlas, and the renderer cuts frames out of it as it draws.

the atlas used to be 512 pixels wide and as tall as it needed: 33,390 px for neo green hill act 1. then the renderer read all of it back as pixels, for the few sprites it turns or recolours one pixel at a time. now the atlas is packed 2,048 pixels wide, tallest frames first, with sonic's frames and the turning pieces in its top rows, and only those 564 rows are read back. the rest stays a picture that is only ever drawn.

the atlas kept as pixels, neo green hill act 1, in megabytes
megabytes
512 × 33,390 px, all of it68.4
2,048 × 6,168 px, its top 564 rows4.6
four bytes for each pixel kept. the atlas itself went from 17.1 million pixels to 12.6.

the rest of the reading got quicker in smaller ways. a palette is named by a short string, two characters for each colour, where it used to be every one of its numbers written out. a frame is written four bytes at a time instead of one. a piece of a sprite is decoded once for all the palettes it is drawn in. and the tint under the water in ice mountain walks the pixels as whole words.

reading each level and setting up its renderer, in milliseconds
beforeafter
neo green hill 1550269
neo green hill 2511211
secret base 1613300
secret base 2551250
casino paradise 1526234
casino paradise 2520211
ice mountain 1838430
ice mountain 2820404
angel island 1640286
angel island 2618279
egg rocket533222
cosmic angel519204
x zone684266
the moon zone680298
forest chao garden550214
the median of three runs. all fifteen took 9.2 s before and take 4.1 s now. setting up the renderer alone went from 29–79 ms to 2–15 ms. with the processor slowed four times, the fourteen acts took 32.7 s and take 15.9 s.

the spotlights of secret base

secret base act 1 opens in the dark, with two beams of light sweeping across the screen. the beams are windows, the console's way of keeping an effect out of part of the screen, and the game writes their edges into a table, a line of the screen at a time. one beam writes a line past the end of the table, into the sprite palette stored after it, and on many frames that lands in sonic's first colour, his outline. it is a bug of the original, and it stays.

sonic running right along a green metal walkway in the dark, two pale beams of light sweeping over the wall behind him, sonic dark outside them and bright when he runs through one
secret base act 1, just after its title card, at the game's speed: a frame of the strip is every second frame of the game. the beams move with every frame, and so does what they write.

the renderer draws sonic in his palette of the moment, and when his colours change it has to recolour his frames. it used to recolour the whole sheet of them every time, which took 11 ms or more, and the spotlights changed his outline over and over. now it keeps the last four colourings, and in each it colours a frame only the first time that frame is drawn.

the slowest frame in each second of secret base act 1, in milliseconds
secondbeforeafter
16.85.4
256.12.1
349.82.7
40.70.4
53.30.5
60.80.7
70.60.4
80.59.3
90.90.3
1065.50.4
112.10.4
121.30.4
130.60.4
140.60.9
150.60.4
160.52.5
1742.00.3
1839.31.6
196.00.4
201.50.3
212.80.3
220.50.7
230.52.9
2439.71.6
2546.61.5
261.90.3
270.80.3
280.60.4
290.50.7
300.50.3
a frame here is the step and the drawing together. before, thirteen frames of this act took longer than a refresh, the longest 65.5 ms, four refreshes; now the longest is 9.3 ms.

those thirteen were the only frames over a refresh in all 25,200 of the fourteen acts, and now there are none. frames over 8 ms, half a refresh, went from 18 to 1, and the average frame from 0.43 ms to 0.29 ms. with the processor slowed four times, the frames over a refresh went from 50 to 14, and 11 of those 14 are the first frame of an act, which builds what the act needs.

fewer calls to draw

many of the game's backgrounds scroll by the line: the console can move each line of the screen on its own, and the sky, the sea and the walls behind the acts use it. the renderer drew such a layer a line at a time, one call to the canvas's drawImage for each of the screen's 160 lines. but most neighbouring lines move together, and lines that scroll alike over neighbouring rows of the map are one rectangle, which one call can draw. the pixels are the same.

a frame of secret base act 2, sonic curled up on an orange girder, washed red by a thin outline drawn around every single line of the background, all the way down the screen the same frame with a few large rectangles outlined over the background, each covering a band of lines
one frame of secret base act 2, three times larger, with every call that draws its two background layers outlined. the frame under the outlines is the same in both.
calls to drawImage for each frame, over 600 frames, in the levels that changed
beforeafter
neo green hill 1267154
secret base 121657
secret base 233871
ice mountain 120142
ice mountain 219839
x zone21657
the moon zone258116
the other levels draw their backgrounds through one block of pixels, and make 41 to 58 calls a frame either way. in neo green hill the sea's 136 lines each move on their own, so most of its calls stay, and the moon zone's last 31 lines of sky each move faster than the one above.

less memory, less garbage

a smaller atlas and a smaller copy of it as pixels left the browser holding much less. ice mountain, which keeps two more atlases for what is drawn under the water, held the most, and lost the most.

memory held by the page's process at the end of each run, in megabytes
beforeafter
neo green hill 1323246
neo green hill 2309232
secret base 1469288
secret base 2322246
casino paradise 1312235
casino paradise 2311233
ice mountain 1568344
ice mountain 2565339
angel island 1357264
angel island 2350254
egg rocket313237
cosmic angel311233
x zone376264
the moon zone396287
between a quarter and two fifths less in every act, the most in secret base 1 and ice mountain.

a game that makes short-lived objects every frame makes the browser stop now and then to sweep them up. over the fourteen runs the small sweeps went from 579 to 318 and the large ones from 59 to 39, and their time together from 449 ms to 280 ms. the longest single pause was about the same, 7.3 ms before and 8.1 ms after.

the sound engine was one of the makers. it mixes the console's sound a sample at a time, and each sample's mix came back as a new pair of numbers, made and thrown away. now one pair is used again and again. over two minutes of music the sweeps it causes went from 470 to 224, and the time to render it from 633 ms to 547 ms, with the same output to the last sample.

what is left

the change is pull request 119 of the game's repository, in four commits, with every measurement in its description.

notes

  1. both versions ran in chrome 156 with no window, driven over its debugging protocol, from the same copy of the tested american ROM. a page loaded one version's modules, stepped the engine with the scripted presses and timed each step and each drawing; the memory is the page process's resident size, and the sweeps are counted from the browser's own trace. firefox 158, with no window, recorded the time of each of its refreshes, and those times were played through both loops; the counts leave out the first second, while the new clock learns the rate. the frames in the strips and the comparison were drawn by the game itself and enlarged without smoothing.