Hoofdthread → Web Worker
Invoer
Meteen: je vinger, op het moment dat hij beweegt.
Game Tailors
Bron: Snel op een goedkope tablet
De teken-app waarin het papier echt is. Geen reclame. Geen accounts. Geen persoonsgegevens: alleen anonieme tellingen, en die kun je uitzetten.
Game Tailors · Schieweg 15Y-40, 2627 AN Delft (NL)
KvK 92437702 · VAT NL866044474B01
info@gametailors.com · +31 6 4227 5919
© 2026 Game Tailors
Snel op een goedkope tablet
Elk plekje van de pagina heeft zijn eigen water, kleur, smurrie en warmte, en een grote pagina heeft bijna tweehonderdduizend plekjes. Een budgettablet kan ze niet allemaal zestig keer per seconde bijwerken. Dus het spel laat de rustige delen van de pagina rusten, doet zijn zwaarste sommen in een snellere taal en helpt de plek onder de vinger als eerste. De rest haalt het een moment later in.

Professor Oops draait als twee helften die niet op elkaar wachten. De ene helft luistert naar de vinger van je kind en tekent het scherm. De andere rekent uit wat de ketchup, het water en het vuur hierna doen. Die tweede helft draait in een Web Worker, een aparte werklijn die de browser aan een webpagina geeft. De twee geven elkaar vele keren per seconde briefjes door.
Dus een pagina die zwemt in de verf, kan de hand niet laten bevriezen. Terwijl de simulatie bezig is, tekent het scherm nog steeds de kwast of de lepel in de hand van je kind uit het nieuwste nieuws dat het heeft, elke keer dat het scherm ververst.
De tekenhelft heeft geen kopie van de wereld. Ze krijgt een replica: de dingen op de pagina (een bal, een kaars, een strooisel glitter), verstuurd als alleen wat er sinds het vorige briefje veranderde, en plaatjes van de vierkantjes papier die veranderden. Een test mislukt als tekencode rechtstreeks naar de simulatie grijpt, want dat soort fout werkt als alles samen draait en doet stilletjes niets in de worker. De kop van de test zelf zegt dat het vier keer is gebeurd.
Als de worker niet kan starten, schakelt het spel vanzelf over en draait het de simulatie in plaats daarvan naast het tekenen. Wat je kind al had gedaan, gaat mee.
Hoofdthread → Web Worker
Meteen: je vinger, op het moment dat hij beweegt.
Hoofdthread → Web Worker
De tijd die is verstreken.
Web Worker → hoofdthread
De veranderde tegels, de replica, de geluiden en de milliseconden die het kostte.
Hoofdthread → Web Worker
Teruggegeven voor het volgende beeld, nooit gekopieerd.
Uit het ontwerp van de worker van het spel. Een streek verschijnt één beeld later dan wanneer alles op één thread zou draaien; in ruil daarvoor wordt de vinger nooit geblokkeerd.
Zeven van de drukste lussen van het spel hebben een tweede, snellere versie in WebAssembly, een compacte vorm van code op laag niveau die browsers snel kunnen draaien. Ze dekken de druk van de wind, stromende smurrie, water en kleurstof die door papier kruipen, olie die op water drijft, vuur, zeep die over melk schiet en vingers die verf rondduwen. Vier ervan werken ook op meerdere getallen tegelijk (SIMD, “single instruction, multiple data”).
De leesbare JavaScript-versies blijven als referentie in het spel. De tests draaien beide op dezelfde rommelige pagina en vergelijken ze. Bij vijf van de zeven moet elk getal op de pagina tot op het laatste binaire cijfer overeenkomen. De andere twee, papier en de drijvende laag, ruilen een beetje afronding in voor snelheid en moeten binnen een honderdduizendste en een tienduizendste overeenkomen.
Als iemand de trage versie verandert en de snelle vergeet, houden de tests hem tegen. Als een telefoon de snelle versies helemaal niet kan draaien, gebruikt het spel de trage en speelt het bijna hetzelfde spel, met minder speling. Het ene bekende verschil: op het trage pad bij de twee laagste detailniveaus waait de wind iets zwakker.
De eigen regel van het spel Dit draait het echte werk: de WebAssembly-module van 4.199 bytes die het spel meelevert voor de drukoplossing van de wind, en een kopie van zijn JavaScript-tweeling. Een werveling roert aan beide kanten dezelfde wind, en na elke oplossing wordt elk van de getallen vergeleken. Waar je op let: nul getallen verschillen, oplossing na oplossing. Druk dan op Nudge the twin by one bit: dat verandert één gewicht in de JavaScript met de kleinste stap die een 32-bits float kan zetten, en de twee lopen binnen één oplossing uit elkaar. De tijden worden op jouw apparaat gemeten, nu.
| Kernel | Wat hij uitrekent | Getallen | Getest tegen zijn tweeling |
|---|---|---|---|
| Lucht | De drukoplossing van de wind, volledige multigrid | f32 × 4 banen | tot op de bit, 12 roostergroottes, elke array na 150 tikken |
| Smurrie | Smurrie die tussen cellen stroomt | f64, scalair | tot op de bit, 10 pagina’s, elke array |
| Papier | Water, kleurstof en het opzuigen in papier | f32 × 4 banen | binnen 10−5, eigen pariteitstest |
| Drijvende laag | Olie en vliesjes op water | f32 × 4 banen | binnen 10−4, elk materiaal |
| Vuur | Warmte die zich verspreidt en branden | f64 × 2; f32 × 4 om de heetste cel te vinden | tot op de bit, 9 pagina’s, elk bewaard veld |
| Zeep op melk | Marangonistroming die kleur meevoert | f64, scalair | tot op de bit, 8 pagina’s |
| Duwen | Vingers en dingen die smurrie wegduwen | f64, scalair | tot op de bit, 14 pagina’s en de tegels voor het opnieuw tekenen |
Elke kernel werd getimed tegen zijn eigen JavaScript-lus op dezelfde pagina. Papier werd 1,6 tot 2,9 keer sneller, de drijvende laag 2,2 tot 2,4. Vuur won het minst, 1,0 tot 1,8, omdat zijn exacte doubles maar twee banen toelaten en veel cellen trage paden nemen. De duwkernel heeft helemaal geen SIMD en won toch 2,6 tot 2,9 keer.
De regel was een kernel alleen te houden bij 1,2 keer of beter op zijn eigen simulatie. Een WebAssembly-kopie van de trage ronde van smurrie (intrekken, vlekken, smelten) kwam tot 1,1 tot 1,2 en werd weer weggehaald. Elk getal hier komt van een computer; er zijn geen metingen van de kernels op een tablet vastgelegd.
Zes van de kernels delen één blok geheugen met de JavaScript. De hoogte van de smurrie, het water en de temperatuur staan daar één keer, en elke kernel en elke JavaScript-lus leest dezelfde getallen ter plekke. Als de vuurkernel een cel verwarmt, ziet de smurriekernel dat meteen, zonder dat er iets wordt gekopieerd of een bericht wordt verstuurd.
Dat geheugen wordt vooraf uitgerekend uit wat elke kernel zegt nodig te hebben, en het groeit nooit. Als een telefoon er geen ruimte voor heeft, bouwt het spel de pagina opnieuw op met minder kernels, en zo nodig met geen.
Het papier is een rooster van maximaal 512 × 384 cellen, elk ongeveer een halve millimeter breed. Het spel snijdt het in kleine vierkantjes, 768 op een grote pagina. Elke soort simulatie beslist voor elk vierkantje of er iets aan de hand is. Een vierkantje opgedroogde verf of kaal papier rust, en krijgt af en toe alleen een snelle blik, vaker bij smurrie die net tot rust is gekomen dan bij een leeg vierkantje. Alleen de delen van de pagina bijwerken die veranderden, is een bekende truc van vallendzandspellen Purho 2019; hier is het een beetje boekhouding per vierkantje die de simulaties in tegels delen.
Als meer vierkantjes werk willen dan een ronde kan betalen, kiest de klok van elke simulatie de vierkantjes voor deze ronde. Die onder de hand gaan eerst. Daarna komt elk vierkantje dat te lang heeft gewacht, wat er verder ook gebeurt. De rest komt om de beurt. Een vierkantje dat wachtte, wordt niet vergeten: het hoort hoeveel rondes het miste en haalt die in met één langere stap, tot een grens die de stap stabiel houdt.
Elk proces haalt op zijn eigen manier in. Een vlek die naar de kleur van zijn smurrie kruipt, gaat met de stappen die de gemiste bezoeken zouden hebben gezet, dus hij schiet nooit door. Eiwit stolt bij elk bezoek met een zekere kans, dus het spel trekt één willekeurig getal, rekent uit bij welk gemist bezoek het zou zijn gestold, en speelt de bezoeken daarvoor als rauw en de rest als gaar.
Vuur gaat verder. Een vierkantje waarvan de warmte, de buren, de smurrie en het materiaal precies zo binnenkomen als bij de vorige stap, en dat koel en rustig is, houdt het antwoord van de vorige stap. Op een warme pagina met deeg slapen 680 van de 768 vuurvierkantjes, voor 4,6 keer minder vuurwerk, en de pagina komt er tot op de bit identiek uit.
Elk vierkantje hier wil werk, zoals op een pagina vol bewegende verf. Dit is de eigen tegelklok van het spel, overgezet voor vierkantjes die werk willen (de blikken op rustige vierkantjes zijn weggelaten): eerst de hand, dan de deadline, dan de beurt. Elk vierkantje laat een rimpeling van kleur zien op de tijd die het heeft ingehaald, dus een vierkantje dat wachtte, laat een oudere rimpeling zien. Vierkantjes met een gele rand worden deze ronde bijgewerkt; een rood hoekje betekent dat het langer wachtte dan de grens en de rest verloor. Het draait trager dan het spel, zodat je het kunt volgen. Probeer: zet de volgorde op rijen en kijk hoe een band over de pagina veegt; druk en sleep om een hand neer te leggen (of zet Een hand in het midden aan) en zie dat haar vierkantjes nooit wachten; kies de grote pagina op detail 4 en kijk hoe er tijd verloren gaat, en dan de telefoonpagina, waar dat niet gebeurt.
Op het laagste detail kost een grote pagina van 768 vierkantjes 38,4 rondes per beurt, voorbij de inhaalgrens van 16; een telefoonpagina van 252 kost 12,6. Op het rijkste detail kost elke pagina minder dan 5.
| vierkantjes op de pagina | rijkste detail | middelste detail | laagste detail | inhaalgrens |
|---|---|---|---|---|
| 252 | 2 | 4 | 13 | 16 |
| 300 | 2 | 5 | 15 | |
| 432 | 3 | 7 | 22 | |
| 768 | 5 | 13 | 38 | 16 |
Bron: Rekenwerk uit de detailniveaus van het spel: de vierkantjes op de pagina gedeeld door de vierkantjes die een ronde mag nemen, tegenover de inhaalgrens.


Op een apparaat dat het bijhoudt, kiest het spel een detailniveau en houdt dat vast zolang het past. Als stappen te lang beginnen te duren, gaat het snel een niveau lichter. Als er ruim tijd is, gaat het langzaam een niveau rijker. Snel omlaag, langzaam terug.
Op een apparaat dat het spel als traag heeft gemeten, of een waarvan de beelden steeds te lang duren, verdeelt het in plaats daarvan de tijd van elk beeld. Het mikt op 30 beelden per seconde, of 60 als dat met ruimte past. Het werk dat moet gebeuren, gaat eerst: de simulaties die niet in vierkantjes zijn gesneden, de vierkantjes onder de hand en het versturen van plaatjes naar het scherm. De achtergrond houdt altijd een gegarandeerd deel van het beeld, zodat hij nooit stopt met inhalen, en het duwen van de hand zelf houdt ook een deel.
Zolang er een hand op de pagina ligt, krijgt de achtergrond zijn gegarandeerde deel en niet meer. Het beeld is eerder klaar, en wat de hand doet, verschijnt eerder. Wat “onder de hand” is, is met opzet ruim: elke vinger, een ding dat over de pagina glijdt, een ding dat landt, een uitbarsting. Ze markeren allemaal één gedeelde kaart, die alle zes de klokken lezen.
Een zwaar beeld wordt in plakjes van een paar milliseconden gesneden, en de worker neemt tussendoor nieuwe aanrakingen op. Een plakje eindigt bij de volgende natuurlijke pauze in een stap, dus een lang deel kan uitlopen, maar een aanraking hoeft niet meer op een heel beeld te wachten.
De balk is één beeld, verdeeld volgens de eigen regel van het spel. De gestreepte lijn is het doel (1/60 of 1/30 s), gekozen met een vereenvoudigde vorm van de regel van het spel. Voor het plaatje zijn de tegels onder de hand op negen gezet. De tijd die elk deel kost, staat onder de balk. Probeer: til de hand op en kijk hoe de achtergrond neemt wat vrij is; maak het apparaat trager en kijk hoe het doel zakt naar 30 beelden per seconde; duw harder en kijk hoe het duwen van de hand stopt bij zijn deel, en de rest wacht op de volgende beelden.
Een test smeert ketchup over een pagina van 320 × 240 op het laagste detail en telt de beelden waarin de streek minstens een kwart zoveel cellen bij de hand veranderde als hij zonder enige grens zou doen. Voordat de hand voorrang kreeg, waren 19 beelden van een streek van 44 beelden levend. Nu zijn het er ongeveer 42. Met een hand in olie op water stegen de levende beelden van 28 naar minstens 35, en onder de hand misten de smurrie, de olie en beide papierklokken geen van de rondes die ze wilden.
De hand alles geven is ook geprobeerd. Met een hele ronde voor de hand wachtte het vliesje olie over de rest van de pagina zo lang dat het treden rond de streek achterliet, 1,71 tegenover 1,63 met een deel, en de deadline moest over het budget heen. Dus de hand krijgt een deel, en de rest van de pagina houdt wat overblijft.
Bij de eerste keren opstarten, na een update en daarna af en toe meet het spel stilletjes zichzelf. Op een moment dat er niets gebeurt, draait een wegwerpworker de echte simulaties op een kleine, drukke kladpagina: zes plassen verf, een doorweekt rondje kleurstof, twee vuurtjes, confetti, glitter en een hete föhn. Een snel apparaat krijgt een pagina van 512 × 384, een middelmatig 384 × 288, een traag 320 × 240. Het kleine scherm van een telefoon stelt een eigen grens, tot 288 × 216.
Een kleinere pagina maakt de kwasten of de dieren niet kleiner. Alles wordt in cellen gemeten, dus een kleinere pagina laat alles een beetje groter en grover zien. Een pagina houdt het formaat waarop hij is getekend; een nieuw formaat geldt alleen voor een nieuwe pagina.
De regel is royaal naar boven en voorzichtig naar beneden. Eén goede meting verhoogt het formaat; er zijn een paar slechte achter elkaar nodig om het te verlagen. Als een tablet een paar lange sessies achter elkaar vooral op de laagste detailniveaus speelt, komt de volgende nieuwe pagina één maat kleiner.
De vorm van de regel van het spel: elke grens heeft een marge naar beide kanten, dus een pagina gaat een maat omlaag een eindje erna en een maat omhoog een eindje ervoor. Omlaag vraagt een paar metingen achter elkaar; omhoog één. Probeer: schuif het apparaat trager voorbij een grens en weer terug: de pagina houdt zijn kleinere formaat tot het apparaat duidelijk sneller is. Op de schuif staat elke stand voor genoeg metingen, en de schaal heeft geen eenheden: hij laat de vorm van de regel zien, niet de getallen.
Met de tijdknoppen kan een kind vooruitspringen terwijl verf droogt of deeg rijst. Een sprong negeert alle rantsoenering hierboven. Hij werkt hetzelfde aantal vierkantjes per ronde bij, welk apparaat het ook is, wist de focus van de hand en duurt langer op een trage tablet. Een test wacht twee uur op drie pagina’s (verf, olie, ketchup) op detail 0, 2 en 4 en krijgt dezelfde pagina, op 18 velden. Hiervoor liet een sprong van drie uur bij het budget van een trage tablet verf 7% en olie 16% minder ver uitgevloeid achter.
Een sprong speelt de uren niet stap voor stap af. Vloeibare dingen krijgen per uur een vast aantal stromingsrondes, tot een grens, en de rest wordt per cel met formules uitgerekend, zoals een gesloten vorm voor drogen. De sprong draait in korte plakjes, zodat het scherm blijft leven, en een streek die intussen wordt getekend, komt erna op het vel.
Ongedaan maken bewaarde vroeger hele kopieën van de pagina. Nu houdt elke oudere stap alleen de vierkantjes bij die verschillen van de stap erna, en een leeg vierkantje kost bijna niets. Na vier streken en een overstroming ging ongedaan maken op een computerpagina van 144 MB naar 17 MB, en op een telefoonpagina van 46 MB naar 6,6 MB.
Dus ongedaan maken gaat zo ver terug als een vast geheugenbudget toestaat, met een bodem en een plafond voor het aantal stappen: ongeveer 65 streken op een droge pagina van 384 × 288 en 11 op een natte.
Snelheidswerk zit vol ideeën die sneller klinken. Elk idee hier is gebouwd en gemeten tegen wat er al was. Dit zijn de ideeën die het niet haalden, met wat de meting zei.
| Het idee | Wat de meting zei | Uitkomst |
|---|---|---|
| Het papier en de smurrie op de grafische chip draaien | Tegenover WebAssembly op de hoofdthread haalde het 25 tot 70% van een beeld met verf of water af, en niets bij het drukste. De worker deed het in elke scène al even goed of beter, zonder 3 tot 4 ms extra GPU-tijd per beeld (Chrome op de computer). Een derde kopie van de logica niet waard. | Weggelaten |
| Begrensde tijd terugbetalen met extra deelrondes | Gebouwd en getest. Op het laagste detail, op pagina’s met olie, vliesjes, ketchup en uitsmeren, lagen het gemiddelde beeld en het traagste op twintig binnen een paar procent naar beide kanten. | Uitgezet |
| Een hele ronde aan de hand geven | De olie over de rest van de pagina wachtte zo lang dat ze treden rond de streek achterliet, 1,71 tegenover 1,63 met een deel, en de deadline ging over het budget heen. | In plaats daarvan een deel |
| Om de andere beurt achterstevoren over de pagina gaan | Vierkantjes aan de uiteinden wachtten twee beurten en dan geen; met een korte grens kregen ze maar 53 tot 97% van hun tijd terug. | In plaats daarvan blokken |
| Steeds minder vaak naar rustige smurrie kijken | Ze raakte achter bij haar buren en de trede ertussen stond recht langs de rand van het vierkantje, 3,5 tegenover 2,8. | Een vast ritme |
| De wind op het papier om de beurt in stukken bijwerken | Naden, vlekken of korrel. Banden langs de wind vervingen ze, en een test houdt de naden binnen de grenzen. | In plaats daarvan banden |
| WebAssembly voor de trage lokale ronde van smurrie | 1,1 tot 1,2 keer sneller, onder de 1,2 waar de regel om vraagt. | Teruggedraaid |
| Een eerste kernel voor staand water | 1,0 tot 1,15 keer, slechter op dunbezette tegels. Waterkernels kwamen later in een andere vorm. | Toen geschrapt |
Wat niet is gemeten, telt net zo zwaar. Er zijn geen tijden van de kernels op een echte tablet vastgelegd, geen gemeten totaal geheugen van een draaiende pagina, en geen vastgelegde vertraging van aanraking tot scherm. Veel hiervan is getest op een computer die zich als een traag apparaat moest gedragen: een ontwikkelschakelaar laat de worker meerdere keren zo lang bezig-wachten en schaalt de kosten die hij meet, zodat de planner het apparaat ziet waarvoor hij bedoeld is. Daarom kon er zoveel worden getest zonder tablet, en daarom is er toch nog een nodig.
Niets van de machinerie hier weet iets van ketchup of kaarsen. Ze leest dezelfde gedeelde markeringen die elke simulatie schrijft, dus een nieuw gereedschap heeft geen plannercode nodig.
Gedeeld: één gedeelde kaart van de hand, gelezen door zes klokken
Onder de hand missen smurrie, olie en beide papierrondes geen ronde die ze willen, en een test controleert het.
Gedeeld: vuur onthoudt wat er bij de vorige stap binnenkwam
680 van de 768 vuurvierkantjes slapen en de pagina komt er tot op de bit hetzelfde uit, vastgelegd in een test.
Gedeeld: een gedeelde markering van wat veranderde
Alleen de plaatjes van pigment en water worden opnieuw verstuurd, niet de smurrie of de hoogte, en een test controleert het.
Gedeeld: de drijvende laag, en dan de lagen die zich zetten
De olie drijft erop omhoog, met de kernel voor de drijvende laag en zonder; het kernelhek controleert beide.
Gedeeld: het deel van het beeld voor de lucht
De wind werkt in banden langs zijn eigen richting, en een test houdt de naden binnen de grenzen.
Gedeeld: een sprong in één vast tempo, de focus van de hand gewist
Dezelfde pagina op drie detailniveaus, vastgelegd in een test.