Find out what the frame is actually spent on, and pick a target #35

Merged
icub3d merged 1 commit from 31-attribute-fixed-render-cost into main 2026-09-13 17:56:03 +00:00
Owner

ADR 0005 left roughly 15 ms of the mission view's frame unattributed and guessed it was
"the shape of a CPU bound". It is not. This attributes it, and settles the frame-rate
target that had been an assumption since the very first measurement.

The probe

src/world/perf.rs grows a second stage that hides the terrain, then the units, then the
light, then switches off MSAA and tonemapping — each rung taking one more thing away, so
consecutive differences name one cost each. Every rung also reports the main schedule's
wall time. Rendering is pipelined onto its own thread, so the gap between that and the
frame time is what the main thread spends waiting rather than computing.

The answer

Pixel Tablet, Android 17, Mali-G710, 2560x1600, free-running, of a 19.91 ms frame:

Cost ms Share
Terrain — 96 columns 1.91 10%
MSAA 4x 2.25 11%
Tonemapping 0.64 3%
Units 0.62 3%
Light 0.38 2%
Everything else 14.10 71%

With nothing drawn, no MSAA and no tonemapping, a frame still costs 14.10 ms against
16.67 ms for 60 fps. At that floor the main schedule is 5.94 ms and 8.16 ms is spent
waiting — so the floor is not the game's own code. 0005's guess was wrong in an
informative way.

Everything the project controls sums to 5.80 ms. Spending all of it still leaves 2.56 ms
of headroom for a game that needs 2.91 ms just to draw the board.

The decision

[ADR 0006] targets 30 fps. The budget becomes 33.33 ms, leaving 13.4 ms spare for the
HUD, animation and effects that do not exist yet.

The 31 fps that started this whole investigation turns out to be the target, reached: with
vsync at 60 Hz a 19.91 ms frame presents every second interval, which is 30 fps. Nothing
was ever broken — the number was being measured against a budget nobody had chosen.

30 fps is a real cost and ADR 0006 says so: it is visibly less smooth when the camera
orbits, and camera movement is something this game does.

Knock-on

  • #32 (anti-aliasing) reverts to a visual question. MSAA's 2.25 ms fits comfortably in
    33 ms, so it is about how the board reads, settled by looking at it. Issue rewritten.
  • #34 (reduce the engine floor) filed and deferred. One hypothesis already failed —
    bevy_pbr panics without GltfPlugin, which cannot be disabled independently in Bevy
    0.19 — and going further needs a profiler rather than A/B builds.

Bug fixed

The resolution stage inherited whatever the scene rungs had hidden, so on its first run it
measured an empty screen. It now restores the full scene first, and reads 19.58 ms at full
resolution against the 19.43 ms the standalone sweep in #29 reported.

Verification

cargo fmt --check, cargo clippy --all-targets --features perf -- -D warnings, and
cargo test (114 passing) are clean; the default build is checked too. No rendering
behaviour changes — the plugin-trim probe was reverted, and BUDGET_MS now reads 30 fps.

Closes #31

🤖 Generated with Claude Code

https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh

ADR 0005 left roughly 15 ms of the mission view's frame unattributed and guessed it was "the shape of a CPU bound". It is not. This attributes it, and settles the frame-rate target that had been an assumption since the very first measurement. ## The probe `src/world/perf.rs` grows a second stage that hides the terrain, then the units, then the light, then switches off MSAA and tonemapping — each rung taking one more thing away, so consecutive differences name one cost each. Every rung also reports the main schedule's wall time. Rendering is pipelined onto its own thread, so the gap between that and the frame time is what the main thread spends *waiting* rather than computing. ## The answer Pixel Tablet, Android 17, Mali-G710, 2560x1600, free-running, of a 19.91 ms frame: | Cost | ms | Share | | --- | --- | --- | | Terrain — 96 columns | 1.91 | 10% | | MSAA 4x | 2.25 | 11% | | Tonemapping | 0.64 | 3% | | Units | 0.62 | 3% | | Light | 0.38 | 2% | | **Everything else** | **14.10** | **71%** | With nothing drawn, no MSAA and no tonemapping, a frame still costs **14.10 ms** against 16.67 ms for 60 fps. At that floor the main schedule is 5.94 ms and 8.16 ms is spent waiting — so the floor is **not** the game's own code. 0005's guess was wrong in an informative way. Everything the project controls sums to 5.80 ms. Spending all of it still leaves 2.56 ms of headroom for a game that needs 2.91 ms just to draw the board. ## The decision **[ADR 0006] targets 30 fps.** The budget becomes 33.33 ms, leaving 13.4 ms spare for the HUD, animation and effects that do not exist yet. The 31 fps that started this whole investigation turns out to be the target, reached: with vsync at 60 Hz a 19.91 ms frame presents every second interval, which *is* 30 fps. Nothing was ever broken — the number was being measured against a budget nobody had chosen. 30 fps is a real cost and ADR 0006 says so: it is visibly less smooth when the camera orbits, and camera movement is something this game does. ## Knock-on - **#32** (anti-aliasing) reverts to a visual question. MSAA's 2.25 ms fits comfortably in 33 ms, so it is about how the board reads, settled by looking at it. Issue rewritten. - **#34** (reduce the engine floor) filed and deferred. One hypothesis already failed — `bevy_pbr` panics without `GltfPlugin`, which cannot be disabled independently in Bevy 0.19 — and going further needs a profiler rather than A/B builds. ## Bug fixed The resolution stage inherited whatever the scene rungs had hidden, so on its first run it measured an empty screen. It now restores the full scene first, and reads 19.58 ms at full resolution against the 19.43 ms the standalone sweep in #29 reported. ## Verification `cargo fmt --check`, `cargo clippy --all-targets --features perf -- -D warnings`, and `cargo test` (114 passing) are clean; the default build is checked too. No rendering behaviour changes — the plugin-trim probe was reverted, and `BUDGET_MS` now reads 30 fps. Closes #31 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
ADR 0005 left roughly 15 ms of the mission view's frame unattributed and
guessed it was "the shape of a CPU bound". It is not. This attributes it and
settles the frame-rate target that had been an assumption since the first
measurement.

`src/world/perf.rs` grows a second probe. It hides the terrain, then the units,
then the light, then switches off MSAA and tonemapping — each rung taking one
more thing away, so consecutive differences name one cost each. Every rung also
reports the main schedule's wall time, which matters because rendering is
pipelined onto its own thread: the gap between that and the frame time is what
the main thread spends waiting rather than computing.

On a Pixel Tablet, of a 19.91 ms frame:

    terrain (96 columns)   1.91 ms
    MSAA 4x                2.25 ms
    tonemapping            0.64 ms
    units                  0.62 ms
    light                  0.38 ms
    everything else       14.10 ms    71%

With nothing drawn, no MSAA and no tonemapping, a frame still costs 14.10 ms
against a 16.67 ms budget for 60 fps. At that floor the main schedule is 5.94 ms
and 8.16 ms is spent waiting, so the floor is not the game's own code — it is
what this engine costs to produce a frame on this device.

Everything the project controls sums to 5.80 ms. Spending all of it leaves
2.56 ms of headroom for a game that needs 2.91 ms just to draw the board.

ADR 0006 therefore targets 30 fps. The budget becomes 33.33 ms, which leaves
13.4 ms spare for the HUD, animation, and effects that do not exist yet — and
the 31 fps that started this investigation turns out to be the target, reached.
With vsync at 60 Hz a 19.91 ms frame presents every second interval, which is
30 fps. The number was being measured against a budget nobody had chosen.

MSAA stays: 2.25 ms fits comfortably in 33 ms, so #32 reverts to the question it
should always have been — how the board reads, settled by looking at it.
Reducing the floor is #34, deferred, because one hypothesis already failed
(`bevy_pbr` panics without `GltfPlugin`, which cannot be disabled independently)
and going further needs a profiler rather than A/B builds.

Also fixes a bug in the harness: the resolution stage inherited whatever the
scene rungs had hidden, so it measured an empty screen. It now restores the full
scene first, and reads 19.58 ms at full resolution against the 19.43 ms the
standalone sweep reported.

Closes #31

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
icub3d merged commit ede99f899f into main 2026-09-13 17:56:03 +00:00
icub3d deleted branch 31-attribute-fixed-render-cost 2026-09-13 17:56:03 +00:00
Sign in to join this conversation.
No description provided.