microverse · headway:gamedev/deny-cool-license · 3d6abbc0db47
Every bounded resource in the engine already knew how full it was. None of them could be found without being named — and a budget nobody names is a budget nobody reports.
The measurement was never the problem. Arena, Pool, Order, DrawList and Graph each already carried a name, an occupancy, a capacity, a high-water mark and a refusal count. What none of them could do was answer a question asked from outside: what is this machine holding right now?
To answer it you had to name every primitive yourself. The shell's per-second report did exactly that, and it shows what that costs.
The failure worth preventing
println!( "draws {}/{} (peak {}) arena L {}/{} F {}/{}", draws.len(), draws.capacity(), draws.peak(), level.len(), level.capacity(), scratch.len(), scratch.capacity(), ); if draws.dropped() > 0 || stats.dropped > 0 { … } if level.overflows() > 0 || scratch.overflows() > 0 { … } draws.reset_stats(); scratch.reset_stats(); …
Four places name each budget: the format string, its argument list, an overflow branch, a reset_stats call. Miss one and the budget still exists — it just stops being reported.
let budgeted: &[&dyn Budgeted] = &[&world, &scratch, &draws, &renderer]; budget::visit_all(budgeted, &mut |b| { print!(" {} {}/{}{} (peak {})", b.name, b.used, b.cap, b.kind.unit(), b.peak); });
One slice literal, at the one site that knows what storage exists. Adding a budget is one word, and the overflow lines are driven off the same walk — so a budget cannot be reported in one place and forgotten in the other.
Try it
The panels below run the same two strategies against the same engine. Add a budget and watch which report notices.
Every budget is accounted for — because nothing has been added yet.
Four budgets, read from the live primitives.
The transform graph adds two lines from one call — it owns a pool and an ordering of that pool. That is the case an accessor returning a single report could not express.
The mechanism
Three decisions
The card said “every pool and arena registers.” There is no registration step.
A registry struct holding &dyn Budgeted would need a capacity — and a capacity picked before anything measures one is exactly the guess BUDGETS.md exists to replace. The registry is a slice literal, sized by the compiler. Nothing can be registered-but-forgotten because nothing registers.
Object safety forces it, and the aggregates settle it.
Vec<BudgetReport> allocates, which is denied in this crate. impl Iterator does not allocate but is not object-safe — so &dyn Budgeted could not exist and neither could the slice. And each level of nesting would chain another iterator type through the signature. The cost: a consumer cannot take two reports and stop. Nothing wants to.
A count past u32::MAX reports 4294967295.
Wrapping would turn a 5 GiB arena into a plausible small number and make the report quietly false. Saturating produces a figure no budget here has, which reads as “go and look.” Neither case is expected — this engine measures its budgets in tens of kilobytes — so the only question is which failure is legible.
The other half
The card also asked that a capacity refusal name the exhausted resource and the operation that wanted it — “not just None.” Checking the code first turned this from work into a test: it was already true everywhere.
| Refusal | Resource | Operation | Carries |
|---|---|---|---|
| Exhausted | arena | op | bytes, padding, used, cap |
| Overflow<T> | pool | op | the rejected value |
| Attach | graph | op | why it was refused |
| DrawList::push | via report | infallible | counted, not returned |
DrawList::push is the one deliberate exception. It is infallible so that rendering failures never reach every gameplay call site, so the guarantee it owes is carried by its budget report instead — the resource is named there, the refusal is counted there. That substitution is now a test rather than a comment.
So the deliverable became a check: tests/budgets.rs drives each primitive to capacity and reads back what it says, asserts a refused operation leaves occupancy and the high-water mark byte for byte as they were, and asserts the overflow count survives the frame boundary that clears the contents.
What it cost, and what is not done
DrawList was the only primitive without a name and gained one — thirteen call sites. The shell's Batcher gained a name and a peak it never had, and implements the core's trait: a budget the core has never heard of, sitting in the same enumeration, indistinguishable to a consumer. RenderStats.dropped was deleted, being the second copy the enumeration made unnecessary.
Unresolved handles deliberately stay out of the report. A command naming a mesh that is not loaded would have been dropped by a buffer with room to spare, so reading it as capacity pressure sends whoever is looking to the wrong place. It keeps its own counter and its own warning line.
There is no display on the machine this ran on — DISPLAY and WAYLAND_DISPLAY both unset — so the window never opens and the report line was never read back. The enumeration is covered by tests on both sides of the boundary; the format string in main.rs is not. Running it once with MICROVERSE_DRAW_COMMANDS=10 should make the overflow warnings fire.