Profiler: real-GPU breakdown by zoom, look and scene, with a baseline (t88 phase 0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 18:31:43 -04:00
co-authored by Claude Opus 5.5
parent 0c0edf46ca
commit 9781aa3ee1
17 changed files with 38797 additions and 57 deletions
+14 -5
View File
@@ -20,7 +20,7 @@ src/ui/ the browser shell: input, HUD, build menu, notebook, setti
audio, the frame loop
test/ headless suites (t_*.js) and their harness; browser tests (play.py,
t_smooth.py, t_dev_console.py); run_all.py
tools/ serve.py (build, serve, rebuild on change), shot.py (screenshots),
tools/ serve.py (build, serve, rebuild on change), server.py (isolated HTTP), shot.py (screenshots),
where.py, map.py, perf.py, playthrough.py, use_venv.py (switches browser
scripts to .venv), scenes/ (screenshot setups), experiments/
(measurements and reports)
@@ -405,7 +405,7 @@ Screw casing geometry shared by both looks, ghosts and placement previews. smoot
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/world.js (404 lines)
### src/render/world.js (419 lines)
Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld
@@ -418,7 +418,7 @@ Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, t
- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
### src/render/smooth.js (99 lines)
### src/render/smooth.js (103 lines)
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes
@@ -553,7 +553,11 @@ Shared world colours, minimap and full map.
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
- variables: MAP_S, MAP
### src/ui/loop.js (140 lines)
### src/ui/perf.js (15 lines)
Browser-only clocks; simulation code stays usable without a browser or a clock.
- variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast
### src/ui/loop.js (148 lines)
The frame loop and boot.
- functions: scanVisible, frame, boot
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
@@ -583,6 +587,7 @@ The frame loop and boot.
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
- `test/t_perf.js`: Profiling fixtures start in the same state after previous scenes and warm-up ticks.
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
@@ -597,10 +602,14 @@ The frame loop and boot.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
- `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
- `tools/perf.py`: Times the renderer and the simulation in a headless browser: light, drawWorld (with and without light and glow), the pixel upload and blit, and one simulation tick, in ms. Rebuild first.
- `tools/perf-measure.js`: Temporary wrappers live only in the profiling page, never in the simulation bundle.
- `tools/perf-scenes.js`: Seeded worlds, no storage round-trip (saves quantize heat and land flying grains).
- `tools/perf.py`: Profile a seeded world on the real GPU, with isolated HTTP serving. Times are milliseconds.
- `tools/playthrough.py`: Headless browser run: load the page, start a world, play a little, take screenshots.
- `tools/serve.py`: Build, serve and rebuild when sources change: python3 tools/serve.py [--port 8000].
- `tools/server.py`: Serve dist/ with isolation headers; isolated_server() owns a free-port background server.
- `tools/shot.py`: Screenshot the game in a headless browser, after setting up a scene with JavaScript.
- `tools/test_perf.py`: Profiler checks: headers, summaries/comparison, and unchanged pixels/simulation with clocks on.
- `tools/use_venv.py`: Let browser scripts run with plain python3 by switching to the repo's .venv.
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
+61 -2
View File
@@ -106,7 +106,7 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
## Pitfalls
- **WebGL in headless Chromium.** There is no GPU, so the smooth look needs SwiftShader:
- **WebGL in headless Chromium.** The browser tests use SwiftShader for portable graphics checks:
launch with `--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader`. Also set
`window.__allowSoftwareGL = true` before the page loads (`add_init_script`); otherwise the game
sees software rendering and switches to the pixel look. `tools/shot.py` does both.
@@ -128,7 +128,66 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
## Measuring
- `tools/perf.py`: renderer and tick times in the browser.
- `tools/perf.py`: the phase 0 CPU/frame profiler, served over HTTP with COOP/COEP isolation.
Rebuilds automatically. Run on an otherwise idle machine, without another browser test:
```sh
python3 tools/perf.py --json tools/perf-baseline.json
python3 tools/perf.py --json /tmp/perf-now.json --compare tools/perf-baseline.json
python3 tools/perf.py --viewport 1920x1080 --world 2048x1024 --zooms 12,6,4,3,2 \
--looks pixel,smooth --scenes settled,factory,heat --seconds 10 --json /tmp/perf.json
python3 tools/test_perf.py
```
The default matrix is three scenes × two looks × five zooms × lighting full/off. The world
seed is 424242; fog is off so all visible grains are shaded. Settings otherwise use game
defaults (including glow and the minimap), with autosave, camera lead and shake disabled.
`settled` runs 600 ticks; `factory` reuses the furnace/transport layout from
`experiments/factory_cost.js`, adding screws, a sand filter and full silos for sustained sand
and ore flow; `heat` places water over magma beside a rock floor. The latter two settle for
another 60 ticks. Each rebuild resets the power-rebuild scheduler, weather random stream and all statistics to
fresh-page values; fixture hashes are checked by `test/t_perf.js`. No save/load snapshot is used: it would quantize heat and land flyers.
**World** is the whole `drawWorld` CPU call. **Light** is the lighting work inside that call;
**Prep** sets up the view/masks/joins; **Colour** fills CPU grain buffers (including ghosts and
flyers); **Upload** submits the pixel buffer or smooth textures; **Blit** submits drawing,
including pixel light/glow composition or the smooth shader and Canvas2D copy. These are CPU
wall times, including driver stalls; they are not GPU timestamp measurements. In pixel look,
the glow upload is part of Blit. The separate `computeLight_ms` times an unconditional light
call even when the row has lighting off. World includes Light: do not add them together.
Overlay totals and each individual draw call are in JSON, with ms/frame and invocation counts; conditional
overlays absent from the fixture have zero calls, not a measured zero execution cost.
Five calls warm each render configuration before 30 measured calls. Simulation warms five
full 120-tick periods on a disposable scene, then rebuilds the same seeded fixture and
measures 240 consecutive ticks. Every direct `simTick` callee is discovered from its source;
JSON records per-call and amortized per-tick costs and invocation counts, including periodic
functions. Inactive paths (e.g. load wake, saplings, flow tracking) have zero calls and null
per-call cost. Unattributed time includes tick bookkeeping and clock/wrapper overhead.
Awake/hot chunk counts are recorded at the start/end of simulation and real runs.
Real-loop tests independently rebuild each scene for zooms 2 and 6 in both requested looks,
regardless of `--zooms`. `--seconds` applies to each of these twelve default runs. Raw samples
retain frame interval (rAF timestamps, including waiting/scheduling), CPU frame work, total
simulation work per frame and number of ticks. p50/p95/p99 use interpolated sorted samples;
five warm-up frames are excluded. Zero-tick frames correctly have zero simulation time.
JSON also records the smooth renderer's adaptive resolution scale: compare it as well as time.
Comparisons require identical options, print all timing changes, and mark >10% regressions;
tiny timings and tails can be noisy, and a zero baseline has no meaningful percent change.
Performance runs launch Chromium with
`--use-angle=vulkan --enable-features=Vulkan --enable-gpu --ignore-gpu-blocklist --enable-gpu-rasterization`.
They record the unmasked GPU/renderer, cores, browser version, viewport/world size,
`crossOriginIsolated`, `navigator.gpu`, source hash and elapsed time. They refuse software
renderers or missing isolation. Chromium needs access to the host GPU devices; a filesystem
sandbox that hides `/dev/dri`/NVIDIA devices must be lifted for this measurement. The browser
tests' SwiftShader numbers are not a performance baseline.
The existing in-game Performance stats toggle shows smoothed CPU world/light/overlay times
and movement/heat/rest per simulation tick. Detailed renderer clocks are off by default
(`PERF_MARKS = false`); the raw frame hook (`PERF_FRAME`) is also off outside the profiler.
`tools/server.py` shares the isolation/no-cache headers with `tools/serve.py`, and supplies a
free-port background server with cleanup for measurement tools.
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
- `tools/experiments/flow.js`: belt and screw conveyor throughput in grains a second.
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
+17
View File
@@ -4,6 +4,23 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-27: t88 phase 0, profiler and baseline (Claude Code + Codex)
- `tools/perf.py` rewritten: real GPU (ANGLE Vulkan flags), served by the new `tools/server.py`
with COOP/COEP (`tools/serve.py` sends them too), zooms 12/6/4/3/2 x pixel/smooth x scenes
settled/factory/heat (`tools/perf-scenes.js`), drawWorld split by `PERF_MARKS`, per-step sim
timings, 10 s real runs with p50/p95/p99, `--json`/`--compare`. Baseline `tools/perf-baseline.json`.
In-game perf readout shows world, light, overlays, move, heat, rest (`src/ui/perf.js`).
- Baseline (1920x1080, RTX 5070): zoom 2 smooth heat scene drawWorld 27.4 ms (colour loop 21.1,
light 5.5, upload 0.6); sim 3.7-4.9 ms/tick (simMove 2.0-2.3, heatPass 1.5-2.6); real loop at
zoom 2 runs at 30 fps (CPU work p50 30-37 ms), at zoom 6 at 60 fps (7-9 ms).
- Files: `tools/perf.py`, `tools/server.py`, `tools/serve.py`, `tools/perf-measure.js`,
`tools/perf-scenes.js`, `tools/test_perf.py`, `tools/perf-baseline.json`, `src/ui/perf.js`,
`src/render/world.js`, `src/render/smooth.js`, `src/ui/loop.js`, `src/styles/style.css`,
`src/manifest.json`, `test/t_perf.js`, `docs/TESTING.md`, `docs/PROJECT_MAP.md`.
- Tests: `python3 test/run_all.py --browser` all passed. Not measured yet: GPU time (timestamp
queries come with WebGPU), Firefox.
## 2026-10-27: OS pointer hidden over the game (Claude Code)
- The canvas used `cursor: crosshair`, so the system pointer showed on top of the game's own aim;