A frame is not a duration; it is a budget. The distinction matters because budgets are negotiable, and durations are not. When we treat 16.6 ms as a goal, every subsystem feels licensed to be late. When we treat it as a contract, lateness becomes a defect with an owner.
This piece collects four years of frame-pacing notes from MiRiS. None of it is novel; all of it is hard-won. We start with the simplest model and add nuance only where the engine forced us to.
1.1 · The naive model
The naive model says: a frame is a function from Staten to Staten+1, and rendering is a side-effect. We measure the function. If it averages under 16.6 ms, we ship.
This model is wrong in three ways. It hides variance behind averages. It conflates the simulation step with the render step. And it ignores the GPU as an asynchronous coprocessor with its own budget.
1.2 · Variance is the product
Players do not feel an average. They feel the worst frame in a hundred. A 12 ms average with a 28 ms 99th-percentile is, in lived experience, a 28 ms game. Optimization that lowers the mean while raising the tail is a regression dressed as a win.
We track three numbers per subsystem, every frame: median, 95th, and 99th. The 99th is the only one that goes in the budget table. It is a harsh standard; it is the right one.
// 16.6 ms total. Numbers are p99, not mean.
constexpr FrameBudget kBudget = {
.input = 0.4, // ms
.gameplay = 3.2,
.animation = 2.1,
.physics = 2.8,
.culling = 1.0,
.render_setup = 1.7,
.audio_mix = 0.6,
.slack = 4.8, // for the GPU and the unknown
};
static_assert(kBudget.sum() <= 16.6, "frame overcommitted");
1.3 · The slack line
The last entry in that table — slack — is the most important one. It is the budget for everything we did not foresee: a level that loads a few more lights, a designer who adds one more particle system, a driver that hiccups for reasons unknown. A frame without slack is a frame already broken.
The cheapest optimization is the one we never have to make, because slack absorbed the surprise.
1.4 · Subsystems own their numbers
A budget is a contract; a contract needs a counterparty. Each subsystem in MiRiS has a single human owner, named in the source, responsible for the p99 of their slice. When the number drifts, the owner is paged — not the build engineer, not the producer, not the on-call generalist.
This sounds bureaucratic. It is the opposite. Bureaucracy is a budget that nobody owns, drifting upward by a tenth of a millisecond per sprint, until one Tuesday the game stutters and a five-person retrospective discovers that everyone contributed and nobody is responsible.
The footnote[1] at the foot of this article expands on the paging policy and the on-call rotation, which we revised twice before it stopped causing resentment.
1.5 · The GPU is a separate country
Everything above is the CPU's frame. The GPU has a frame too, and it speaks a different language. Submitting work in 4 ms of CPU time is meaningless if the GPU then takes 22 ms to drain it. We keep two budget tables, one per device, and a third that tracks the queue between them. The queue is where deadlocks hide.
We will return to the GPU side in §2. For now: trust nothing that is not measured on the device that does the work.
[1] The on-call policy is documented in the engineering handbook, chapter 7. The short form: an owner is paged once per regression, never twice in a week, and never on the weekend unless the regression is shipping-blocking. The point is to make the contract real, not to make engineers miserable.