diff --git a/docs/plans/2026-08-22-visualmaster-campaign.md b/docs/plans/2026-08-22-visualmaster-campaign.md index b3f6b676..a44290e0 100644 --- a/docs/plans/2026-08-22-visualmaster-campaign.md +++ b/docs/plans/2026-08-22-visualmaster-campaign.md @@ -82,7 +82,7 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level. | VM3 | F4 — linear-light post stack | **CODE-COMPLETE 2026-08-22** — `87677f9c`, `51178f7c`, closeout; Opus APPROVE; **owner visual gate OWED** (brief below) | | VM4 | F5 — overclaiming docs, incl. the reviewer's own | **CLOSED 2026-08-22** | | VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) | -| VM6 | Foliage wind | **CODE-COMPLETE 2026-08-23** — `0930c35d`, `39e8408c`, `43e3abed`, `a82959f1`, `fccba839`, `eec95535`; four Opus review rounds (round 3 APPROVE, round 4 narrow re-review of `fccba839`+`eec95535`); pixel proof [note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md) (13,854 robust wind px vs 19 floor, scenery only); **owner visual gate OWED** (§VM6 Acceptance) | +| VM6 | Foliage wind | **CODE-COMPLETE 2026-08-23** — `0930c35d`, `39e8408c`, `43e3abed`, `a82959f1`, `fccba839`, `eec95535`, `754d59d9`; five Opus review rounds (round 3 APPROVE; round 4 REJECT caught the overhead-sun regression, fixed in round 5); pixel proof [note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md) (14,993 robust wind px vs 65 floor, scenery only; wind-off frames match the plain pipeline at mean |Δ| 0.007); **owner visual gate OWED** (§VM6 Acceptance) | | VM7 | Closeout: full gates, register, roadmap, merge | **owner** | Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 (done) → VM5 (done) → VM3 (code-complete, gate owed) → VM6 (code-complete, gate owed) → VM7. VM0 goes first because everything after it is measured @@ -495,7 +495,7 @@ direction to read, and the register row says so. indoors — nothing moves; (6) pack off — nothing moves. The owner's words decide; the numbers above are starting points to tune live. -### VM6 outcome (implementation landed 2026-08-22; code-complete 2026-08-23 at `eec95535`; owner visual gate outstanding) +### VM6 outcome (implementation landed 2026-08-22; code-complete 2026-08-23 at `754d59d9`; owner visual gate outstanding) Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the design above; a same-day fix-round commit corrected the weather-table key. @@ -725,13 +725,15 @@ gated-off frame is numerically the plain pipeline. Wind is unaffected — `foliage_wind.glsl` reads only the AtmosphericFrame half of set 3, never the shadow block. -**Pixel proof after round 4 (2026-08-23, `eec95535`):** the same +**Pixel proof after rounds 4–5 (2026-08-23, `eec95535` → `754d59d9`):** the same apparatus that found the defect — High preset, `sun-shadow-strength=0` so no shadow term can move, same pinned clock, wind on vs off, two captures per arm so the repeat pairs measure the run-to-run floor — gives a robust wind -mask of **13,854 px** (amplified: strength 2, lean/branch 1 m, flutter -0.5 m) against a **19 px** floor, and **1,402 px** at the default Clear -strength; every marked pixel is a treeline tree, hillside tree or shoreline +mask of **13,854 px** on `eec95535` and **14,993 px** on `754d59d9` +(amplified: strength 2, lean/branch 1 m, flutter 0.5 m) against a 19–65 px +floor, and **1,402 px** at the default Clear strength; the wind-off frame on +`754d59d9` matches the pre-round-4 plain pipeline at mean |Δ| 0.007 (round +4's `eec95535` was 1.77 — the overhead-sun regression); every marked pixel is a treeline tree, hillside tree or shoreline bush, and no house, fence, road, lifestone, ground, water or UI pixel moves. The first method tried (clock pin 0 s vs 3 s, wind-off pair as the control) is recorded as confounded in the note and must not be reused: the treeline diff --git a/docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md b/docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md new file mode 100644 index 00000000..ecf65067 --- /dev/null +++ b/docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md @@ -0,0 +1,145 @@ +# VM6 — does the wind move the trees, and only the trees? + +**Date:** 2026-08-23 · **Campaign:** VM slice VM6 · **Status:** see the +verdict at the end; the owner's visual gate (plan §VM6 Acceptance) is still +owed regardless. + +**Question.** With the built-in `acdream.atmospheric` pack, does the +foliage-wind displacement reach the **production** world geometry, and is it +confined to procedural scenery foliage? + +This note records the apparatus that finally answered it, including the +first method that did not — two wrong conclusions were drawn from it before +the confound was found, and both are recorded here so nobody re-derives them. + +## Apparatus + +Offline pixel gate (`tools/run-offline-pixel-gate.ps1`, Release, `-SkipBuild`, +`-Uncapped`, 1280×720, day group 0 "Sunny" → `WeatherKind.Clear`, day +fraction 0.5, MSAA 0), isolated settings, the fixed offline scene the VM0/VM3 +captures use, drawn through `RetailPViewPassExecutor.DrawPackedProductionRoute` +(the production entity route). `-RenderPackPreset high`; `ACDREAM_SKY_PHASE_SECONDS` +pins the pack's per-graph clock so two captures at the same pin see the same +wind phase. Pixel metric: max-channel |Δ| ≥ 8 +(`tools/vm6/wind-pixel-proof.py`, numpy). + +### Method 1 (confounded — do not reuse): "t0→t3 with wind on, minus t0→t3 with wind off" + +The idea was that the clock pin at 0 s vs 3 s moves only the wind, so the +wind-off pair is a control for everything else the clock drives. It is not: +the same binary, same settings, same pin differs run-to-run by **0 or ~280 +px**, bimodally, and those pixels are isolated single-pixel flips along the +treeline's leaf edges (`artifacts/vm6amp/churn-crop.png`) — rasterisation +edge flips, not content. Whether the churn lands in the wind pair or the +control pair is luck, so "wind-only" counts of 23 / 314 / 568 / 703 px from +this method on `43e3abed` / `a82959f1` were all within that churn. The +overlay images looked convincing precisely because the churn sits on tree +silhouettes. Two conclusions drawn from it — "the F1 packed-classifier fix +is confirmed by pixels" and "wind reaches the geometry" — were unsupported. +(The F1 fix itself is correct; the round-3 Opus review traced it to the GPU +word by reading, and the round-4 apparatus below confirms geometry motion.) + +### Method 2: same pin, wind on vs off, signal amplified, repeats as the floor + +Settings `wind-strength = 2`, `wind-lean-metres = 1`, `wind-branch-metres = 1`, +`wind-flutter-metres = 0.5` make the canopy displacement ~1 m — several +pixels even at this scene's treeline distance, where the default Clear wind +(≤ 11 cm lean) is sub-pixel. Two captures per arm; the repeat pairs measure +the floor; the four cross pairs measure the effect. + +## Results + +### `fccba839` (round 3), `sun-shadow-strength = 0` + +| Pair | px ≥ 8 | +|---|---| +| wind-off A vs wind-off B (floor) | 22 | +| amplified-wind A vs amplified-wind B (floor + churn) | 317 | +| amplified A vs wind-off A / B | 65 / 49 | +| amplified B vs wind-off A / B | 284 / 273 | +| robust mask (all four cross pairs, neither repeat pair) | **1** | + +A 1 m wind produced nothing. A CPU probe in `ResolveFoliageWind` (removed +after the run) showed the state was right — first advance at serial 2 snaps +to Clear `(0.25, 0.15)`, strength 1, gate on, one graph instance — so the +uniform was correct and the world pass never used it. + +**Cause (by reading, round-4 finding):** the wind was welded to "directional +shadows rendered this frame". `DirectionalSunShadowRenderer.Render` left the +frame binding `Disabled` whenever the environment gate said no; +`WbDrawDispatcher.PipelinesFor` then fell back to the plain `mesh_modern` +pipeline, which has no `foliage_wind.glsl` at all; and +`BindDirectionalShadowReceiver` returned early on `!Enabled`. The gate is +`dayGroupPolicy × sunElevationResponse × strength`, so the wind also stopped +every night, at user strength 0 and under the portal cover. Fixed in round 4 +by publishing a valid-but-disabled binding (zeroed shadow block, flags 0 — +the receiver shader already returns visibility 1.0 on that bit — plus the +wind frame slice) so the built-in pack's world pass always runs the receiver +pipeline. + +### `eec95535` (round 4), `sun-shadow-strength = 0` + +| Pair | px ≥ 8 | +|---|---| +| wind-off A vs wind-off B (floor) | 19 | +| amplified-wind A vs amplified-wind B (repeat) | 51 | +| amplified A/B vs wind-off A/B (four cross pairs) | 13,876 / 13,887 / 13,893 / 13,903 | +| **robust mask (all four cross pairs, neither repeat pair)** | **13,854** | +| **default Clear strength vs both wind-off, floor-excluded** | **1,402** (2,655 at |Δ| ≥ 3) | + +Per-60-row band of the robust mask: `[8580, 4101, 800, 61, 0, 110, 14, 188, 0, 0, 0, 0]` — +the treeline (rows 0–180), the hillside trees (rows 300–360) and the +shoreline bushes (rows 420–480). Overlays: +`evidence/vm6/eec95535-amplified-wind-vs-off-shadows-off.png` (every tree +and bush red; no house, fence, road, lifestone, ground, water or UI pixel), +`evidence/vm6/eec95535-default-clear-wind-vs-off-shadows-off.png`, the +amplified frame itself `evidence/vm6/eec95535-amplified-wind-frame.png`, and +the Method-1 churn crop `evidence/vm6/method1-run-to-run-churn-crop.png`. + +### `754d59d9` (round 5 — gated-off frames light from the authored sun again) + +The round-4 narrow review found that round 4's decoupling lit every +shadow-gated-off frame from straight overhead: both receiver vertex shaders +take the sun *direction* from the shadow block, and the disabled block's +`(0,0,1)` guard became the sun. Round 5 makes the receiver verts fall back +to the plain pipeline's `uLights` expression when the flag bit is clear. +Same apparatus, wind-off arm compared across binaries (shadows off, same +pin): + +| Pair | px ≥ 8 | mean |Δ| | +|---|---|---| +| `754d59d9` wind-off A vs B (floor) | 65 | 0.002 | +| **`754d59d9` vs `fccba839` (plain pipeline, pre-round-4)** | **277** | **0.007** | +| `eec95535` vs `fccba839` (the round-4 regression) | 32,538 | 1.77 | + +Wind on `754d59d9`: robust mask **14,993 px** (amplified), same bands as +round 4. + +## Verdict + +On `754d59d9` the foliage wind reaches the production world geometry with +the shadow term removed (so it is geometry, not shadow), is 700× the +run-to-run floor when amplified and 70× at the default Clear strength, and +is confined to procedural scenery foliage in this scene. Earlier binaries: +`43e3abed` had no geometry wind at all on the production route (round-2 +review F1, confirmed by reading); `a82959f1`/`fccba839` had it only while the +shadow gate was open (round-4 defect); `eec95535` had the wind but lit +shadow-gated-off frames from overhead (round-5 defect). + +Commands: `tools/run-offline-pixel-gate.ps1 -Out