diff --git a/docs/PRDs/assets/PRD-541-card-lod-levels-keep-the-canopy.md b/docs/PRDs/assets/PRD-541-card-lod-levels-keep-the-canopy.md index 02fb20160..858502874 100644 --- a/docs/PRDs/assets/PRD-541-card-lod-levels-keep-the-canopy.md +++ b/docs/PRDs/assets/PRD-541-card-lod-levels-keep-the-canopy.md @@ -42,8 +42,8 @@ Layer: the cook (`packages/assets/`). The game sets nothing; the scale is the ke - [x] AC-1: every ratio card level ships ≥ 0.95 of LOD0's card area, read back from the cooked bytes; the terminal level ships min(1, 4 × keep). proof: `foliage-lod.spec.ts` — 8 passed; red before the change: `expected 0.5162100266415837 to be greater than or equal to 0.95` - [x] AC-2: Machinefall `map-walk` route GPU p95 passes the pooled walking bound (≤ 5 % of walking frames over 8.3 ms) on 3 quiet runs. proof: `TN_FRAME_BUDGET` windows, quiet desktop RTX 2080 — **passed 2026-10-09** on the clamp build (`lodfix11`), 3 quiet runs: 5/911, 3/1186, 4/988 walking GPU frames over 8.3 ms, pooled 12/3085 = 0.39 %. -- [ ] AC-3: no visual loss against PRD-478's tip. proof: 3 fresh blind raters at or above on the 4 `map-walk` and 4 `map-views` poses, and a pop series with no candidate-only late object — 24/24 votes at or above (20 prefer, 4 ties); `world-capture` 34 frames, 2 fresh blind judges: 0 candidate pops in 33 intervals, 1 base pop (interval 00, a distant tree cluster appears). Laptop Intel GPU, 2026-10-08. Sheets: PR 463. - - Reopened 2026-10-08: the evidence above is the build where every level cast; the design is now the clamp (`04400564a`), so the raters and pop series run again on that build. +- [ ] AC-3: no visual loss against PRD-478's tip. proof: 3 fresh blind raters at or above on the 4 `map-walk` and 4 `map-views` poses, and a pop series with no candidate-only late object. **Remains open after repair 2026-10-13:** the 2026-10-08 24/24 vote and 33-interval pop record predates final clamp `04400564a` and is historical context only. The current-core candidate site is prepared at `~/offload/sites/prd478-finalclamp-site` (bundle SHA-256 `cdb44459c84b278d400f29b8a43f3521c4995c688bb7f25b98e401a86b80d534`) against the existing develop baseline `~/offload/out/pop-dev`. **Capture state 2026-10-13 (terminal, nothing queued, nothing live):** job `prd478-finalclamp-visual-20261013` ended terminal 1 in `~/offload/out/prd478-finalclamp` with 4 `map-views` pose PNGs, 0 `map-walk` PNGs and 0 `world-capture` PNGs; the retry `~/offload/out/prd478-finalclamp-resume/map-walk` ended terminal 2 with `locator.screenshot: Timeout 29936ms exceeded` while waiting for element stability. No active PRD-473, PRD-478 or PRD-541 capture process was found on this host; remote lease state is not verified, so the earlier lease note is withdrawn. Both artifact sets are kept. No rating is claimed until 3 fresh blind raters judge the 8-pose bundle and the pop series. + - **Next probes:** follow PRD-478's AC-3 probe list. First pin the combined PRD-473 default-on runtime with final clamp `04400564a` as the site's source, because the site matches the recorded bundle and asset-manifest hashes yet no source metadata establishes that combination, so no capture from it is acceptance evidence. Then diagnose the map-walk screenshot on the live clock and on the paused clock. Neither the ratings nor the pop series can start until a full candidate capture exists. Score only with 3 fresh verdict files. Phase 3 stays NOT STARTED. ## Execution Phases diff --git a/docs/PRDs/open-world/PRD-478-open-world-frame-architecture.md b/docs/PRDs/open-world/PRD-478-open-world-frame-architecture.md index a27937943..3114e1e54 100644 --- a/docs/PRDs/open-world/PRD-478-open-world-frame-architecture.md +++ b/docs/PRDs/open-world/PRD-478-open-world-frame-architecture.md @@ -1,6 +1,6 @@ # PRD-478 — Open-world frame architecture: walking at 120 fps -**Status:** IN PROGRESS +**Status:** IN PROGRESS — AC-1 and AC-3 reopened 2026-10-13 after review found that the archived evidence did not cover the final comparison contract. **Priority:** P1 — Open: walking CPU and GPU p95 targets with deterministic map-walk iteration tooling. **Complexity:** 8 (HIGH) — 10+ engine files (+3), worker boundary and GPU-driven shadow levels are new mechanisms (+3), engine and Machinefall release separately (+2); risk override: none **Owner:** João @@ -55,7 +55,14 @@ Change where the frame's work runs, not how much of it a frame skips: ## Acceptance Criteria -- [x] AC-1 [local]: Machinefall `?scene=map-walk` walking CPU render p95 ≤ 8.3 ms. Read as the walk/idle split of `TN_FRAME_BUDGET` windows, where the walk mask is logic-tick/key-held intervals joined on `counters.simulationTick` (an earlier `fps < 100` mask is only a slow mask on older logs and cannot support a walking claim), over 3 interleaved runs against develop on a quiet RTX 2080. proof: `TN_FRAME_BUDGET` windows from `playtest perf`. **Passed 2026-10-09** on the merge build (`lodfix11`: PR 473 with the PRD-549 scaler and shadow fixes), 3 quiet RTX 2080 runs (start/end/10 s peak load < 10; peaks 8.2, 8.2, 7.6): walking render frames over 8.3 ms 509/7200 (7.1 %), 291/9600 (3.0 %), 306/9900 (3.1 %), pooled 1106/26 700 = 4.1 % ≤ 5 %, so the pooled walking p95 is ≤ 8.3 ms; GPU 12/3085 = 0.39 %. Exact `overTarget` counts (`exact_share.py`). Deviation: the develop arm was not interleaved; the bar is absolute. +- [ ] AC-1 [local]: Machinefall `?scene=map-walk` walking CPU render p95 ≤ 8.3 ms. Read as the walk/idle split of `TN_FRAME_BUDGET` windows, where the walk mask is logic-tick/key-held intervals joined on `counters.simulationTick` (an earlier `fps < 100` mask is only a slow mask on older logs and cannot support a walking claim), over 3 interleaved runs against develop on a quiet RTX 2080. proof: `TN_FRAME_BUDGET` windows from `playtest perf`. **Reopened 2026-10-13:** the cited 2026-10-09 record is candidate-only and explicitly says the develop arm was not interleaved, so it does not satisfy this criterion. **State 2026-10-13:** no replacement run started and none is running. No active PRD-473, PRD-478 or PRD-541 capture process was found on this host; remote lease state is not verified, so the earlier lease note is withdrawn. Host `joao` is an Intel IrisXe (gen-12lp) laptop with no NVIDIA adapter and no desktop SSH alias, so the 3 interleaved pairs cannot run here; no timing result is carried forward. + - **Exact next probe:** ask the owner for the desktop RTX 2080 SSH address, then alternate develop and candidate for three pairs there with the shipped runner, keeping `SCENARIO`, `SITE`, `PORT`, and `OUT` distinct for each arm: + ```sh + CAPTURE_LOCK=1 CAPTURE_LOCK_TIMEOUT_MS=3600000 node packages/playtest/dist/runner/cli.js "${SCENARIO}/map-walk.playtest.json" --url "http://127.0.0.1:${PORT}/?scene=map-walk&tnFrameSpans=1" --server-command "python3 -m http.server ${PORT} --bind 127.0.0.1 --directory ${SITE}" --browser-recipe webgpu --headed --live-clock --timeout 900000 --artifacts "${OUT}" >"${OUT}/report.json" 2>"${OUT}/stderr.log" + jq -r '.. | objects | select(has("text")) | .text' "${OUT}/console.json" >"${OUT}/console.log" + node packages/playtest/dist/runner/cli.js perf --file "${OUT}/console.log" --text + ``` + Keep the load below 10 at start, end, and each 10-second sample; verify the RTX 2080 in `adapter.info`; report CPU and GPU p95 for each arm and the pooled walking mask joined by `counters.simulationTick`. - [x] AC-2 [local]: the same walk's GPU p95 ≤ 8.3 ms, from timestamp-query `gpuMain + gpuShadow + gpuOther + gpuCompute`. proof: the same windows. **Pass 2026-10-08:** 3 interleaved runs against develop on the desktop RTX 2080 (live clock), walking-window GPU p95 max 6.57 / 5.72 / 6.43 ms; the largest walking-window p95 bounds the pooled one. Caveats: start load 11–19, not idle, and another process used 2–22 % of the GPU, which can only inflate these numbers; GPU p95s come from the frames whose timestamps resolved in full (about 1 in 10). Run 3 also carries `7709c2a72`, a CPU-only change. **Pass 2026-10-08 (re-ticked):** 3 runs of this branch at `52d5e6cac` on the desktop RTX 2080, live clock, each kept only with load under 10 at start, end and every 10 s (peaks 8.2, 4.0, 4.3), 300-frame windows, walking windows ≥ 80 % key-held. A pooled walking GPU p95 is ≤ 8.3 ms when at most 5 % of walking GPU samples exceed it; bounded from window quantiles (≤ 1 % past a window's p99, ≤ 5 % past its p95, ≤ 50 % past its p50), that share is at most 3.5 %, 3.2 % and 4.4 %. Candidate-only runs under the load gate; develop's comparison is the interleaved 2026-10-07 runs (GPU p95 9.2–9.9 ms). - **2026-10-08 candidate with PRD-541, 1 of 3 quiet runs:** load peak 5.6 over the run (now sampled every 10 s). AC-2 bound: at most 2.3 % of walking frames over 8.3 ms GPU (pass ≤ 5 %); route GPU p95 max 8.04 ms. AC-1 bound: 22.7 % over 8.3 ms render; route render p95 windows reach 15.6, 9.0 and 9.0 ms early in the route, FPS per 1500-frame window 107–418. A span run under load 30–40 ranks the same CPU spans as before: `mainPass`, then `compute`, then `sceneUpdate`. - **2026-10-08 quiet runs, AC-1 and AC-2 both still fail, only in the early route:** 3 live-clock runs of this PR (`b3c95a5a3` core, keys on, 1500-frame windows) on the desktop RTX 2080, each kept only when load1 was under 10 at both start and end (2.1–5.3 at start, 5.9–8.8 at end; 2 of 5 runs discarded for load). Over the route windows (ticks 470–6640): render p95 per window median 5.3 / 5.5 / 5.9 ms, max 10.9 / 11.5 / 10.8 ms, the max in the first route window each time; GPU p95 median 5.2 / 5.3 / 5.2 ms, max 9.4 / 9.2 / 9.8 ms, again the first route window (run 3 also has 9.7 ms in its second window). Every window after the first two is under 8.3 ms on GPU; on CPU, 3 later windows reach 8.6–9.7 ms. The load caveat on the earlier evidence is gone: what is left is the streaming-heavy start of the route, which PRD-538 targets for GPU. @@ -67,7 +74,22 @@ Change where the frame's work runs, not how much of it a frame skips: - 2026-10-08 first follow-up cut, `35e5e7f24`: each GPU-scene dispatch submits its clear and cull kernels in one `renderer.compute([...])` call, so three opens one compute pass and one timestamp query instead of two (red-green `world-gpu-scene.spec.ts`; 22 spec files, 402 passed). `map-walk` 20/20 on the third capture; the first two lost loads to `net::ERR_NETWORK_CHANGED` and one fetch, not to the change, and the highway frame matches the earlier capture. One live-clock run on the RTX 2080 (load 23–30, 23 walking windows): `compute` mean 0.55 ms (0.81–1.80 before), `mainPass` p95 max 7.2 ms (8.3–11.5 before), walking render p95 median 6.9 ms, max 13.6 ms. AC-1 still fails on the max; a single run is not a pair. - 2026-10-08 second cut, `1fe8b3087`: three's WebGPU backend issues one `queue.writeBuffer` per update range and never merges them, and the GPU scene registered one range per placement and key, so a cell admission was one write per placement inside `renderer.compute`. `addRange` extends the previous range when the new one touches it (red-green `world-gpu-scene.spec.ts` `addRange`; 213 passed). `map-walk` 20/20. Wide-window live-clock runs (1500 frames, keys on): `compute` mean 0.40–0.43 ms against 0.73–0.94 before; render p95 per route window median 4.7–5.0 ms, max 10.7–14.7 ms (before: median 11.1–11.8, max 15.6–19.9 ms, at higher load 16–33 against 7–13, so part of that drop is load); GPU p95 max 9.1–10.0 ms, still in the early route, where streaming invalidations re-render the shadow level about twice a second (200–280 level renders per walk, against 5 window moves). Why so many: Machinefall has one ±250 m level, and the default `invalidationDelay` scales relative to the finest level (`0.25 * extent / finestExtent`), so a single large level gets the finest level's 0.25 s and up to four full 4096² re-renders a second from streaming. A longer delay would trade those GPU spikes for shadows arriving late near the player, which AC-3 forbids. The fix that keeps both is a partial map update that re-renders only the invalidated region. That is a new mechanism, and it is the next step for AC-2. - 2026-10-08, most of those re-renders came from invalidations no shadow needed. Counting one walk's 3,800 region invalidations by caller: 1,409 were TerrainTiles block meshes entering or leaving the tree (swap twins over the same ground), 1,025 WorldCells admissions (largely meshes that cast nothing), and 391 bundle and group re-records. The caster poll also counted 4,500 flag flips, 3,568 of them terrain level-mesh `visible` swaps. Two cuts: `5a6d9d02f` marks terrain meshes `tnShadowSwap` and the poll ignores their `visible` bit; `ed75639ef` skips a non-casting or swap-twin mesh on the tree-change path (red-green specs for both; 24 spec files, 460 passed). Interleaved A/B, previous build against `ed75639ef`, 2 pairs, all 19/19, zero console errors: level renders per walk 241 and 213 against 68 and 79 (−68 %). Render and GPU p95 are within noise; the desktop sat at load 18–26 with other GPU users. `map-walk` 20/20; a blind judge finds the shadows equivalent on all four poses. PRD-538's partial redraw still matters for the renders that remain, but it is now a smaller lever. -- [x] AC-3 [local]: no visual loss. 3 fresh blind raters score every map-walk and map-views pose at or above develop `a602467db`, the pop road band matches develop, and a frame-by-frame judge finds no late objects or shadows. proof: `scripts/visual-ab.ts` score plus the pop series. — **Pass 2026-10-08** on this branch at `57c8ba8c6` (PRD-541 absorbed, cast clamp, target-frame LOD budget, prewarm gate holds for owed builds), laptop Intel GPU captures: 3 fresh blind raters score all 8 poses at or above develop `a602467db` (24/24 votes, 23 prefer, 1 tie; poses taken after loading, on `52d5e6cac`, which differs only in the loading gate); pop series (`world-capture`, 33 intervals, two runs) judged blind against the pre-absorption tip: 0 candidate pops, while the base still pops at interval 00. The road-band number is covered by that pop series; see Decisions. +- [ ] AC-3 [local]: no visual loss. 3 fresh blind raters score every map-walk and map-views pose at or above develop `a602467db`, the pop road band matches develop, and a frame-by-frame judge finds no late objects or shadows. proof: `scripts/visual-ab.ts` score plus the pop series. **Reopened 2026-10-13:** the cited 2026-10-08 4-walk/4-view ratings and pop series predate final clamp `04400564a`, so they cannot prove this criterion. A current-core post-clamp site was prepared at `~/offload/sites/prd478-finalclamp-site`. **Capture state 2026-10-13 (terminal, nothing queued, nothing live):** job `prd478-finalclamp-visual-20261013` ended terminal 1 in `~/offload/out/prd478-finalclamp` with 4 `map-views` pose PNGs, 0 `map-walk` PNGs and 0 `world-capture` PNGs; the retry `~/offload/out/prd478-finalclamp-resume/map-walk` ended terminal 2 with `locator.screenshot: Timeout 29936ms exceeded` while waiting for element stability (diagnostic `TN_PLAYTEST_RUNNER_FAILED`). No active PRD-473, PRD-478 or PRD-541 capture process was found on this host; remote lease state is not verified, so the earlier lease note is withdrawn. Both artifact sets are kept. No blind approval is claimed until the shipped visual bundles are scored by 3 fresh raters and the pop series is judged against develop `a602467db`. + - **Exact next probes, in order:** + 1. *Pinned combined source provenance.* The site matches the recorded bundle SHA-256 `cdb44459c84b278d400f29b8a43f3521c4995c688bb7f25b98e401a86b80d534` and asset manifest `940a777d3bd1a51d7229d2f9686485f53fa1a36e72cdf318103b7079f158ebcf`, but no source metadata establishes the combined PRD-473 default-on runtime with final clamp `04400564a`. Until that combination is pinned and recorded here, no capture from this site is acceptance evidence. + 2. *Screenshot on the actual map-walk.* Runner dependencies are restored with a frozen `@threenative/playtest` filtered install. `doctor` on the site reports Intel gen-12lp, 0 console errors and a fixedstep `simulationTick` of 0, so there is no liveness. A synthetic Playwright paused-clock screenshot passes, so a clock cause stays UNPROVEN. Take one screenshot on the real `map-walk` route on the live clock and one on the paused clock, and record which one times out. + 3. *Desktop timing host.* AC-1's 3 interleaved RTX pairs run only on the owner's RTX 2080, which needs the owner's SSH address. + - **Scoring commands,** to run from the engine checkout once a full candidate capture exists. Export `CANDIDATE_SOURCE` as the verified combined runtime source revision from probe 1 before the candidate manifest line runs. It has no default, because the earlier `c2c3eba33` argument is not established as that revision: + ```sh + pnpm exec tsx scripts/world-capture-manifest.ts ~/offload/out/pop-dev a602467db 30 --scenario machinefall-world-capture --world Machinefall + pnpm exec tsx scripts/world-capture-manifest.ts ~/offload/out/prd478-finalclamp/world-capture "$CANDIDATE_SOURCE" 30 --scenario machinefall-world-capture --world Machinefall + pnpm visuals:world --before ~/offload/out/pop-dev/world-capture.json --after ~/offload/out/prd478-finalclamp/world-capture/world-capture.json --out ~/offload/out/prd478-finalclamp/world-gate + mkdir -p ~/offload/out/prd478-finalclamp/ab-before-20261013-r1 ~/offload/out/prd478-finalclamp/ab-after-20261013-r1 + cp ~/offload/out/dev-map-walk/*.png ~/offload/out/dev-map-views/*.png ~/offload/out/prd478-finalclamp/ab-before-20261013-r1/ + cp ~/offload/out/prd478-finalclamp/map-walk/*.png ~/offload/out/prd478-finalclamp/map-views/*.png ~/offload/out/prd478-finalclamp/ab-after-20261013-r1/ + pnpm visuals:ab --before ~/offload/out/prd478-finalclamp/ab-before-20261013-r1 --after ~/offload/out/prd478-finalclamp/ab-after-20261013-r1 --out ~/offload/out/prd478-finalclamp/ab --duplicates 2 --raters 3 + ``` + Score only with three fresh verdict files; do not treat bundle creation or a builder inspection as a visual result. - **2026-10-05 evidence, box left open:** the PRD-477 world gate on Machinefall's world-capture route, develop vs this PR's tip (`752ee7f15`), 2 + 2 runs, 3 fresh blind critics: **pass, exit 0**. Same-pose 4 → 4 on every counted row, no near-band pop, no candidate-only event in every candidate run. What still fails the box's own words ("no late objects or shadows"), on develop as much as here: (1) far forest blocks beyond the dirt road stream in late at 150–300 m, at frames 28–32 in every series; (2) a pre-existing, nondeterministic stray conifer cluster about 35 m ahead from walk-01 to walk-21, seen in 1/4 develop runs, 3/4 tip runs, and 2/6 tip runs with the static-matrix change reverted. Not yet run: the map-views poses and the comparison against develop `a602467db`. - **2026-10-07 raters on `fc9d56c1a`, box still open:** all 8 poses (4 `map-walk`, 4 `map-views` from Machinefall `main`'s `playtests/views/map-views.playtest.json`) against develop `a602467db`, desktop NVIDIA Turing, private Xvfb, keys on; captures pass 20/20 and 4/4 on both arms. 3 fresh blind raters, A/B order random per pose: this PR scores at or above develop on 24 of 24 votes (23 higher, 1 tie). Largest gaps: camp overview 7–8 against 4 (develop draws straight diagonal shadow bands across the camp), bridge from river 7 against 5 (develop's ground has no shadows), highway 7–8 against 6. Sheet: #437. Still open in this box: the pop road band and pop series on this build, and the "no late objects" clause, which develop fails as much as this PR (far forest blocks stream in late at 150–300 m). PRD-537 (draft PR 459) plans whole-map far impostors, the likely fix for that clause. - **2026-10-08 with the target-frame LOD budget (`52d5e6cac`), box still open on one clause:** laptop Intel GPU. 3 fresh blind raters against develop `a602467db`: at or above develop on 24 of 24 votes (23 prefer, 1 tie). Pop series against the pre-absorption tip, 2 fresh judges: mid-walk, no candidate-only pop (three low-confidence flags, all on the base or on both). The one open clause is the start of the walk: the distant forest arrives in intervals 00–02 in every build, develop included. Repeat captures move it between those intervals (capture variance in when streaming finishes), so the clamp-build interval-01 pop above was that variance. The loading overlay drops before the visible ring has published; diagnosis running. @@ -77,8 +99,8 @@ Change where the frame's work runs, not how much of it a frame skips: ## Blocked on -- Phase 1's coverage box: the owner's side-by-side review of the camp and highway aerials against `?refShadow=1`. - Bare far hills (owner, 2026-10-08): PRD-537 (draft PR 459), whole-map impostors; Phase 1 measurement is running, and its dark impostor shadow is that PRD's Phase 2. +- Desktop RTX 2080 timing host (owner, 2026-10-13): AC-1 needs the owner's RTX 2080 over SSH. Host `joao` is Intel IrisXe (gen-12lp) with no NVIDIA adapter and no desktop SSH alias, so no interleaved pair runs here. The owner unblocks it with the SSH address. ## Iteration method @@ -93,12 +115,12 @@ A timing round under PRD-475 cost 1–3 hours, most of it waiting for a quiet ma ## Execution Phases #### Phase 1: Shadow windows move only when the camera does -**Status:** IN PROGRESS +**Status:** DONE **Files:** `packages/core/src/render/virtual-shadow-pages.ts`, `packages/core/src/render/virtual-shadow.ts`; `packages/core/__tests__/`; Machinefall `playtests/scenes/map-walk.playtest.json` - [x] The iteration tooling above works on map-walk: two runs of one build with the same flag report the same level re-render count, and the paired walk compares the arms frame by frame over the movement interval. proof: two same-arm runs with equal counters plus one paired-walk report. **Pass 2026-10-07:** `scripts/playtest-pair.ts` (`f9ee2ec71`) is the paired-walk report; it compares two reports at every labelled step (every 24 ticks, not every frame). Two counter fixes were needed: `stats.windowSteps` (`47b5b8664`) counts window travel in pages, so frame sampling cannot change it, and `?tnAdaptiveRefresh=0` (`ce9f7ccf3`) fixes the refresh step, which otherwise follows measured render cost. Same build `fc9d56c1a`, keys on, `world-capture` twice on the quiet laptop (Intel Iris Xe, load ~0.6): both runs 10/10; camera equal on every label; walk totals equal, `shadowByMove` 5|5 and `windowSteps` 2108|2108 at `drain-loads`. Caveat: during the walk, 3 window moves land one label apart (walk-08, walk-24, walk-25), because the window follows the view focus, which a ray finds against terrain that streams in on its own timing. Compare walk totals, not per-label counts. - 2026-10-03 (runbook A3): Machinefall `feat/world-gate-capture` `d80b24d` publishes `shadowByMove`/`shadowByInvalidation`/`shadowRenders` on the `world` entity, read at labelled ticks of `world-capture.playtest.json`. Two develop runs (RTX 2080 WebGPU, CI load ~19): `byMove` walk-16 2/2, walk-32 4/4 — repeatable; `byInvalidation` 39/32 and 69/62 — streaming-timed, so it cannot be a same-arm-equal counter. The end-of-scenario `TN_VIRTUAL_SHADOW` line is not comparable across runs (it lands at frame 2100 vs 3600). Open: the box's "level re-render count" must be scoped to `byMove` (the window-moves mechanism this phase owns), and the paired per-label report is a jq over two reports, not yet a script. - [x] A refresh-step change with a stationary camera neither moves a level's window nor re-renders it; a real move of step + 1 texel re-renders; every origin stays on the per-level texel grid. proof: red-green `virtual-shadow-pages.spec.ts` and `virtual-shadow.spec.ts` cases (unit-green on `feat/prd-478-shadow-snap` `748348c9c`, not yet on this branch). **Pass 2026-10-05 on this branch:** the five cases from `feat/prd-478-shadow-snap` `748348c9c` are present and green here. In `virtual-shadow-pages.spec.ts`: "keeps every origin on its texel grid with independent refresh thresholds", "does not move a stationary window when its refresh threshold shrinks below its drift", and "changes only the requested level when a scalar refresh step was supplied". In `virtual-shadow.spec.ts`: "should keep stationary windows cached when their refresh steps change" and "should re-render after moving exactly a refresh step plus one texel". Result: 112/112 across the shadow spec files. The code arrived through the #423 consolidation; the cherry-pick added only `done/PRD-478-shadow-refresh-snap.md`. -- [ ] Shadow levels cover what the view sees: their windows follow the view's focus (where the view meets the ground), not the eye, so an aerial view keeps fine levels on what it frames. A tall caster's shadow does not end at a level's guard square. The camp and highway aerial views match the stock-shadow reference (one 4096² map over ±250 m) within the blind judge and the owner's side-by-side review. proof: red-green spec on the window centre for a raised camera, plus the `?refShadow=1` reference triptychs on #401. +- [x] Shadow levels cover what the view sees: their windows follow the view's focus (where the view meets the ground), not the eye, so an aerial view keeps fine levels on what it frames. A tall caster's shadow does not end at a level's guard square. The camp and highway aerial views match the stock-shadow reference (one 4096² map over ±250 m) within the blind judge and the owner's side-by-side review. proof: red-green spec on the window centre for a raised camera, plus the `?refShadow=1` reference triptychs on #401. **Passed 2026-10-09, owner review:** the merge build (`lodfix11`) against `?refShadow=1` rendered from the same build, `map-views` (camp overview, highway air, bridge deck, bridge from river): "left looks worse, right looks better" — the virtual shadow is preferred to the stock 4096² reference. Sheet: `~/Downloads/threenative-pr473/aerial-review-ref-vs-pr473.jpg` (local, owner's machine). - 2026-10-07 reference run, box still open: a stock-shadow reference was rebuilt in the capture copy (`?refShadow=1`: one 4096² stock map over ±250 m, near 1, far 800, every layer visible, `tnShadowGpuKeys=0`; a first attempt that saw only layer 0 left the loading gate up and was discarded). `map-views` against it, blind: this PR (`fc9d56c1a`, keys on) matches at camp overview and bridge deck; highway air is "mostly" (forest-floor tree shadows lighter and sparser) and bridge from river is "mostly" (one extra dark area). Develop: camp overview "no" (stair-step bands, missing left-forest shadow), highway air matches, bridge deck "mostly", river "no". A trial that took the unbiased distance for shadow LOD (`9aa9d9791`) lost a direct blind comparison 1–4 (3 ties) with denser-than-reference shadows and is reverted (`fe9d0540c`). Left: highway air, and the owner's side-by-side (Blocked on). - [x] Measured on map-walk against cut12n and cut12n with `adaptiveRefresh: false`: walking render p95, GPU p95 and level re-render rate, plus AC-3's blind A/B and pop series. proof: `TN_FRAME_BUDGET` walk/idle split, `?tnFrameSpans=1`, `scripts/visual-ab.ts`. **Measured 2026-10-08 against today's build with `adaptiveRefresh` on and off** (see Decisions): desktop RTX 2080, live clock, `map-walk`, load 6–30 at run start, build `35e5e7f24`; walking windows ≥ 80 % key-held; three runs voided by `net::ERR_NETWORK_CHANGED`. Phase 1 path (`tnShadowGpuKeys=0`, 2 runs): render p95 median 4.2 / 9.5, max 20.5 / 14.6 ms; GPU p95 max 8.3 / 9.5; shadow GPU p95 max 5.5 / 6.2; level renders 219 / 215. Phase 2 path (keys on, 2 runs): render median 11.4 / 6.2, max 14.9 / 14.8; GPU max 7.5 / 10.7; shadow GPU max 3.9 / 5.7; renders 200 / 211. Keys on with `?tnAdaptiveRefresh=0` (3 runs): render median 6.3 / 7.2 / 5.3, max 14.9 / 8.7 / 13.0; GPU max 6.0 / 3.1 / 9.1; renders 225 / 284 / 209. At this load the arms do not separate beyond run-to-run noise on render or total GPU. The keyed shadow is cheaper at its p95, and a fixed step re-renders more. Blind A/B: 24 of 24 votes at or above develop. Pop series: 1 one-arm pop against develop's 4 (notes under AC-3). - [x] No shadow without its tree at the first playable frame: the adaptive LOD bias holds until the world is prewarmed and the main pass draws it. proof: red-green spec `world-gpu-scene.spec.ts` "holds the bias through the load and rises once the main pass draws the world"; Machinefall start pose with and without the fix (pending GPU capture). Second cause, found after `0386c861c`: the gate table names every level of an asset from the moment it is adopted and fills in the keys the prewarm has minted so far (measured on a four-level chain: `parts=2,0,0,0`, then `2,2,0,0`, `2,2,2,0`), so a level the biased distance names had no keys and the dispatch drew the placement nowhere while the CPU path drew it and its shadow halves drew under it; the selection now takes the last level that has keys. proof: red-green spec `world-gpu-scene.spec.ts` "draws the last source level that has keys when the biased gate names one that has none" (red `expected +0 to be 1`, green after `world-gpu-scene.ts` `drawableLevel`). **GPU capture done 2026-10-05:** the start pose with and without this PR's fix (`558f08cbf`, caster admission plus the cached-level redraw) on Machinefall, 12 captures per matrix, merge on and off, nvidia/turing. Develop drew canopy shadows with no trees in some runs. The fix was clean in all of its runs per a blind judge and the owner's reviewer, with the floor brightness at develop's level (212 vs 211). The two specs above pass. diff --git a/docs/PRDs/open-world/RUNBOOK-machinefall-120fps-with-fab-quality.md b/docs/PRDs/open-world/RUNBOOK-machinefall-120fps-with-fab-quality.md index b1400d64d..d072a49da 100644 --- a/docs/PRDs/open-world/RUNBOOK-machinefall-120fps-with-fab-quality.md +++ b/docs/PRDs/open-world/RUNBOOK-machinefall-120fps-with-fab-quality.md @@ -226,7 +226,7 @@ this host, so it is not attributed. The prewarm (`e580e76cb`) stays off this PR, invalidation before this mechanism is retried. Details in "Where we stand". - [ ] **A8 · [PRD-539](../Unreal/performance/PRD-539-low-resolution-temporal-reconstruction-quality-and-cost.md): temporal reconstruction closes the GPU gap (successor of PRD-455).** After: A7. ⏱🌍👁 - [ ] **A9 · PRD-478 acceptance.** After: A8. ⏱🌍👁 - - Done when: AC-1, AC-2 and AC-3 are ticked on a quiet host 🙋, and PRD-478 is in `done/`. + - Done when: AC-1, AC-2 and AC-3 are ticked on a quiet host 🙋; then PRD-478 may move to `done/`. - [ ] **A10 · PRD-477 Phases 2–3 and AC-1: budgets and switches become engine decisions.** After: A9. ⏱🌍👁 - [x] **A11 · [PRD-489](../done/PRD-489-gpu-scene-occlusion-culling.md) Phase 1 only: the occlusion go/no-go.** After: A9. ⏱ - If it declines, record the decision and close the PRD. If it goes ahead, its Phases 2–3 become row A12. 🌍👁