Freezing the simulation is the easy half. You gate the update loop, the hazards stop, the timer stops, and on your own screen the game looks properly paused — which is exactly why the remaining half goes unnoticed. The hard half is that everything which measures the game has to agree it is paused, and that the pause has to know who is holding it. In one week we rediscovered that shape in five of our own games, from five different directions, and filed it five times before anyone said out loud that it was one problem.
The courtesy is the settled part
What a game owes an interrupted player is not in dispute here. We have made that argument already: the instant the game is no longer in focus, nothing that can harm the player may keep running — stop the clock, freeze the hazards, halt the simulation. That is the courtesy, stated. This post is the other half of the job.
The difficulty has a specific cause. For the interruptions the platform owns — a call, a lock screen, an app switch — the operating system hands you a callback, and a competent app hangs its pause on that signal and is done. Nothing hands you a callback for one of my own widgets is now covering the board. A menu, a win overlay, a round briefing, a rewarded ad are things your own code did on purpose, so each is wired by hand, and each is a place a wire can be missing. Our games are free and ad-supported with a single optional Pro purchase that removes the ads, so an ad presentation is itself a surface the engine must hold a pause for.
The gate was right; its input was lying to it
The clearest example is from Iron Swarm, our wave-defence
tank game. It awards mastery progression only for what the code calls a genuine run,
and the test for a genuine run is an OR: you either reached wave two, or
you stayed alive for sixty seconds. The second branch exists to be generous —
plenty of honest runs end early and badly — and the gate’s own
documentation comment said, in as many words, that its job was to block an
instant-suicide farm loop.
That branch was measured with a wall clock: time of day at the run’s start against time of day at the run-end seam. A wall clock keeps accruing while the simulation is correctly frozen. So the exploit is: start a run, hold any pause for sixty seconds — the pause menu, the round briefing before you press START, a backgrounded app, a rewarded ad — then resume and die at wave zero or one. The waves branch fails. The seconds branch passes. The run banks progression. Time spent doing nothing counted as time spent playing; it required no skill, and the backgrounded variant ran unattended. The pause machinery worked perfectly throughout. The thing keeping score simply never asked it anything. Our own audit put the root cause in one line: a gameplay-semantic quantity sourced from wall-clock instead of simulation time, making all eleven pause reasons invisible to it.
That is the bitter part: a gate correctly designed for exactly the abuse it was written to stop, fed a number that did not mean what its name said. (The same audit found progression banking more than once per run — a different root cause, not this story.)
Why we did not just subtract the paused time
There is an obvious cheap fix, and rejecting it is the generalisable lesson. Keep the wall clock, track how long the game was paused, subtract. It works — and it is a promise to correctly bracket every paused interval forever, across overlapping reasons and the backgrounded case where the process may not even be scheduled to notice its own resume. Get one bracket wrong and the defect is back with a smaller footprint. Worse, it re-opens the hole the moment somebody adds a twelfth reason, which, as it happens, somebody did that week.
So the number changed its source instead. The run now carries a
simulation clock accumulated from the engine’s own delta time, incremented inside
update after every gameplay-tick early return — pause gate,
hit-stop, async round rebuild — so it advances if and only if the world actually
stepped that frame. That is correct for every pause reason that will ever exist, including
the ones nobody has written yet. Nothing else moved; the defect was never in the
gate’s design. When a quantity is wrong, ask whether the consumer is broken or
the source is lying before you touch the consumer. Deriving a clock from the thing
it measures is the same instinct as
deterministic by design: make the
artifact structurally incapable of disagreeing with reality.
One thing we will not dress up: we found this by reading code, not by playing it. The host was so contended that day that one static-analysis run took 937 seconds, so the exploit was not executed at audit time — and the audit said so in writing, because a green test is necessary but not sufficient for a timing defect, the standard we set for our automated review lane. Device verification came after the fix shipped: hold the pause menu past sixty seconds, resume, die at wave zero or one, bank nothing. That is the wrong way round, and it is what happened.
A visible overlay is a statement about painting, not about hit-testing
The same family produced a second defect in Sudoku by
WizusLabs, and this one reached players. Two taps on an already-finished puzzle could
resurrect it and restart its clock, inflating the solve time the player was looking at. We
first assessed it as latent, because the win overlay is drawn on top of the board and the
control in question sits underneath it. Then a hit-test probe at that control’s
location, with the win overlay presented, came back
reachesTopBar=true reachesWinOverlay=false. The overlay was not absorbing
pointer events over the top bar. Live, not latent, and a player’s thumb was all it
took.
Being on top is a fact about paint order. Absorbing input is a separate property something in the widget tree has to actually claim, and the two get conflated constantly because on your own screen they look identical: the thing is covered, so surely nothing behind it can be touched. It can. So we stopped trusting the eyeball and enumerated all 22 blocking overlays in the app for pointer-barrier safety, rather than waiting for a third instance to teach us the same thing.
Who is holding the pause?
The second family is a different problem that looks identical from outside, and the honest place to start is what went right. Iron Swarm’s five independent pause owners were collapsed into one arbiter holding a set of reasons rather than a boolean, which makes both classic resume bugs structurally impossible: the pause edge fires only when the set goes from empty to non-empty, and every release removes its own reason and then checks the set is empty before resuming. Releasing what you never acquired is a no-op; acquiring twice and releasing once cannot strand the game paused. Those two bugs ship with every hand-rolled pause flag, and they were gone by construction.
Then the crack. One reason value had two independent producers, and because the arbiter is a set rather than a per-owner counter, a reason is either held or not — so whichever producer released first released the other’s intent with it, resuming the simulation with a blocking surface still on screen. Reason sharing, not nesting depth, is what breaks a set-based arbiter. No currently reachable sequence had both producers live at once, so it was filed as an observation rather than a bug — and fixed anyway, with a one-reason-one-producer rule, because it is exactly the shape that bites the moment a new surface reuses an existing reason.
Embergrid is the mirror image, and it is the one with proof. There nesting depth is counted, and that choice was demonstrated rather than assumed: a bare acquire-and-release pair is not nesting-safe, because cleanup unwinds inner before outer — so the inner release empties a single-member owner set while the outer overlay is still visible, un-pausing the world underneath something the player is looking at. A deliberate mutation of the test suite reproduced exactly that, which is how we know the depth counter is load-bearing rather than decorative; the owner set is asserted at 10 observation points in a runtime gate that passes on a real device. Side by side the two are one discovery from opposite ends: a set fixes counting only if you also settle who may put things in it. Boardlore is a third variant in one sentence — its overlay-pause observer implements only the four route callbacks, so a visible non-route overlay bypasses it with the clock running behind, which is how it turned up in a guard nobody has seen fail.
What the correct version costs
So what does doing this properly cost? In NeuralSpark, our brain-training app, overlay pause is live across 84 hosts — and it got there through one universally-invoked seam, not 84 edits. That is the payoff, and it is only available if pause is a service with owners. A boolean each screen sets cannot be fixed centrally, because there is no centre.
The cost is real and worth naming: an owner set with depth
counting and a producer rule per reason is meaningfully more machinery than
paused = true, and it is machinery a new contributor has to learn before
adding a menu. It earns that for one reason. A boolean makes correctness a property of every
call site, and there is no such thing as a game whose call sites all stay correct. An
arbiter makes it a property of one file — and the defects that survive, as ours did,
stop being “somebody forgot” and become questions with answers: who produces
this reason, and does anything keeping score still believe a different clock?
Freezing the simulation is the easy half. The hard half never announces itself, because a paused game looks paused — and the parts that were still running were the parts nobody was watching.
Notes
This is a first-hand account of defects in our own games, not a survey of anyone else’s. Every count here is read out of our own five repositories the week of 2026-07-29 — five games, eleven pause reasons at the time of the audit (the arbiter gained a twelfth within the week), five pause owners collapsed into one, the sixty-second gate branch, 22 blocking overlays enumerated, 10 owner-set observation points, 84 overlay-pause hosts, and one static-analysis run that took 937 seconds. They are facts about a moment in five codebases, not benchmarks, and they will drift the next time any of those games ships. There are no performance figures and no before-and-after quality metrics in this post, because we have none we would stand behind; inventing one to look rigorous would undercut the argument.
Two things stated plainly rather than softened. One of these defects was reachable by players — the finished-puzzle clock in Sudoku — with its reachability established by our own on-device hit-test probe after the fix shipped; we are not filing it as a near-miss. And the progression exploit was found by reading code, not by exploiting it: the run on a device came after the fix shipped, which is the wrong order and is recorded that way in the project’s own audit. Our games are free and ad-supported, with a single optional Pro purchase that removes the ads.
Keep reading: all posts on the WizusLabs Engineering blog.