makepad/skills/makepad-aaa-graphics-builder/references/render-budget.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

3.7 KiB
Raw Permalink Blame History

Render budget

apps/arcade/BUDGETS.md is the authority. This is the working summary and the method.

What the numbers say

Measured on an M-class Mac, release. The Quest column in BUDGETS.md is a conservative estimate until run on device — do not quote it as measured.

thing cost
100 movers + 50 rigid bodies + 65×65 terrain 0.038 ms/tick
60 Hz frame budget 16.6 ms
script per tick ≤ 2 ms, one cumulative 500k-instruction pool
entity lookup O(log n)

Simulation is ~0.2% of the frame. It is never your performance problem. Draw calls and CPU skinning are.

Bandwidth

stream before after
cube instance 44 floats / 176 B 32 floats / 128 B (27%)
skinned character vertex 16 floats / 64 B 6 floats / 24 B (62%)
shadow mesh vertex 16 floats / 64 B 6 floats / 24 B (62%)
terrain vertex 16 floats / 64 B unchanged (uploaded once per revision)

The skinned vertex matters most because it re-uploads every frame: the Knight at 3716 verts went from 238 KB/frame to 89 KB/frame. On a bandwidth-bound tiler that is the largest single saving available.

The instance saving came from moving sun_color, sun_sky, sun_ground, fog_color off the instance stream into uniforms — 12 floats of pure per-cube duplication.

Quest is bandwidth-bound before it is ALU-bound. Optimise bytes per frame first.

Measuring

Never estimate. Read it:

renderer.report_frame_ms(ms);   // -> bool: did adaptive quality change?
renderer.frame_p90_ms();        // Option<f32> - report p90, not mean
renderer.quality_level();
renderer.quality_reason();
stats.instance_floats;          // RenderStats: from the compiled shader
renderer.model_triangles(id);

Report p90, not average. A 16 ms average with a 40 ms p90 stutters, and the average hides it.

Headless evidence:

cargo run -p makepad-arcade --example bigworld_probe --release

Always measure in --release. A debug-build frame time tells you nothing about shipping.

Cutting, in order

  1. Instance/vertex bytes — packed layout everywhere, batch-constant values as uniforms.
  2. Draw calls — batch by material and mesh. Many small draws beat nothing.
  3. Skinned character count — the most expensive category per unit. Cap it; degrade distant characters.
  4. Shadow castersset_shadow_budget(n); bake AO for the rest.
  5. Terrain resolution — uploaded once per revision, so this is a memory and vertex-count question more than a per-frame one.
  6. Particle count — cheap individually, unbounded collectively. Cap the pool.
  7. Fragment work — last, and only with a measurement showing you are ALU-bound.

Adaptive quality

GameRenderer owns it: report_frame_ms each frame, set_refresh_hz for the device (72 Hz on Quest, not 60), set_shadow_budget, Stage / StageMode, and quality_reason() to explain a drop.

Do not build a second frame pacer. Two controllers oscillate against each other and produce worse frame times than either alone. If quality is dropping unexpectedly, read quality_reason() — that is what it is for.

Budget template

Fill this in for the target device before optimising, so you know when to stop:

target device   Quest 2, 72 Hz -> 13.9 ms
sim                       0.1 ms   (measured)
skinned chars    8 x      ? ms
shadow casters   24 x     ? ms
opaque draws     ? calls  ? ms
particles        ? max    ? ms
HUD                       ? ms
headroom         >= 20%

Twenty percent headroom is not padding: it is what absorbs the worst wave, the busiest camera angle, and thermal throttling on a mobile part after ten minutes of play.