Opus narrow re-review ofae651312: APPROVE. Closes its three residuals: - AP-232 filed: retail's single-pass stage-1 OUTPUT alpha (MODULATE(TEXTURE, CURRENT) @0x0059c549) is the blend weight for a translucent subset; acdream's two-draw model is exact for opaque subsets (fog identity pinned) and a bounded weight difference on translucent ones. Distinct from AP-34 (queue order). Owed since05970306. - TerrainAtlas.DetailSamplerDescription names the production sampler (WRAP/LINEAR x3 per ACRender::SetDetailSurfaceInternal @0x006b6280); the test now asserts that constant's properties instead of a test-local copy. - Plan VM1 section: fragment now described as fogged; VM1 marked CLOSED with the Holtburg measurement (+2.17/+0.57/+0.16 vs predicted +2.2/+0.66/+0.16) and the detail-on cost (+0.3-0.5 ms CPU at Arwic). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
413 lines
23 KiB
Markdown
413 lines
23 KiB
Markdown
# Campaign VM — VisualMaster
|
||
|
||
**Date:** 2026-08-22 · **Status:** PLANNED — awaiting the owner's goal
|
||
**Phase id:** **Campaign VM** — named by the project owner on 2026-08-22
|
||
**Branch:** `claude/git-sync-status-5fb1d2` (= main + Campaign AR, `c51b07ef`)
|
||
**Predecessor:** [Campaign AR](2026-08-21-atmospheric-rendering.md) and its
|
||
[independent review](../research/2026-08-22-campaign-ar-review.md)
|
||
**Scheduling:** owner-directed, like Campaign AR. Does not displace M4.
|
||
|
||
## Goal
|
||
|
||
Make Campaign AR **provably safe to merge** and then **finish the look**:
|
||
close every review finding with evidence rather than assertion, put the
|
||
Tier-1 post stack in linear light so its controls mean what they say, and add
|
||
the one cheap, high-impact enhancement still missing from the discussed
|
||
scope — **gentle, believable wind in Dereth's foliage**, driven by AC's own
|
||
authored weather, opt-in through the same render-pack contract, with the
|
||
default retail-faithful path untouched.
|
||
|
||
Campaign VM is shipped when both sides of the final matrix pass:
|
||
|
||
- **pack off:** pixel-identical to the pre-campaign build outside the two
|
||
intended parity changes (terrain normals, building detail), with production
|
||
CPU/GPU/allocation within the pre-campaign noise envelope; and
|
||
- **pack on:** linear-light post stack, registered-or-removed detail fade,
|
||
moving foliage that the owner judges "real, not a screensaver", shadows
|
||
that move with the leaves, and all of it inside Campaign AR's declared
|
||
budgets.
|
||
|
||
## Operating model
|
||
|
||
The launcher/AR cadence, unchanged:
|
||
|
||
- **Fable** plans, verifies claims against source/decomp, writes the
|
||
closeouts, and drives the work order. Nothing in a closeout may claim more
|
||
than its artifact shows — every "passes" names the command and the lane.
|
||
- **Sonnet** implements each slice against the pinned contract below.
|
||
- **Opus** reviews every slice with two lenses (architectural / retail-
|
||
faithful) before its closeout; findings go through a fix round and a narrow
|
||
re-review.
|
||
- **The owner** is stopped for exactly three things: the cdb read on the
|
||
retail client (VM2), the visual gates (VM3, VM6, VM7), and the merge.
|
||
|
||
Rules binding on every slice: no workarounds (CLAUDE.md), no guessed
|
||
AC-specific constants — a number without a decomp/DAT anchor gets a register
|
||
row in the same commit — and the pack-off path is the oracle: any slice that
|
||
moves a pack-off pixel outside its declared mask is a bug, not a tuning.
|
||
|
||
## Lesson carried into every slice — a branch that exists is not a branch that runs
|
||
|
||
VM2 found that Campaign AR's #226 port, the 2026-08-21 findings doc it was
|
||
briefed from, and the AR review's "probably" were all describing the
|
||
**fallback** branch of retail's detail pass — the `stage == 0` framebuffer
|
||
blend that retail only takes when the adapter cannot advertise
|
||
`D3DTEXOPCAPS_PREMODULATE`. Real hardware (the owner's AMD, and anything
|
||
modern) takes the single-pass texture-stage branch, which computes a
|
||
different formula with the opposite sign of effect (darkens, not brightens).
|
||
The decomp was read correctly three times; what nobody did until VM2 was
|
||
spend two minutes reading the capability on a live client.
|
||
|
||
Binding rule for this campaign and after: **when retail gates a mechanism
|
||
behind a capability, preference, or `trysinglepass`-style switch, the port
|
||
is not done until the gate's live value is recorded** — a cdb read on the
|
||
PDB-paired client, cited in the port's note with the binary GUID. "Grep
|
||
named → decompile → port" answers what the code *says*; only the live read
|
||
answers what it *does* on the hardware the game actually ran on. A guess
|
||
about driver caps ("rarely advertised") is not evidence and must not appear
|
||
in a review verdict.
|
||
|
||
Corollary for brightness questions: the owner's live "too bright" (exposure
|
||
1.0 → 0.80) was the pack-on gamma-space tonemap (F4, VM3), not this. The
|
||
detail overlay's fallback brightening is +3 % on building shells only; the
|
||
single-pass path darkens by ~10 %. Neither explains a scene-wide level.
|
||
|
||
## Slice ledger
|
||
|
||
| Slice | Closes | Gate |
|
||
|---|---|---|
|
||
| VM0 | F1 — default-path invariance | **CLOSED 2026-08-22 — PASS**; [report](../research/2026-08-22-vm0-default-path-invariance.md) |
|
||
| VM1 | F2/F3 — #226 single-pass re-port + fade removal | **CLOSED 2026-08-22** — `05970306`, `388457a7`, `ae651312`, closeout; Opus APPROVE |
|
||
| VM2 | F3 — which retail detail path ran | **CLOSED 2026-08-22** — single-pass; see [cdb note](../research/2026-08-22-vm2-retail-detail-path-cdb.md) |
|
||
| VM3 | F4 — linear-light post stack | automated + **owner visual** |
|
||
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | docs |
|
||
| VM5 | F8 — volumetric banding; F7 filed | automated |
|
||
| VM6 | Foliage wind | automated + **owner visual** |
|
||
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
|
||
|
||
Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured
|
||
against the baseline it establishes.
|
||
|
||
---
|
||
|
||
## VM0 — Default-path invariance (F1)
|
||
|
||
**Why:** the only "pre-campaign oracle" is a 2×2 synthetic, one-draw
|
||
recording-device fixture. `WbDrawDispatcher.Rhi.cs` was refactored underneath
|
||
the default path. Nobody has compared real pack-off pixels or production
|
||
performance against `6c79d35c`.
|
||
|
||
**Implementation**
|
||
|
||
1. Build `6c79d35c` in a throwaway worktree (Release). Capture with
|
||
`tools/run-offline-pixel-gate.ps1 -Out artifacts/vm0/base` — the tool's
|
||
own documented baseline mode.
|
||
2. Capture HEAD pack-off twice with the same tool and camera set:
|
||
`BuildingDetailTextures=false` (isolates the A2 normal change) and
|
||
`=true` (the shipped default).
|
||
3. Extend the tool's compare step with a **terrain mask** (same mechanism as
|
||
the existing `sky-mask.png`): pixels whose depth/material belong to
|
||
`terrain_modern` are excluded from the identity assertion and reported
|
||
separately.
|
||
4. Assert: HEAD(detail=false) vs base — **zero** differing pixels outside the
|
||
terrain mask; inside the mask, differences are reported as a histogram
|
||
(expected: low-amplitude shading deltas, no structural change).
|
||
HEAD(detail=true) vs HEAD(detail=false) — differences confined to
|
||
building/EnvCell surfaces (second mask from the detail replay's own
|
||
command set).
|
||
5. Production performance A/B, **no automation observer, no validation
|
||
layers, uncapped Release**, at the CLAUDE.md production-profile camera and
|
||
at pinned dense Arwic: base vs HEAD pack-off. Record CPU/GPU p50/p95/p99,
|
||
FPS, `alloc_kb p50`, GC counts. The pre-campaign references are
|
||
519.7 FPS / 1.869 ms CPU p50 (profile) and ~3.0/4.9 ms CPU p50/p95 (dense
|
||
Caul, digest §render). Deltas must sit inside run-to-run noise; any
|
||
allocation growth on the pack-off path is a regression to fix in this
|
||
slice, not to file.
|
||
6. Add the masked comparison as a repeatable tool mode
|
||
(`-Baseline … -TerrainMask`) so VM3/VM6 can re-run it.
|
||
|
||
**OUTCOME (2026-08-22): PASS.** Connected, visible-window, isolated-config captures: base+normals vs HEAD-off has zero strong static differences outside the animated lifestone at Holtburg and the open field; production CPU is 13% cheaper and allocation 25x lower on HEAD's pack-off path (no observer, uncapped Release). Three config traps (isolated FOV, the real Roaming settings still selecting the pack, minimized-window throttling) are recorded in the report. Replaced the planned terrain mask with a base+normals baseline (no mask needed).
|
||
|
||
**Acceptance:** the two pixel assertions in step 4 hold; the perf A/B shows
|
||
no default-path regression; the Fable report records the exact commands,
|
||
commits and artifact paths. If a regression is found, it is fixed here and
|
||
the report names the cause — no "accepted difference" without a diff that
|
||
explains it.
|
||
|
||
## VM1 — Remove the invented detail fade (F2)
|
||
|
||
**Why:** `RetailDetailTextureContract.FullDetailDistanceMetres = 10` /
|
||
`ZeroDetailDistanceMetres = 50` have no retail anchor. Retail's
|
||
`DrawBuilding`/`DrawEnvCell`/`RenderMeshSubset`/`SetDetailSurfaceInternal`
|
||
carry no distance term; attenuation is the LINEAR mip chain.
|
||
|
||
**Decision (Fable, retail-faithful rule):** remove the ramp. **Amended by VM2:** VM1 also re-ports the blend to the single-pass math (see the VM2 outcome) — the fade removal and the blend correction land together, with the `RetailDetailTextureContract` helper and tests rewritten around `Expected(base, detail, diffuseAlpha)`. Mip averaging
|
||
already converges the live category texture to its 1.033 mean factor.
|
||
|
||
**Implementation**
|
||
|
||
- Delete the two constants and `vDetailFade` from `mesh_detail.vert/.frag`
|
||
(`mesh_detail.vert:83`); the fragment outputs `applyFog(detail.rgb)` with alpha `detail.a * instanceOpacity` — fogged so the two-draw result collapses to retail's fog-after-combine pixel (VM1 review fix).
|
||
- Confirm the detail texture is uploaded with a full mip chain and sampled
|
||
LINEAR/LINEAR/LINEAR, WRAP — that *is* retail's attenuation; test it.
|
||
- Rename `RetailDetailTextureContract` members so nothing un-anchored is
|
||
labelled retail; keep the blend-factor helper and its tests.
|
||
- Update the #226 pseudocode note: strike the "10 m / 50 m" lines, add the
|
||
decomp citations above, and state that attenuation is mip-driven.
|
||
- If the owner later wants a ramp back, it returns as a **pack setting**
|
||
(enhancement), never on the default path.
|
||
|
||
**OUTCOME (2026-08-22): CLOSED.** Four commits: the single-pass re-port + fade removal (`05970306`); the interior shell/detail passes binding their own instance opacity — a pre-campaign stale-slot read (`388457a7`); the Opus fix round — the detail contribution is fogged so the two-draw result collapses to retail's fog-after-combine pixel, mip/sampler test, doc caveats (`ae651312`); and the closeout (AP-232 for the translucent-subset blend weight, the sampler test pinned to the production constant). Measured on Holtburg buildings: predicted +2.2/+0.66/+0.16 levels vs measured +2.17/+0.57/+0.16 (`artifacts/vm1`, grass/sky controls 0.00). Detail-on costs +0.3-0.5 ms CPU / +0.1 ms GPU at dense Arwic, still under the pre-campaign baseline. Opus dual-lens review: APPROVE WITH FIXES -> fix round -> narrow re-review APPROVE.
|
||
|
||
**Acceptance:** no reference to a distance fade remains in src or the note;
|
||
VM0's masked comparison re-run shows the only change is on detail surfaces;
|
||
at >50 m a building reads the same as at 60 m (mips), not a hard step.
|
||
|
||
## VM2 — Which detail path did retail hardware run? (F3, owner-gated)
|
||
|
||
**Why:** the port is of the two-pass framebuffer fallback. With
|
||
`m_caps.bCanDoSinglePassDetailing`, retail used stage-1 texture ops
|
||
(`PREMODULATE` + `BLENDCURRENTALPHA` — a lerp). The cap needs
|
||
`D3DTEXOPCAPS_PREMODULATE` (0x0059f6c6), which consumer drivers rarely
|
||
exposed — probable, not proven.
|
||
|
||
**Implementation:** one cdb script (`tools/cdb/vm2-detail-caps.cdb`): attach
|
||
to the PDB-paired `acclient.exe`, `dt acclient!RenderDevice::render_device`
|
||
→ `m_caps.bCanDoSinglePassDetailing`, `m_caps.bTexOpDotProduct3`, and the raw
|
||
`m_D3DCaps.TextureOpCaps`; `qd`. Record the answer in the #226 note and the
|
||
review doc.
|
||
|
||
- `bCanDoSinglePassDetailing == 0` → the port is the path players saw; close.
|
||
- `== 1` → file the single-pass stage math as the correct target, register
|
||
the current blend as a bounded divergence, and schedule the re-port as its
|
||
own slice (it is a fragment-shader change only).
|
||
|
||
**Acceptance:** the value is recorded with the binary GUID and the date.
|
||
|
||
**OUTCOME (2026-08-22): `bCanDoSinglePassDetailing = 1`, `trysinglepass = 1`.** Retail on the owner's GPU runs the single-pass stage path: `lerp(base·diffuse, detail.rgb, detail.a·diffuse.a)` — a mild darkening, not the fallback's brightening. The #226 port targets the wrong path and is re-ported inside VM1 (blend `SRCALPHA+INVSRCALPHA`, output `detail.rgb, detail.a·diffuseAlpha`, neutral at `detail.a == 0`). Also read: `LandscapeDetailTextures = 0` is a real separate preference. Evidence and the exact stage math: [2026-08-22-vm2-retail-detail-path-cdb.md](../research/2026-08-22-vm2-retail-detail-path-cdb.md).
|
||
|
||
## VM3 — Linear-light post stack (F4)
|
||
|
||
**Why:** bloom threshold, ACES, Rec.709 luma, saturation and the 0.5
|
||
contrast pivot all assume linear light; they receive gamma-encoded retail
|
||
colours and the output is never re-encoded. `exposure = 0.80` is the
|
||
compensation.
|
||
|
||
**Design**
|
||
|
||
- The main-world intermediate stays gamma-encoded `Rgba16Float` — the world
|
||
pass and its alpha blending are retail's and must not change.
|
||
- Every pack read of world colour decodes once: `acdreamDecode(c) = pow(c, 2.2)`
|
||
(a named function in `atmospheric_common.glsl`; 2.2 is the retail-era
|
||
display assumption, documented as such — not sRGB piecewise, which would
|
||
imply a precision the source never had).
|
||
- Bloom chain, sun-ray composite and volumetric composite operate in linear.
|
||
- `atmospheric_filmic.frag`: exposure → ACES → grade → vignette in linear,
|
||
then `acdreamEncode(c) = pow(c, 1/2.2)` to the UNORM swapchain.
|
||
- Defaults re-tuned so the **neutral** preset reproduces the pack-off image
|
||
within the VM0 tolerance when every effect is at its neutral value
|
||
(exposure 1.0, bloom 0, tonemap mix 0, saturation 1, contrast 1,
|
||
vignette 0) — the existing "every effect can be set to neutral" acceptance
|
||
now actually holds numerically, and is asserted by a test.
|
||
- Opinionated defaults (what the owner accepted at 0.80 exposure) are
|
||
re-derived in linear and presented for the visual gate; expect exposure to
|
||
return to ~1.0 and the bloom threshold to move up.
|
||
|
||
**Acceptance:** neutral preset ≡ pack-off within tolerance (new automated
|
||
test on the recording RHI plus VM0's masked tool on a real capture); Stage-1
|
||
luminance table re-captured; owner visual gate: "same look as accepted, no
|
||
clipping, highlights roll off". Budget unchanged (two `pow` per pixel).
|
||
|
||
## VM4 — Truthful documents (F5)
|
||
|
||
Correct, in the AR plan and its reports, the four sentences the review
|
||
named: the 2×2 oracle described as a production pin; the turning-hitch
|
||
conclusion drawn under the automation observer; the "zero skips" headline
|
||
(say "hermetic lanes"); and the Track A premise that `TerrainUtils.GetNormal`
|
||
produced a faceted look. Also correct the reviewer's own findings doc §4
|
||
(`2026-08-21-terrain-and-atmospheric-rendering-findings.md`): that function
|
||
orients scenery; the render normals were already smooth; A2 replaced
|
||
central-difference with retail's split-aware incident-face average.
|
||
Add the VM0 numbers as the new baseline table. Docs only; no code.
|
||
|
||
## VM5 — Volumetric jitter, and file the shared transform buffer (F8, F7)
|
||
|
||
- `atmospheric_volumetric.frag`: interleaved-gradient-noise offset per pixel
|
||
on the march start (`fract(52.9829189 * fract(dot(gl_FragCoord.xy,
|
||
vec2(0.06711056, 0.00583715))))`), plus the existing quarter/half-res
|
||
upsample. Automated: a fixed-camera capture's step-banding metric (row
|
||
autocorrelation at the step period) drops below the current value;
|
||
GPU delta < 0.02 ms.
|
||
- File issue: the shadow pass uploads a second transform buffer; a future
|
||
GPU-culling step should bind the main pass's instance SSBO. Not built here.
|
||
|
||
## VM6 — Foliage wind (the feature)
|
||
|
||
### What "real movement" means here
|
||
|
||
Trees do not wave like flags. Real foliage has **three motions at three
|
||
speeds**: the whole tree leans slowly with the mean wind (seconds), branches
|
||
swing at their own natural frequency (about one second), and leaves flutter
|
||
fast and independently (fractions of a second). Gusts come and go over tens
|
||
of seconds. Neighbouring trees are out of step. Trunk bases do not move.
|
||
Anything that ignores one of these reads as "screensaver" — which is what
|
||
the owner does not want.
|
||
|
||
### The rule for what sways (no guessing, data-driven)
|
||
|
||
- **Candidate set:** procedural scenery only — entity ids in the
|
||
`0x8XXYYIII` namespace (`ProceduralSceneryIdAllocator`, bit 31). Landblock
|
||
statics (fences, signposts, buildings), weenies, creatures and players never
|
||
sway.
|
||
- **Foliage subsets:** within a candidate, the **alpha-cutout material
|
||
subsets** (leaves, fronds, bushes, grass tufts). Rocks have none and stay
|
||
still; a tree's trunk is opaque and gets only the slow lean (below).
|
||
- **Opaque subsets of a candidate that also owns cutout subsets** (trunks,
|
||
branches) receive the slow lean only, scaled by height, never the flutter.
|
||
- A pack may **exclude** object ids (`FoliageExclusions`, a list in the
|
||
descriptor) for the rare scenery object that is cutout but not foliage.
|
||
There is no include list: the rule is the rule.
|
||
|
||
This is a render-only classification flag per batch — bit 1 of
|
||
`BatchData.flags` (`mesh_atmospheric.vert` already reads `flags`; bit 0 is
|
||
the #226 built-mesh marker). The dispatcher sets it once at classification
|
||
time from the entity id namespace and the subset's blend class; the pack-off
|
||
`mesh_modern` pipelines never read it.
|
||
|
||
### The motion (vertex shader, pack variant only)
|
||
|
||
Applied in `mesh_atmospheric.vert` and in all six
|
||
`directional_shadow_world_*` vertex shaders (constraint: **the shadow must
|
||
move with the leaf** — the caster and receiver displacement are one shared
|
||
include, `foliage_wind.glsl`, so they cannot drift apart).
|
||
|
||
Inputs: instance world origin `o` (translation column of
|
||
`Instances[i].transform`), vertex world position `p`, `time` (from
|
||
`uCameraAndTime.w`, already in the vertex stage), and the pack's wind block
|
||
(new `uFoliageWind` in `atmospheric_common.glsl`):
|
||
|
||
```
|
||
vec4 uFoliageWindDirection; // xy unit direction, z = mean strength [0..1], w = gust strength
|
||
vec4 uFoliageWindParams; // x = lean amplitude m, y = branch amplitude m, z = flutter amplitude m, w = max height m
|
||
```
|
||
|
||
All amplitudes are **metres at the canopy top**; they are pack settings with
|
||
bounded ranges, not shader constants.
|
||
|
||
```
|
||
h = clamp((p.z - o.z) / maxHeight, 0, 1) // 0 at the base, 1 at the top
|
||
k = h * h // bend grows with height²: bases stay put
|
||
ph = dot(o.xy, vec2(0.137, 0.291)) // per-tree phase from world position
|
||
g = 0.5 + 0.5 * sin(0.05 * t + ph) // gust envelope, ~20 s
|
||
+ 0.25 * sin(0.13 * t + 1.7 * ph)
|
||
s = mean + gust * g // instantaneous strength
|
||
|
||
lean = k * leanAmp * s * (0.8 + 0.2 * sin(0.35 * t + ph)) // slow lean
|
||
branch = k * branchAmp * s * sin(1.1 * t + ph + 2.0 * h) // ~1 Hz, phase runs up the tree
|
||
flutter = h * flutterAmp * s * sin(6.0 * t + 7.0 * fract(sin(dot(p.xy, vec2(12.9898, 78.233))) * 43758.5453))
|
||
|
||
d = dir * (lean + branch) + perp(dir) * 0.35 * branch + vec2(flutter) * normalize(vec2(cos, sin) of vertex hash)
|
||
p.xy += d
|
||
p.z -= 0.5 * dot(d, d) / max(h * maxHeight, 0.5) // bend shortens, it does not stretch
|
||
```
|
||
|
||
Opaque (trunk) subsets use `lean` only. Cutout subsets use all three. The
|
||
vertex hash decorrelates leaves on the same tree; the instance phase
|
||
decorrelates trees. The `p.z` term is the cheap length-preserving correction
|
||
so a bent canopy sinks slightly instead of growing.
|
||
|
||
Normals are **not** rotated (Gouraud on a cutout leaf with flipped lighting
|
||
would flicker); AC's flat-lit foliage does not need it.
|
||
|
||
### Weather drives it (AC owns the weather)
|
||
|
||
The pack's `AtmospherePolicyDeclaration` gains a `FoliageWind` table keyed by
|
||
the same categorical `activeDayGroup` the rays already use:
|
||
|
||
| Day group | mean | gust |
|
||
|---|---|---|
|
||
| Clear | 0.25 | 0.15 |
|
||
| Cloudy | 0.45 | 0.30 |
|
||
| Overcast | 0.60 | 0.35 |
|
||
| Rainy | 0.85 | 0.60 |
|
||
|
||
plus a global **Wind** slider (0–2×) and an **Off** setting. Transitions
|
||
between day groups interpolate over the existing weather delta seconds
|
||
(`uAtmosphereWeather.z`), so a weather change never snaps. Wind direction is
|
||
a pack setting (default NE→SW, 225°); there is no authored retail wind
|
||
direction to read, and the register row says so.
|
||
|
||
### Indoor, shadows, physics, picking
|
||
|
||
- Indoors (`!IsOutdoor`) wind is zero — EnvCells have no scenery anyway.
|
||
- Shadow casters apply the identical displacement (shared include).
|
||
- Physics is untouched by construction: the collision BSP is the trunk and
|
||
nothing here touches Runtime or Core physics. The acceptance test asserts
|
||
the Runtime/physics diff is empty.
|
||
- World picking (`WorldPicker`) picks against the undisplaced mesh; a leaf
|
||
may be up to `leanAmp + branchAmp` metres from its pick volume. That is
|
||
accepted and registered (one row, "render-only foliage displacement");
|
||
nobody picks leaves.
|
||
|
||
### Budget and quality scaling
|
||
|
||
- Cost: ~25 ALU per foliage vertex, zero CPU, zero submissions. Target:
|
||
< 0.05 ms GPU at 1080p dense foliage route; measured with the AR matrix
|
||
tooling (`tools/run-atmospheric-performance-matrix.ps1`).
|
||
- Low preset: lean + branch only (no flutter). Medium/High: all three.
|
||
|
||
### Acceptance
|
||
|
||
- Automated: the classification flag is set only for bit-31 entities' cutout
|
||
subsets (and lean-only for their opaque subsets); pack-off pipelines never
|
||
read the flag and VM0's masked comparison is unchanged; the shared include
|
||
is byte-identical between receiver and caster variants (test reads both
|
||
SPIR-V inputs' source); weather table interpolates without discontinuity;
|
||
budget row passes.
|
||
- **Owner visual gate**, in this order: (1) Holtburg outskirts, Clear, noon —
|
||
"barely moving, alive"; (2) same place, Rainy — "clearly windy, still not
|
||
a flag"; (3) watch one tree for 30 s — gusts arrive and leave, neighbours
|
||
out of step; (4) the tree's shadow on the ground moves with it; (5) walk
|
||
indoors — nothing moves; (6) pack off — nothing moves. The owner's words
|
||
decide; the numbers above are starting points to tune live.
|
||
|
||
## VM7 — Closeout and merge
|
||
|
||
- Full gates: `tools/run-release-gate.ps1` (hermetic lanes), the AR
|
||
reference matrix re-run for the changed presets, VM0's masked comparison
|
||
on the final binary, the connected lifecycle/reconnect route.
|
||
- Register: rows for the wind direction constant, the render-only
|
||
displacement vs picking, and whatever VM2 decides; TS-52 stays retired.
|
||
- Roadmap: move Campaign VM to shipped; AR's "final owner gate" rows that
|
||
VM0/VM3/VM6 satisfy are marked so in the AR plan.
|
||
- Memory: a `project_visualmaster_campaign.md` digest entry (the wind rule,
|
||
the linear-light decision, the VM0 oracle recipe).
|
||
- Owner: final pack-off / pack-on visual matrix, then merge to main.
|
||
|
||
## What this campaign does NOT do
|
||
|
||
- It does not touch the retail-faithful default path except through VM0's
|
||
evidence and VM1's removal of an un-anchored constant.
|
||
- It does not add normal maps, PBR, SSAO, water, or a second weather system.
|
||
- It does not move trunks' collision, walkability, or anything in Runtime.
|
||
- It does not animate landblock statics, weenies, grass decals that are not
|
||
scenery, or creatures.
|
||
- It does not rebuild the shadow transform buffer (filed, VM5).
|
||
- It does not re-port #226 to the single-pass path unless VM2 proves retail
|
||
ran it.
|
||
|
||
## Proposed owner goal
|
||
|
||
> **Campaign VM — VisualMaster.** Drive
|
||
> `docs/plans/2026-08-22-visualmaster-campaign.md` to shipped on branch
|
||
> `claude/git-sync-status-5fb1d2`: VM0 proves the pack-off path is
|
||
> pixel-identical to `6c79d35c` outside the terrain/detail masks with no
|
||
> production perf or allocation regression; VM1 removes the un-anchored
|
||
> detail fade; VM3 puts the post stack in linear light with a numerically
|
||
> neutral preset; VM4 makes every document truthful; VM5 de-bands the
|
||
> volumetrics; VM6 adds weather-driven foliage wind (scenery-only, cutout
|
||
> subsets, three motions, shadows follow) behind the render pack. Fable plans
|
||
> and verifies, Sonnet implements, Opus dual-lens reviews every slice. Stop
|
||
> me only for: the retail cdb read (VM2), the three visual gates (VM3, VM6,
|
||
> VM7), and the merge. Every closeout claim must name its command, lane and
|
||
> artifact.
|