You're right — the telemetry path itself was contributing to perceived
browser lag, not just the GPU work. Two specific sources removed:
render/mod.rs::rebuild_chunk:
- Per-chunk log::info!("rebuild_chunk {coord}: {ms}ms ...") fired
~289 times during world init, plus on every edit. Chrome's console
buffer accumulates these and DevTools recalcs on each new line.
Removed entirely. The bench toggles + FPS HUD already give us the
measurement signal; per-chunk timing was noise once verified.
web/main.js setupFpsHud:
- Was a 5 Hz setInterval that did `el.textContent = ...` and two
`classList.toggle` calls each tick (15 DOM writes/sec). Now:
* Polls at 1 Hz (the EMA already smooths)
* Only writes textContent when the displayed string actually
changes (skip is the common case once stabilized)
* Only writes class when the tier (green/warn/bad) crosses
a threshold
Same readout, ~15× fewer DOM writes.
web/main.js setupStatusLoop (HP bar):
- Same dedupe pattern. 10 Hz polling, but DOM writes only when
hp or alive changes from the last sample. Was firing 3 style
writes per tick unconditionally; now zero in steady state.
No functionality changes — every telemetry getter, every bench
toggle, every test-harness export still works (just verified via
the 18-check functional contract suite).
Wasm release built; tests 63/63 pass.
The features added in Rounds A–D were correct but expensive. The hot
path per frame was sky_color() called from apply_fog for EVERY distant
pixel — 4-octave cloud fbm + star hash + sun/moon disc per fragment,
hundreds of thousands of pixels per frame. Profile-driven cuts that
keep all features but stop paying for them in the wrong places:
1. apply_fog now mixes terrain toward sky_dome (cheap gradient) not
sky_color (gradient + clouds + sun + moon + stars). Distant terrain
still fades to the right-direction sky color at every time of day;
the per-pixel cost drops by ~80%. Full sky_color still runs for
the SKY BACKGROUND pass where it's actually paid for.
2. Sky pipeline draws AFTER terrain with depth_compare = LessEqual.
The full-screen sky was previously written first then over-painted
by terrain — sky's expensive fragment shader ran on every screen
pixel. Now it only runs on pixels with no terrain in front of them
(depth = 1.0 cleared), which on most views is 30–60% of the screen
instead of 100%.
3. fbm2 reduced from 4 → 3 octaves. Negligible visual change at the
scales we sample, ~25% cheaper per cloud-pixel.
4. Cloud branch skips entirely when day_strength < 0.05 (full night).
Clouds invisible at night anyway, fbm + smoothstep + mix skipped.
5. In-game FPS HUD (top-right corner):
- Telemetry struct gains frame_dt_ms (EMA-smoothed in app.rs
with coefficient 0.85 so the number is readable, not flickery).
- wasm bridge: get_frame_dt_ms().
- main.js setupFpsHud() polls it at 5Hz, color-coded:
green ≤ 18ms (≥55fps), amber 18-33ms, red beyond.
- Reads what THE GAME measures, not the browser's
requestAnimationFrame which gets throttled to 1 Hz on
unfocused windows.
No features removed. God rays, FXAA, ACES tonemap, bounce baking,
specular materials, leaf translucency — all still there. Tests:
63 passing. Wasm release clean.
The death overlay sat at z-index 100 while the settings menu sat at
z-index 60, so a player who died and then opened the menu (or whose
respawn timing overlapped with opening the menu) would see the red
death curtain on top of every settings widget. Worse, the overlay
also applies `backdrop-filter: blur(2px)` which would have tinted the
menu visibly even if z-order had been fixed differently.
Fix: a single CSS rule
body.menu-open #death { display: none; }
makes the menu and death overlays mutually exclusive whenever the
menu is open. The death overlay returns immediately when the menu is
closed (assuming the player is still dead). Verified live in the
running browser via the test harness:
body.dead alone → death visible (display: flex)
body.dead + body.menu-open → death hidden (display: none)
body.menu-open removed → death visible again
No JS changes needed — purely a stylesheet fix.
run.sh:
- phase() wrapper logs elapsed seconds per build step.
- Tracks total build+startup at the end.
- Output is "==> phase / [Ns] phase" so the slow steps are obvious.
test/run.py:
- Per-step time.perf_counter() around each scenario step.
- "slowest steps" summary printed at the end so the worst
offenders are immediately visible.
- Total wall-clock time at scenario end.
src/render/mod.rs:
- browser_now() helper: web_sys::performance().now() on wasm,
Instant-based on native. Monotonic ms timestamps for tick/toc.
- Renderer::rebuild_chunk wraps build_chunk_mesh in a t0/t1
measurement and logs anything over 5ms with vertex/index counts.
Surfaces sky_visibility cost in the browser console.
web/main.js:
- Exposes window.voxel_game = wasm after init so the test
harness can drive scenarios declaratively (set_scene_time,
teleport, look_at, get_position, etc.).
src/shader.wgsl:
- Fix duplicate `let to_eye` declaration introduced in Round D
(specular's normalized to_eye conflicted with fog's raw version).
Renamed fog's local to_eye_raw. The test harness caught this
immediately — first WGSL compile error, first scenario run.
Findings from running scenarios/lighting-times-of-day.yaml:
- 289 chunks × ~100ms avg = ~29s mesh-build on main thread.
- Page-ready latency dominated by this. window.voxel_game appears
almost immediately (init resolves before chunks build), but
the world is invisible until meshes are uploaded.
- sky_visibility (8 cosine rays × HashMap voxel lookups) is the
hot path inside build_chunk_mesh.
Next: make chunk-mesh build progressive (one or two chunks per tick
instead of all up-front), so the world becomes visible immediately
and pops in over a few seconds.
- Greedy meshing now bakes per-vertex AO with 4-corner sampling and an
anisotropic-diagonal split when corner AO disagrees.
- WGSL: extracted sky_dome() for hemisphere ambient sampling so vertical
faces match the sun-side sky tint at day; ambient_strength mixed by
day strength instead of a flat constant.
- Step-1 post pipeline: render scene into an offscreen color texture,
pass-through to the surface. Foundation for FXAA/shafts that will
follow.
- Input bug: merge_held() now recomputes per tick from sticky keyboard +
live touch bridge, so releasing the joystick actually stops the
player (previous OR-into-self bug ate playtests).
- Touch UI hit-zones reordered (menu/hotbar above the joystick z-index);
hotbar widened to 10 slots with tap-to-select on mobile.
- find_safe_spawn anchors on natural_surface_y so spawn is deterministic
from noise — towers built at spawn no longer climb the spawn point.
- move_axis is sub-stepped (0.45-block max) so high-velocity falls can't
teleport the player inside terrain.