---
title: "Fast on a cheap tablet"
description: "How Professor Oops keeps a page of paint moving on a cheap tablet: a Web Worker, seven WebAssembly kernels tested against their twins, the hand served first."
source_name: "Game Tailors"
source_url: "https://professoroops.com/en/inside/speed/"
site_home: "https://professoroops.com/"
source_description: "The drawing app where the paper is real. No ads. No accounts. No personal data: only anonymous counts, which you can switch off."
locale: "en"
legal:
  - "Game Tailors · Schieweg 15Y-40, 2627 AN Delft (NL)"
  - "KvK 92437702 · VAT NL866044474B01"
  - "info@gametailors.com · +31 6 4227 5919"
copyright: "© 2026 Game Tailors"
published_at: "2026-11-10T12:04:18.815Z"
modified_at: "2026-11-10T12:04:18.815Z"
---

# Fast on a cheap tablet

## The paint under your child's finger keeps moving, even on a cheap tablet

Every spot of the page holds its own water, colour, goop and heat, and a big page has almost two hundred thousand spots. A budget tablet can't work them all sixty times a second. So the game rests the quiet parts of the page, does its heaviest sums in a faster language, and serves the place under the finger first. The rest catches up a moment later.

[For engineers](<#hood-engineers>)

![Painted illustration of a tablet and a phone on a yellow sheet with a ketchup zigzag, both screens showing paint splats, milk swirls and a crayon house, the painter smiling at them.](<https://professoroops.com/_assets/entity_behXKeDXPUwYbHHJZMv7.webp>)

## Drawing and simulating run side by side, so the finger never waits for the paint

Professor Oops runs as two halves that don't wait for each other. One half listens to your child's finger and paints the screen. The other works out what the ketchup, the water and the fire do next. That second half runs in a Web Worker, a separate line of work that the browser gives a web page. The two pass notes to each other many times a second.

So a page swimming in paint can't freeze the hand. While the simulation is busy, the screen still draws the brush or the spoon in your child's hand from the newest news it has, every time the screen refreshes.

The drawing half has no copy of the world. It gets a replica: the things on the page \(a ball, a candle, a sprinkle of glitter\), sent as only what changed since the last note, and pictures of the squares of paper that changed. A test fails if any drawing code reaches for the simulation directly, because that kind of mistake works when everything runs together and silently does nothing in the worker. The test's own header says it has happened four times.

If the worker can't start, the game moves over by itself and runs the simulation alongside the drawing instead. Whatever your child already did goes with it.

The paint under your child's finger keeps moving, even on a cheap tablet

For engineers

### The paint under your child's finger keeps moving, even on a cheap tablet

The whole engine runs in a Web Worker; the main thread draws from a compact replica and uploads only the squares of paper that changed. Seven WebAssembly kernels \(four use SIMD in the released game\) keep their JavaScript twins: five are tested equal to the bit, two within 32-bit float rounding. Each tiled simulation has its own clock: the hand's squares go first, every square is guaranteed a turn within a bounded wait, and a late square catches up in one longer step, held under a stability cap.

[Under the hood](<#hood-worker>)

Drawing and simulating run side by side, so the finger never waits for the paint

Under the hood

### Drawing and simulating run side by side, so the finger never waits for the paint

The worker holds the whole engine. The main thread sends input the moment it happens and a regular tick with the time that has passed. The worker steps the simulation in fixed 1/60 s steps and answers with a frame: the changed pieces of the page's pictures, the replica, the sounds the step made, and its own timings. Only one tick is out at a time and owed time adds up on the main side, so the simulation neither spirals nor idles. A frame's buffers are handed across, never copied, and handed back for reuse.

The replica sends only what changed. An unchanged object costs a few bytes; a changed one sends only its changed values, packed in flat typed streams. The busiest lists, such as confetti and glitter, travel as compact rows: encoding on those pages dropped to 0.4× of its old time, from 7.9 to 3.1 ms on the busiest scene. Draw code still calls the objects' own methods on the main side, so the replica feels like the real thing to the code that draws it.

A WebAssembly memory can't be handed between threads, so the worker copies only the changed squares out, the ones round the hand first. With no upload limit, an automated test holds what was sent to be exactly what a fresh copy of the whole page would give, to the bit, after every frame of 24 busy scenes.

If the worker stays silent too long after starting, every message sent so far, kept as copies, is replayed into a simulation on the main thread, and a late worker is stopped, so two simulations never run. A test plays a stroke and 60 ticks both ways, and every saved field matches.

### One frame's round trip

Main thread → Web Worker

#### Input

At once: your finger, the moment it moves.

Main thread → Web Worker

#### Tick

The time that has passed.

Web Worker → main thread

#### Frame

The changed tiles, the replica, the sounds, and the milliseconds it took.

Main thread → Web Worker

#### Buffers back

Handed back for the next frame, never copied.

From the game's worker design. A stroke shows one frame later than it would if everything ran on one thread; in return the finger is never blocked.

## The hardest calculations run in a faster form of code, and tests check they give the same answers

Seven of the game's busiest loops have a second, faster version in WebAssembly, a compact, low-level form of code that browsers can run quickly. They cover the wind's pressure, runny goop flowing, water and dye creeping through paper, oil floating on water, fire, soap racing over milk, and fingers pushing paint about. Four of them also work on several numbers at once \(SIMD, "single instruction, multiple data"\).

The readable JavaScript versions stay in the game as the reference. The tests run both on the same messy page and compare them. For five of the seven, every number on the page must match to the last binary digit. The other two, paper and the floating layer, trade a little rounding for speed and must agree within a hundred-thousandth and a ten-thousandth.

If someone changes the slow version and forgets the fast one, the tests stop them. If a phone can't run the fast versions at all, the game uses the slow ones and plays nearly the same game, with less to spare. The one known difference: on the slow path at the two lowest detail levels, the wind blows a little weaker.

[Under the hood](<#hood-kernels>)

The hardest calculations run in a faster form of code, and tests check they give the same answers

Under the hood

### The hardest calculations run in a faster form of code, and tests check they give the same answers

The kernels are written in AssemblyScript, compiled with SIMD switched on, and shipped inside the game's own script, so the apps fetch nothing extra.

**Why most exact kernels use doubles.** JavaScript does its sums in 64-bit floats, and storing into a 32-bit array rounds. A kernel that does the same operations in 64-bit, in the same order, rounding at the same moments, comes out identical. Goop, fire, soap and pushing do that, and pay for it in vector width: fire gets two lanes instead of four, and the other three use none in the released game. The pushing kernel even reproduces the browser's own way of working out a distance, because the compiler's library rounds differently now and then. The fire kernel draws its random numbers from the same stream as the JavaScript, kept in the shared memory.

**The air does it the other way round.** Its JavaScript twin rounds every operation to 32 bits, in a fixed order, so the JavaScript matches four 32-bit lanes. The solver is damped Jacobi multigrid, chosen because the Gauss-Seidel it replaced leaned: draughts blew a fifth harder one way than the other. For multigrid itself, see <sup>[Brandt 1977](<https://professoroops.com/en/inside/research-simulation/#brandt-1977>)</sup> and <sup>[Briggs 2000](<https://professoroops.com/en/inside/research-simulation/#briggs-2000>)</sup>, found later and consistent with the code.

**Three layers of checking.**

- **Twin hashes.** Each kernel names the JavaScript it copies, 141 functions and constants in the released game. A build step hashes them with comments and layout stripped away, and a test fails until the kernel is ported again or checked and the new hash recorded. It proves "looked at again", not "correct".
- **Twin engines.** Same seed, same page, one engine with one kernel and one without, the other kernels off so their rounding can't blur it. Goop, soap and pushing compare every array of the world; fire every saved field; air the wind on 12 grid sizes, then every array after 150 ticks. Each test also checks the kernel really ran.
- **The gate.** Every material, dry and wet paper, a fire, six layouts, all kernels on: at most 0.2 % of cells may differ by more than 2 × 10<sup>−3</sup>. The behaviour tests run the JavaScript loops and the apps run the kernels; this gate joins the two.

In the code for the next update next update, one known disagreement is pinned rather than hidden: six cells along the bottom edge of a tilted sheet, a few ten-thousandths apart in one dye. The test is marked as failing, so it says so the day it's fixed.

### Try it: the game's own air kernel against its twin

The game's own rule This runs the real thing: the 4,199-byte WebAssembly module the game ships for the wind's pressure solve, and a copy of its JavaScript twin. A swirl stirs the same wind on both sides, and after every solve each of the numbers is compared. **What to look for:** zero numbers differ, solve after solve. Then press **Nudge the twin by one bit**: it changes one weight in the JavaScript by the smallest step a 32-bit float can take, and the two drift apart within a solve. The times are measured on your device, right now.

![The game's own air kernel against its twin](<https://professoroops.com/_assets/entity_GAbLsTF282lGAmdCTvMl.png>)

Solves compared

Numbers that differ, last solve

JavaScript twin

WebAssembly kernel

| Kernel | What it works out | Numbers | Tested against its twin |
| --- | --- | --- | --- |
| Air | The wind's pressure solve, full multigrid | f32 × 4 lanes | **to the bit**, 12 grid sizes, every array after 150 ticks |
| Goop | Runny goop flowing between cells | f64, scalar | **to the bit**, 10 pages, every array |
| Paper | Water, dye and the wick in paper | f32 × 4 lanes | **within 10<sup>−5</sup>**, its own parity test |
| Floating layer | Oil and films on water | f32 × 4 lanes | **within 10<sup>−4</sup>**, every material |
| Fire | Heat spreading and burning | f64 × 2; f32 × 4 to find the hottest cell | **to the bit**, 9 pages, every saved field |
| Soap on milk | Marangoni flow carrying colour | f64, scalar | **to the bit**, 8 pages |
| Pushing | Fingers and things shoving goop | f64, scalar | **to the bit**, 14 pages and the redraw tiles |

How much faster each kernel made its own simulationPushing has no SIMD and was timed on oil pages. "goop, slow" is a kernel for goop's slow pass, later taken out. The goop flow kernel in the game has no recorded figure, and no tablet was measured.

Paper went 1.6 to 2.9 times faster, the oil layer 2.2 to 2.4, fire 1.0 to 1.8 and pushing 2.6 to 2.9; goop's slow pass reached only 1.1 to 1.2.

- least, across scenes
- most, across scenes

Data table

How much faster each kernel made its own simulation

|  | least, across scenes | most, across scenes |
| --- | --- | --- |
| paper | 1.6 | 2.9 |
| oil layer | 2.2 | 2.4 |
| fire | 1.0 | 1.8 |
| pushing | 2.6 | 2.9 |
| goop, slow | 1.1 | 1.2 |

Source: Desktop timings, each kernel against its own JavaScript loop on the same pages, measured when it was ported.

### A kernel stays only if it earns its keep

Each kernel was timed against its own JavaScript loop on the same page. Paper went 1.6 to 2.9 times faster, the floating layer 2.2 to 2.4. Fire gained least, 1.0 to 1.8, because its exact doubles allow only two lanes and many cells take slow paths. The pushing kernel has no SIMD at all and still gained 2.6 to 2.9 times.

The rule was to keep a kernel only at 1.2 times or better on its own sim. A WebAssembly copy of goop's slow pass \(soaking, staining, melting\) reached 1.1 to 1.2 and was taken out again. Every figure here comes from a desktop; there are no tablet measurements of the kernels on record.

### One memory, planned to the byte before the first stroke

Six of the kernels share one block of memory with the JavaScript. The goop height, the water and the temperature live there once, and every kernel and every JavaScript loop reads the same numbers in place. When the fire kernel warms a cell, the goop kernel sees it at once, with nothing copied and no message sent.

That memory is worked out in advance from what each kernel says it needs, and it never grows. If a phone hasn't room for all of it, the game builds the page again with fewer kernels, and if need be with none.

[Under the hood: one memory](<#hood-heap>)

One memory, planned to the byte before the first stroke

Under the hood

### One memory, planned to the byte before the first stroke

Each kernel declares which shared fields it reads and how much scratch room it needs. A field read by several kernels is placed once, and the whole plan is made for the page's size before the first stroke. The memory is created at its full size and never grows, because growing a WebAssembly memory would cut loose every view the JavaScript holds on it. An automated test holds the memory used to the plan, to the byte, on 15 page sizes, and checks that growing it fails.

Most of the page's busiest fields live in this shared memory, and most of those are read by two or more kernels. Rarer layers stay ordinary JavaScript arrays. Each try at a smaller set of kernels builds the world again, so memory is never kept for kernels that aren't running. Each kernel's constants have their own room, added after one kernel's tables overwrote another's in a test.

## Parts of the page where nothing moves get only a quick check now and then

The paper is a grid of up to 512 × 384 cells, each about half a millimetre across. The game cuts it into small squares, 768 of them on a big page. Each kind of simulation decides for every square whether something is going on there. A square of dried paint or bare paper rests, and gets only a quick look now and then, more often for goop that has just settled than for an empty square. Working only the parts of the page that changed is a known trick of falling-sand games <sup>[Purho 2019](<https://professoroops.com/en/inside/research-simulation/#purho-2019>)</sup>; here it's a little bookkeeping per square that the tiled simulations share.

When more squares want work than a pass can afford, each simulation's clock picks this pass's squares. The ones under the hand go first. Next comes any square that has waited too long, whatever else is going on. The rest go in turn. A square that waited isn't forgotten: it's told how many passes it missed and takes them in one longer step, up to a cap that keeps the step stable.

Each process catches up in its own way. A stain creeping towards its goop's colour goes by the steps the missed visits would have taken, so it never overshoots. Egg white sets by chance on each visit, so the game draws one random number, works out which missed visit it would have set on, and plays the visits before as raw and the rest as cooked.

Fire goes further. A square whose heat, neighbours, goop and material all come in exactly as they did last step, and which is cool and quiet, keeps last step's answer. On a warm dough page 680 of 768 fire squares sleep, for 4.6 times less fire work, and the page comes out identical to the bit.

[Under the hood](<#hood-tiles>)

Parts of the page where nothing moves get only a quick check now and then

Under the hood

### Parts of the page where nothing moves get only a quick check now and then

Six clocks run, one for each tiled pass: goop's flow and its slow local work, the floating layer's flow and local work, and the paper's two passes. Each remembers, for every square, when it was last worked and how long it has waited. When more squares want work than a pass allows, it picks in three steps:

- **The hand.** Squares under the hand go first, though never so many that the deadline loses its room. Squares near the hand and in its wake get a share of the pass, longest-waiting first.
- **The deadline.** Any square that has waited as long as the clock allows goes, over budget if need be. A seeded property test runs 60 random pages, budgets and hands for 500 passes each and finds no square waiting past that limit.
- **The round.** The rest go in blocks about one pass in size. Each round starts from a fresh, scrambled place on the page and walks its blocks in a changing direction, so no edge of the page is always last.

A late square steps the passes it missed in one go, up to a cap; the slowest kinds of work get a longer one. Long steps can't blow up, because the flows are clamped to what's stable. A clamped square says so, so a held-back puddle is never mistaken for a calm one, and a test checks it. Egg white's chance is played out from a single random draw: the chance it sets at least once in *n* visits with chance *p* each is 1 − \(1 − *p*\)<sup>*n*</sup>, and the same draw says on which visit. Held at a setting temperature, the share that sets matches visit by visit and in one go, pinned by a test.

The caps were measured. A shorter cap on the paper left a wash 11 % wetter after 30 s; the cap the game uses, 0.7 %. A ketchup stain at the lowest detail went from 31 % to 89.5 % of full-rate speed after 6 s once the slow local work got its longer cap. Fire only sleeps where it's cool and quiet, and wakes up regularly to check. Seven pages run with sleep on and off match on every saved field.

Try it: a page covered in paint on a slow tablet The game's own ruleEvery square here wants work, as on a page covered in moving paint. This is the game's own tile clock, ported for squares that want work \(the looks at settled squares are left out\): hand first, then the deadline, then the round. Each square shows a ripple of colour at the time it has caught up to, so a square that waited shows an older ripple. Squares outlined in yellow are worked this pass; a red corner means it waited longer than the cap and lost the rest. It runs slower than the game, so you can follow it. **Try:** switch the order to **rows** and watch a band sweep the page; press and drag to put a hand down \(or switch on **A hand in the middle**\) and see its squares never wait; pick the big page at detail 4 and watch time get lost, then the phone page, where it isn't.

![A page of tiles all wanting work, worked by the game's tile clock: tiles ripple with colour at the time they have caught up to, the tiles worked this pass are outlined, and tiles that lost time past the cap get a red corner. Press and drag to put a hand down.](<https://professoroops.com/_assets/entity_ru6Cpvjd67WtgSWcF3l5.png>)

Detail0 · 1601 · 1002 · 603 · 364 · 20Page288 × 216384 × 288512 × 384Orderblocksrows

tiles a pass—

tiles on the page—

a round—

a tile's step—

longest wait now—

time lost past the cap—

Passes until every square of a busy page gets a turnPages: 252 squares on a phone's 288 × 216, 300 small, 432 medium, 768 large. Above the dashed line, a page covered in moving paint loses time. At the lowest detail on a big page, a round takes about two thirds of a second.

At the lowest detail a big page of 768 squares takes 38.4 passes a round, past the catch-up cap of 16; a phone page of 252 takes 12.6. At the richest detail every page takes under 5.

- richest detail
- middle detail
- lowest detail
- catch-up cap

Data table

Passes until every square of a busy page gets a turn

| squares on the page | richest detail | middle detail | lowest detail | catch-up cap |
| --- | --- | --- | --- | --- |
| 252 | 2 | 4 | 13 | 16 |
| 300 | 2 | 5 | 15 | — |
| 432 | 3 | 7 | 22 | — |
| 768 | 5 | 13 | 38 | 16 |

Source: Arithmetic from the game's detail levels: the squares on the page divided by the squares a pass may take, against the catch-up cap.

![The game's hidden debug panel showing its tile map over a page covered in stripes of paint at the lowest detail, mid-stroke: orange squares where the paint was worked this pass and blue where the paper was, in a few blocks across the page and a cluster round the hand.](<https://professoroops.com/_assets/entity_VhpLANvAx1tzEQRzFMgP.webp>)

**The real game, mid-stroke at the lowest detail.** The game's own debug map lights the squares worked this pass, orange for the paint and blue for the paper: a few blocks from the round, and the squares round the hand, which go first.

![The debug panel's map of how far behind each square is, on a page covered in yellow paint at the lowest detail, a second after the finger lifted: rectangular blocks from yellow \(just worked\) through orange to red \(waited as long as the cap allows\), most of the lower half red.](<https://professoroops.com/_assets/entity_JnqDLsZp6SbMZnFZu6qq.webp>)

**How long each square has waited.** A second after the finger lifts, the waits lie in rectangular blocks laid down by the round: yellow squares were just worked, red ones have waited as long as their cap allows. At the lowest detail this page asks for more than a pass can give, so much of it is red, and there time is let go.

## When time is short, the hand goes first

On a device that keeps up, the game picks a level of detail and holds it while it fits. When steps start running long, it moves a level lighter, quickly. When there's plenty of room, it moves a level richer, slowly. Down fast, back slowly.

On a device the game has measured as slow, or one whose frames keep running long, it shares out each frame's time instead. It aims for 30 frames a second, or 60 when that fits with room. The work that has to happen comes first: the simulations that aren't cut into squares, the squares under the hand, and sending pictures to the screen. The background always keeps a guaranteed share of the frame, so it never stops catching up, and the hand's own pushing keeps a share too.

While a hand is on the page, the background gets its guaranteed share and no more. The frame finishes early, and what the hand does shows sooner. What is "under the hand" is wide on purpose: every finger, a thing skidding across the page, a thing landing, a burst. They all mark one shared map, which all six clocks read.

A heavy frame is cut into slices of a few milliseconds, and the worker takes in new touches between them. The slice ends at the next natural break in a step, so a long part can run over, but a touch no longer waits for a whole frame.

[Under the hood](<#hood-hand>)

When time is short, the hand goes first

Under the hood

### When time is short, the hand goes first

The scheduler starts from an estimate of what each kind of work costs and then measures it, believing a rise quickly and a fall slowly. Each frame's time is shared out in order of need: what must happen, the hand's squares, the hand's pushes, then the background, with a guaranteed floor for both the background and the pushes. A small feedback loop trims the budget from how long recent frames really took.

The background's time is split over the flow, the slow local work and the paper by one common measure: how much change each square owes, weighted up near the hand. The kind of work with the most to catch up gets the most squares. This decides how many squares, never which: the clocks still go by hand, deadline and order.

Hand focus works like heat. A square under a touch is hot, and the heat fades step by step. The hottest squares go first, then a ring round them, then the recent wake at a lower priority. Only so many steps fit in a frame; past that, time runs slow and is counted.

Five detail levels change how many squares a pass and how often slower work runs. They never change the size of a cell. Embers are physics and stay as many on any device; only particles that are purely for show thin out.

Try it: where one frame's time goes The game's own ruleThe bar is one frame, shared out by the game's own rule. The dashed line is the target \(1/60 or 1/30 s\), picked by a simplified form of the game's rule. The tiles under the hand are taken as nine for the picture. The time each part takes is listed under the bar. **Try:** lift the hand and watch the background take what's free; make the device slower and watch the target drop to 30 frames a second; push harder and watch the hand's pushes stop at their share, the rest waiting for the next frames.

![One frame drawn as a bar split into the fixed simulations, the tiles under the hand, the hand's pushes, the background tiles, packing and the replica, against a dashed line for the target frame time.](<https://professoroops.com/_assets/entity_Y2GtUdYTQx45ayxDRzSo.png>)

- One frame at 60 frames a second, 1 step
- One frame at 30 frames a second, 2 steps
- target
- budget B
- spare
- over the target
- fixed sims
- hand's tiles
- hand's pushes
- background tiles
- packing
- replica

Device1.0× slower

Hand pushes2.00 ms

frame—

target—

budget B—

fixed sims—

hand's tiles—

hand's pushes—

background tiles—

background, tile visits a step—

packing—

replica—

pushes left for later frames—

Live frames round the hand, of a 44-frame strokeKetchup: mean over ten strokes, 40 to 44; the test holds it to at least 41. Oil: the test's bound, at least 35. "Before" figures from the test's notes.

Smearing ketchup, 19 frames were live before the hand came first and 42.3 now; with a hand in oil on water, 28 before and at least 35 now.

- before the hand came first
- now

Data table

Live frames round the hand, of a 44-frame stroke

|  | before the hand came first | now |
| --- | --- | --- |
| ketchup smear | 19.0 | 42.3 |
| hand in oil on water | 28.0 | 35.0 |

Source: An automated test at the lowest detail on a 320 × 240 page.

### Tested at the slowest detail

A test smears ketchup across a 320 × 240 page at the lowest detail and counts the frames in which the stroke changed at least a quarter as many cells near the hand as it would with no limit at all. Before the hand came first, 19 frames of a 44-frame stroke were live. Now about 42 are. With a hand in oil on water, live frames rose from 28 to at least 35, and under the hand the goop, the oil and both paper clocks missed none of the passes they wanted.

Giving the hand everything was tried too. With all of a pass for the hand, the film of oil over the rest of the page waited so long that it left steps round the stroke, 1.71 against 1.63 with a share, and the deadline had to go over budget. So the hand gets a share, and the rest of the page keeps the remainder.

## Each device times itself and gets a page size it can carry

On its first launches, after an update and now and then after that, the game quietly times itself. In a moment when nothing is happening, a throwaway worker runs the real simulations on a small busy scratch page: six puddles of paint, a soaked disk of dye, two fires, confetti, glitter and a hot hair dryer. A fast device gets a 512 × 384 page, a middling one 384 × 288, a slow one 320 × 240. A phone's small screen sets its own limit, down to 288 × 216.

A smaller page doesn't shrink the brushes or the creatures. Everything is measured in cells, so a smaller page shows it all a little bigger and coarser. A page keeps the size it was drawn at; a new size only ever applies to a new page.

The rule is generous going up and careful going down. One good measurement raises the size; it takes several bad ones in a row to lower it. If a tablet plays several long sessions in a row mostly at the lowest detail levels, the next new page comes one size smaller.

[Under the hood](<#hood-size>)

Each device times itself and gets a page size it can carry

Under the hood

### Each device times itself and gets a page size it can carry

The timing runs the same engine the game plays with, kernels included, on a scratch page small enough that all of it works every step. A measurement is a handful of short runs. The first is dropped as warm-up and the **best** of the rest kept, because being busy elsewhere only ever makes a run slower. On a device with few cores it only steps while the table's own simulation is asleep, and a finger cancels the run.

Reported memory and the number of cores cap the size before anything is measured. The limits have a margin both ways, so a device that sits near one doesn't flip back and forth. The page is never bigger than the screen needs, and a grown-up can override the size in settings.

Why it matters: on a big page at the lowest detail, one round of a whole page takes longer than the catch-up cap, so a whole-page mess loses time there. On a small page the round fits inside the cap. Shortly after the first frame the simulations also warm up on a scratch page in idle time, so the browser has optimised them before the first big mess.

Where a device's score moves its page size IllustrationThe shape of the game's rule: each border has a margin both ways, so a page drops a size a little past it and rises a little before it. Down takes several measurements in a row; up takes one. **Try:** slide the device slower past a border and back again: the page keeps its smaller size until the device is clearly faster. On the slider each position stands for enough measurements, and the scale has no units: it shows the rule's shape, not its numbers.

![A scale of how long a device takes for one step, from fast on the left to slow on the right, split into a large, a medium and a small page. A page drops a size a little past each border and rises a size a little before it, so a device near a border doesn't flip.](<https://professoroops.com/_assets/entity_JwePgLbS6fcFOOaOcQCn.png>)

- large
- 512 × 384
- medium
- 384 × 288
- small
- 320 × 240
- fast
- slow
- time a step
- size down
- size down
- size up
- size up

Time a step, fast to slow30

the next new page—

### A two-hour wait comes out the same at every detail level

The time-skip buttons let a child jump ahead while paint dries or dough rises. A skip ignores all the rationing above. It works the same number of squares a pass whatever the device, clears the hand's focus, and takes longer on a slow tablet. A test waits two hours on three pages \(paint, oil, ketchup\) at detail 0, 2 and 4 and gets the same page, on 18 fields. Before this, at a slow tablet's budget, a three-hour skip left paint 7 % and oil 16 % less spread.

A skip doesn't play hours out step by step. Runny things get a fixed number of flow passes for each hour, up to a limit, and the rest is worked out with formulas per cell, such as a closed form for drying. The skip runs in short slices so the screen stays alive, and a stroke drawn meanwhile lands after it.

[Under the hood: time skips](<#hood-skip>)

A two-hour wait comes out the same at every detail level

Under the hood

### A two-hour wait comes out the same at every detail level

A skip is a job that can stop between any two pieces of work and pick up again, so it never holds the worker for long. Reactions over the skipped time follow the chemists' rule of thumb that a rate roughly doubles for every 10 °C warmer \(Q10 = 2\). One test compares a fingerprint of 18 fields at three detail levels, on one machine with the JavaScript loops. Another cuts an oven bake and a wait into the finest slices, with ticks in between, and gets every field, and the next random number, equal to one go. Two real devices aren't compared: page sizes differ, and browsers' maths functions can differ in the last digit.

### Undo keeps only the squares that changed

Undo used to keep whole copies of the page. Now each older step keeps only the squares that differ from the step after it, and an empty square costs almost nothing. After four strokes and a flood, undo on a desktop page went from 144 MB to 17 MB, and on a phone page from 46 MB to 6.6 MB.

So undo goes back as far as a fixed memory budget allows, with a floor and a ceiling on the number of steps: about 65 strokes on a dry 384 × 288 page and 11 on a wet one.

Undo memory after four strokes and a floodAn automated test holds it under a third of whole copies, on purpose a low bar.

On a desktop page undo went from 144 MB with whole copies to 17 MB with square diffs; on a phone page from 46 MB to 6.6 MB.

- whole copies \(before\)
- square diffs \(now\)

Data table

Undo memory after four strokes and a flood

|  | whole copies \(before\) | square diffs \(now\) |
| --- | --- | --- |
| desktop page | 144.0 | 17.0 |
| phone page | 46.0 | 6.6 |

Source: Measured with a benchmark of four strokes and a flood.

[Undo, under the hood](<#hood-undo>)

Undo, under the hood

Under the hood

### Undo, under the hood

Only the newest step is kept whole; older steps hold the squares that differ. Squares are compared bit for bit, so every value, even negative zero and not-a-number, comes back exactly, and a test pins it. If most of a field changed, the old copy is handed over whole instead of copied. The inner loops are written once for each kind of number they handle, because a loop that sees more than one kind of array runs at about half the speed. Taking a snapshot with every field changed went from 24 to 12 ms.

## Ideas that sounded faster, such as using the graphics chip, were tested and dropped

Speed work is full of ideas that sound faster. Each one here was built and measured against what was already there. These are the ones that didn't make it, with what the measurement said.

| The idea | What the measurement said | Outcome |
| --- | --- | --- |
| Run the paper and goop on the graphics chip | Against WebAssembly on the main thread it took 25 to 70 % off a paint or water frame, and nothing in the busiest. The worker already matched or beat it in every scene, without 3 to 4 ms of extra GPU time a frame \(desktop Chrome\). Not worth a third copy of the logic. | Left out |
| Repay clamped time with extra sub-passes | Built and tested. At the lowest detail on oil, film, ketchup and smear pages, the mean frame and the slowest one in twenty were within a few per cent either way. | Switched off |
| Give all of a pass to the hand | The oil over the rest of the page waited so long it left steps round the stroke, 1.71 against 1.63 with a share, and the deadline went over budget. | A share instead |
| Go round the page backwards every other round | Squares at the ends waited two rounds and then none; with a short cap they got back only 53 to 97 % of their time. | Blocks instead |
| Look at settled goop less and less often | It fell behind its neighbours and the step between them stood straight along the square's edge, 3.5 against 2.8. | A fixed rhythm |
| Work the wind on the paper in patches by turns | Seams, patches or grain. Bands along the wind replaced them, and a test keeps the seams in bounds. | Bands instead |
| WebAssembly for goop's slow local pass | 1.1 to 1.2 times faster, under the 1.2 the rule asks for. | Reverted |
| A first kernel for standing water | 1.0 to 1.15 times, worse on sparse tiles. Water kernels came later in another form. | Dropped then |

What hasn't been measured matters as much. There are no kernel timings from a real tablet on record, no measured memory total of a running page, and no recorded touch-to-screen latency. Much of this was tested on a desktop told to behave like a slow device: a development switch makes the worker busy-wait several times as long and scales the costs it measures, so the scheduler sees the device it's meant for. That's why so much could be tested without a tablet, and also why it still needs one.

## Every tool gets the speed tricks for free

None of the machinery here knows about ketchup or candles. It reads the same shared marks every simulation writes, so a new tool needs no scheduler code.

Shared: one shared map of the hand, read by six clocks

### Smear paint on a page full of paint, on a slow tablet

Under the hand, goop, oil and both paper passes miss no pass they want, and a test checks it.

[Runny, sticky and oily](<https://professoroops.com/en/inside/liquids/>)

Shared: fire remembers what came in last step

### Leave a warm dough page alone

680 of 768 fire squares sleep and the page comes out the same to the bit, pinned by a test.

[Heat and fire](<https://professoroops.com/en/inside/heat/>)

Shared: a shared mark of what changed

### Let a wet page dry

Only the pigment and water pictures are sent again, not the goop or the height, and a test checks it.

[Light and shadow](<https://professoroops.com/en/inside/light/>)

Shared: the floating layer, then the layers settling

### Let water creep under oil on the paper

The oil floats up on it, with the floating-layer kernel and without; the kernel gate checks both.

[Water in paper](<https://professoroops.com/en/inside/water/>)

Shared: the air's share of the frame

### Blow the hair dryer over wet paper on a slow device

The wind works in bands along its own direction, and a test keeps the seams within bounds.

[Air that really moves](<https://professoroops.com/en/inside/air/>)

Shared: a skip at one fixed rate, the hand's focus cleared

### Wait two hours after spilling paint and oil

The same page at three detail levels, pinned by a test.

[Dough, mould and time](<https://professoroops.com/en/inside/living/>)

7

WebAssembly kernels, each kept beside its JavaScript twin and tested against it.

0 bits

Difference between the air kernel and its JavaScript twin, on 12 grid sizes. Try it above.

19→42

Live frames of 44 round a hand smearing ketchup at the lowest detail, measured in a test.

4.6×

Less fire work on a warm dough page, where 680 of 768 squares sleep.

2.9×

The best speed-up a kernel gave its own simulation, measured on a desktop.

144→17 MB

Undo memory after four strokes and a flood on a desktop page.

## Where we simplify, and why

Every trick on this page trades something exact for something a tablet can afford. Here is each trade, and what it costs.

**In real life.** Time flows everywhere at once

**In the game.** Fixed 1/60 s steps; the paper and slow local work at half that rate, on alternate ticks; on a slow device squares step at different times and catch up in one longer step, up to a cap.

**Why that's enough.** Bounded work per frame. Neighbours a pass apart leave a faint step at square edges: on a slow page of oil over water 1.15× to 1.6× the steps inside squares, against 1.1× to 1.4× with no limit.

**In real life.** A late patch of paint should get all its time back

**In the game.** It gets back time up to a stability cap \(a longer one for the oil layer and slow local work\); the rest is dropped and counted. A clamped long step does less than it stands for.

**Why that's enough.** Stability first. A whole-page mess at the lowest detail on a big page does lose time, which is why slow devices get smaller pages.

**In real life.** The clock keeps real time

**In the game.** Only so many steps a frame; past that, time runs a little slow and is counted.

**Why that's enough.** Better than a spiral where every late frame makes the next one later.

**In real life.** The JavaScript's double precision

**In the game.** The paper and floating-layer kernels work in 32-bit lanes.

**Why that's enough.** Four lanes instead of two. Rounding stays under 10<sup>−5</sup> and 10<sup>−4</sup>.

**In real life.** The screen shows the present

**In the game.** One frame behind the simulation; on a slow device squares are uploaded on a budget, the hand's squares first, any square left waiting too long jumping the queue.

**Why that's enough.** The finger is never blocked. A far corner can look stale for a few frames, longer when the upload budget is starved.

**In real life.** "Visible change" is what an eye notices

**In the game.** Each sim's own calm threshold is the unit, and the page is either seen or hidden.

**Why that's enough.** Cheap and comparable across sims. It's a stand-in, never tested against human vision.

**In real life.** Quiet heat keeps changing a little

**In the game.** Fire reuses last step's result where nothing came in differently; at the lowest two levels some quiet squares also sit out every other step.

**Why that's enough.** Sleep changes nothing, to the bit. Sitting out costs 2 to 3 °C at worst on a hair-dryer page.

**In real life.** How fast a device is

**In the game.** One number, the best of a few short runs of one fixed scene.

**Why that's enough.** Stable on a busy device. It doesn't see graphics cost or a tablet warming up over time; the play record covers part of that.
