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.
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 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.


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.
| before | after | |||
|---|---|---|---|---|
| twice | skipped | twice | skipped | |
| firefox, 59.97 Hz | 663 | 664 | 0 | 0 |
| firefox, 59.83 Hz | 0 | 0 | 0 | 0 |
| firefox, 59.80 Hz | 0 | 1 | 0 | 1 |
| simulated, 59.94 Hz | 2 | 6 | 0 | 0 |
| simulated, 59.96 Hz, uneven | 41 | 43 | 0 | 0 |
| simulated, 60 Hz, 1% late | 28 | 63 | 0 | 35 |
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.
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.
| megabytes | |
|---|---|
| 512 × 33,390 px, all of it | 68.4 |
| 2,048 × 6,168 px, its top 564 rows | 4.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.
| before | after | |
|---|---|---|
| neo green hill 1 | 550 | 269 |
| neo green hill 2 | 511 | 211 |
| secret base 1 | 613 | 300 |
| secret base 2 | 551 | 250 |
| casino paradise 1 | 526 | 234 |
| casino paradise 2 | 520 | 211 |
| ice mountain 1 | 838 | 430 |
| ice mountain 2 | 820 | 404 |
| angel island 1 | 640 | 286 |
| angel island 2 | 618 | 279 |
| egg rocket | 533 | 222 |
| cosmic angel | 519 | 204 |
| x zone | 684 | 266 |
| the moon zone | 680 | 298 |
| forest chao garden | 550 | 214 |
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.
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.
| second | before | after |
|---|---|---|
| 1 | 6.8 | 5.4 |
| 2 | 56.1 | 2.1 |
| 3 | 49.8 | 2.7 |
| 4 | 0.7 | 0.4 |
| 5 | 3.3 | 0.5 |
| 6 | 0.8 | 0.7 |
| 7 | 0.6 | 0.4 |
| 8 | 0.5 | 9.3 |
| 9 | 0.9 | 0.3 |
| 10 | 65.5 | 0.4 |
| 11 | 2.1 | 0.4 |
| 12 | 1.3 | 0.4 |
| 13 | 0.6 | 0.4 |
| 14 | 0.6 | 0.9 |
| 15 | 0.6 | 0.4 |
| 16 | 0.5 | 2.5 |
| 17 | 42.0 | 0.3 |
| 18 | 39.3 | 1.6 |
| 19 | 6.0 | 0.4 |
| 20 | 1.5 | 0.3 |
| 21 | 2.8 | 0.3 |
| 22 | 0.5 | 0.7 |
| 23 | 0.5 | 2.9 |
| 24 | 39.7 | 1.6 |
| 25 | 46.6 | 1.5 |
| 26 | 1.9 | 0.3 |
| 27 | 0.8 | 0.3 |
| 28 | 0.6 | 0.4 |
| 29 | 0.5 | 0.7 |
| 30 | 0.5 | 0.3 |
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.
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.
| before | after | |
|---|---|---|
| neo green hill 1 | 267 | 154 |
| secret base 1 | 216 | 57 |
| secret base 2 | 338 | 71 |
| ice mountain 1 | 201 | 42 |
| ice mountain 2 | 198 | 39 |
| x zone | 216 | 57 |
| the moon zone | 258 | 116 |
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.
| before | after | |
|---|---|---|
| neo green hill 1 | 323 | 246 |
| neo green hill 2 | 309 | 232 |
| secret base 1 | 469 | 288 |
| secret base 2 | 322 | 246 |
| casino paradise 1 | 312 | 235 |
| casino paradise 2 | 311 | 233 |
| ice mountain 1 | 568 | 344 |
| ice mountain 2 | 565 | 339 |
| angel island 1 | 357 | 264 |
| angel island 2 | 350 | 254 |
| egg rocket | 313 | 237 |
| cosmic angel | 311 | 233 |
| x zone | 376 | 264 |
| the moon zone | 396 | 287 |
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.
the change is pull request 119 of the game's repository, in four commits, with every measurement in its description.