docs(vm4): every Campaign AR document says what its evidence shows

Dated 'VM4 correction' notes, not silent rewrites:
- AR plan: NoOpRenderPackProductionIntegrationTests is a 2x2 recording-
  device fixture, not a production pin; the real invariance is VM0's.
  Both '14,928/14,928 zero skips' totals qualified: hermetic lane filter.
- Stage-2 connected report: the dense-town CPU figures ran under the
  automation observer (9.8 MB/frame alloc) and say nothing about the
  owner's hitch on the product; clean numbers are VM0's.
- Track A report + findings doc: TerrainUtils.GetNormal only orients
  scenery; the render normals were already smooth; A2 replaced central
  differences with retail's incident-face average (the reviewer's own
  premise error).
- Findings doc: 'retail brightens' is the fallback path only; real
  hardware runs the single-pass lerp (VM2). LandscapeDetailTextures = 0
  answers the landscape open question. VM0 production table added as the
  baseline.
- Completion audit retail-path row and the review's F1/F2/F3/F5 headers
  annotated with their closures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-22 22:51:59 +02:00
parent dcdd102824
commit 6e76314b08
7 changed files with 117 additions and 30 deletions

View file

@ -80,11 +80,14 @@ gated on the single `Render::m_RenderPrefs.EnvironmentDetailTextures`. **So
retail's setting is a BUILDINGS-AND-INTERIORS setting, not a terrain
setting** — which explains why its UI label is "Building Detail Textures".
`SmartBox::SetDetailTexturing` (0x00451df0) *can* pass landscape through — its
callers are **untraced**; that is the open question for whether terrain detail
is ever enabled in practice.
`SmartBox::SetDetailTexturing` (0x00451df0) *can* pass landscape through.
**ANSWERED 2026-08-22 (VM2 cdb read):** `Render::m_RenderPrefs.LandscapeDetailTextures`
is a real, separate preference and reads **0** on the live client
(`EnvironmentDetailTextures = 1`); `landscape_detail_surface` is null while
`building_detail_surface`/`environment_detail_surface` are set. Terrain detail
is off by preference, and there is no Options row for it.
### The blend — and retail's own bug
### The blend — the FALLBACK path (see the correction below)
`ACRender::SetDetailSurfaceInternal` (0x006b6280):
@ -117,12 +120,18 @@ category:
| `0x06006D57` | 0.414 | 0.210 | **1.204** | **+20% brighter** |
| `0x06006D58` | 0.165 | 0.132 | **1.033** | **+3% brighter** |
**Retail's own detail pass BRIGHTENS rather than roughens.** A proper
roughening modulate needs `dst = ZERO` so the result centres on 1.0; here the
`dest x (1-alpha)` term adds most of the original back on top. This
independently confirms the community observation that it looks like surfaces
"reflect more instead of being rougher", and it raises a real design decision
for #226 (see open questions).
**The FALLBACK detail pass brightens rather than roughens** — and, **CORRECTED
2026-08-22 (VM2/VM4)**, the fallback is not what real hardware runs. The
framebuffer blend above is taken only when `stage == 0`, i.e. when the adapter
cannot advertise `D3DTEXOPCAPS_PREMODULATE`. A live cdb read on the owner's AMD
GPU gave `m_caps.bCanDoSinglePassDetailing = 1` and `trysinglepass = 1`, so
retail uses the single-pass texture-stage path in `D3DPolyRender::SetSurface`
(0x0059c4d0): `lerp(base * diffuse, detail.rgb, detail.a * diffuse.a)` — a mild
blend toward the detail colour (about 10 % on mid-tones with the live category
texture). The community's "reflect more instead of being rougher" describes
the fallback only. Full evidence:
[`2026-08-22-vm2-retail-detail-path-cdb.md`](2026-08-22-vm2-retail-detail-path-cdb.md).
The #226 port was re-done to the single-pass math at Campaign VM VM1.
### Terrain draw path
@ -168,9 +177,14 @@ non-single-pass fallback that is **untraced**. Vertex lighting comes from
- Diagonal split direction comes from the `FSplitNESW` hash of world cell
coords (constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`, `0x519B8F25`).
- `src/AcDream.Core/Rendering/Wb/TerrainUtils.cs` `GetNormal` returns the
**flat per-face normal** (cross product of whichever triangle the sample
lands in). **No averaging anywhere in the tree.** That is what produces the
faceted look.
**flat per-face normal**. **CORRECTED 2026-08-22 (VM4):** that function only
orients procedural scenery (`SceneryGenerator.cs:166`); it never fed the
render normal, and the claim that it produced a faceted look was wrong. The
rendered mesh already used smooth central-difference normals (Phase 3b,
`LandblockMesh`). Retail (`CLandBlockStruct::calc_lighting` 0x00531700)
averages the unit plane normals of each vertex's incident polygons; Campaign
AR's A2 ported that, replacing central differences — a real parity port with
a subtle visual delta (VM0: 1114 % of pixels at mean |Δ| ≈ 11.5).
**Open question, must be answered from the decomp, no guessing:** does retail
smooth terrain vertex normals? Entry points: `ACRender::landPolyDraw` (both
@ -233,6 +247,19 @@ registered bounded divergence.
profile is **CPU-SUBMISSION-BOUND** (memory:
`feedback_render_perf_measurement`).
**VM0 baseline (2026-08-22, connected, uncapped Release, no automation
observer, `ACDREAM_FRAME_PROF=1`; `2026-08-22-vm0-default-path-invariance.md`):**
| Spot | Binary | CPU p50 / p95 | GPU p50 | alloc KB/frame |
|---|---|---|---|---|
| Holtburg | `6c79d35c` | 4.7 / 5.1 ms | 0.4 | 574 |
| Holtburg | Campaign AR, pack off | 4.1 / 4.4 ms | 0.4 | 21 |
| Arwic (dense) | `6c79d35c` | 6.0 / 6.5 ms | 0.7 | 582 |
| Arwic (dense) | Campaign AR, pack off | 5.2 / 5.6 ms | 0.7 | 29 |
Any figure captured under `ACDREAM_AUTOMATION_ARTIFACT_DIR` carries the
render-scene observer's allocation and is NOT comparable with this table.
Consequences:
- GPU-side **fullscreen** work is nearly free in observed FPS — it fills GPU
@ -308,9 +335,9 @@ Design points the user asked for:
3. What is the non-single-pass detail fallback in `landPolyDraw`? (§2)
4. What uses the `BumpMap` / `DotProduct3` path, if not the detail textures?
(§3)
5. For #226: port retail's brightening blend **verbatim**, fix it to `dst=ZERO`
so it roughens as intended and register the deviation, or expose both behind
the setting? (§2)
5. ~~For #226: port retail's brightening blend verbatim, fix it, or expose
both?~~ **ANSWERED 2026-08-22:** neither — the brightening blend was the
fallback; VM1 ported the single-pass lerp that real hardware runs (§2).
## 8. Where this sits

View file

@ -51,9 +51,15 @@ The decisive named-retail function is
4. The following sunlight/ambient loop dots those normalized shared-vertex
normals with `LScape::sunlight` and writes per-vertex lighting.
That is incident-face normal averaging, not flat per-face shading. The
WorldBuilder-derived `TerrainUtils.GetNormal` identified in the findings §4
is therefore a simplified tool path and not the retail oracle.
That is incident-face normal averaging. **VM4 correction (2026-08-22):** the
findings doc's premise that `TerrainUtils.GetNormal` produced a faceted render
was wrong — that function only orients procedural scenery
(`SceneryGenerator.cs:166`) and never fed the render normal. The rendered
terrain mesh already had smooth central-difference normals (Phase 3b,
`LandblockMesh`). The real change A2 made is central-difference smoothing →
retail's split-aware incident-face average: a genuine, decomp-verified parity
port, but a subtler visual change than either document implied (VM0 measured
1114 % of pixels at mean |Δ| ≈ 11.5 on the same view).
The approved port is in `LandblockMesh.BuildRetailVertexNormals`. It uses the
same split hash and exact emitted triangle topology, accumulates each

View file

@ -24,7 +24,7 @@ been pushed.
| Requirement | Verdict | Authoritative evidence |
|---|---|---|
| Stage 1 project-owner stop | Pass | The owner's 2026-08-22 “Looks good!” acceptance, after exposure 1.0 → 0.80, is recorded in the [live-gate report](2026-08-22-atmospheric-stage1-live-gate.md). |
| Retail-faithful path stays default and authoritative | Pass | The checked-in production no-op oracle, six physical pack-off rows, connected disable/restore transitions, and package-failure rows all pass. A branch diff contains only newly named atmospheric/detail shader sources plus the generated shader manifest; no pre-campaign retail shader source is modified. |
| Retail-faithful path stays default and authoritative | Pass (evidence strengthened by VM0, 2026-08-22: exact pack-off pixel identity vs `6c79d35c` on static content and a clean production perf A/B — see `2026-08-22-vm0-default-path-invariance.md`; the no-op oracle named here is a 2x2 recording-device fixture) | The checked-in production no-op oracle, six physical pack-off rows, connected disable/restore transitions, and package-failure rows all pass. A branch diff contains only newly named atmospheric/detail shader sources plus the generated shader manifest; no pre-campaign retail shader source is modified. |
| Physics and collision unchanged | Pass | `git diff origin/main...HEAD -- src/AcDream.Runtime src/AcDream.App/Physics src/AcDream.Core/Physics` is empty. The connected routes use the existing gameplay simulation without changing it. |
| Tier 1 bloom, filmic tone mapping, colour grade, vignette, neutral settings, resize, and retained-UI/private-view isolation | Pass for implementation and available automation/physical rows | Slice 1 tests and the RX 9070 XT 30-row matrix pass; final project-owner image judgment remains below. |
| Tier 1 authored-sun rays | Pass for implementation and connected state transitions | Authored sun/day-group/weather, occlusion-mask, behind-camera/off gates, and pre-tonemap composition are automated; connected time/weather transitions pass. Final physical-display dawn/noon/dusk, occlusion, and edge-flicker judgment remains below. |

View file

@ -72,8 +72,20 @@ camera turn, server, character, entity population (15,522), and warmed scene:
| Six-second turn | 27.8 / 32.9 / 36.7 ms | 28.6 / 32.2 / 40.2 ms | 0.6 / 1.2 ms | 1.5 / 1.9 ms |
| Stationary | 28.6 / 30.2 / 31.1 ms | 29.1 / 30.9 / 32.6 ms | 0.9 / 0.9 ms | 1.7 / 1.8 ms |
The user's turning hitch reproduces on the retail path: Medium did not worsen
turn CPU p95 (32.2 versus 32.9 ms). It adds the expected GPU work, about
**VM4 correction (2026-08-22):** both runs below executed under the connected
automation observer (`ACDREAM_AUTOMATION_ARTIFACT_DIR` render-scene oracle),
which the Slice H closeout documents as allocating 3.910.5 MiB/frame
(`alloc_kb p50 = 9,830` here, 31 Gen0 collections in 216 frames). The absolute
CPU figures are therefore observer cost, not product cost, and this A/B says
nothing about the owner's turning hitch on the product. Clean production
numbers (no observer, uncapped Release) are in the
[VM0 report](2026-08-22-vm0-default-path-invariance.md): dense Arwic CPU p50
6.0 ms pre-campaign vs 5.2 ms pack-off. The paragraph below is kept as the
historical record of the Medium-minus-retail increment, which the observer does
not distort.
With that caveat: Medium did not worsen turn CPU p95 under the observer (32.2
versus 32.9 ms). It adds the expected GPU work, about
0.70.9 ms at p95/p50 in this run. The Atmospheric screenshot's pack-owned
window reports incremental CPU p50/p95/p99 of 0.209/0.248/0.302 ms, inclusive
GPU p50/p95/p99 of 1.374/1.818/1.869 ms, 14,890 casters, three cascades,

View file

@ -55,6 +55,10 @@ run; decide F2; schedule F3/F4 as follow-ups; then the owner visual gate.
### F1 — HIGH (evidence): default-path invariance is unproven
> **CLOSED (VM0, 2026-08-22): PASS.** Exact pixel identity on static content
> vs `6c79d35c` (Holtburg + open field) and a clean production perf A/B with no
> regression — `2026-08-22-vm0-default-path-invariance.md`.
The plan says `NoOpRenderPackProductionIntegrationTests` "pins the
pre-campaign pass list, pipeline set, draw/dispatch tuple, framebuffer
SHA-256". It does — for a **2×2 synthetic framebuffer, one draw call, one
@ -86,6 +90,14 @@ record shows it was run against a `6c79d35c` capture. The six physical
### F2 — HIGH (process): the #226 fade constants are invented and unregistered
> **Post-review correction (VM1, 2026-08-22):** "invented" was too strong. The
> 10 m → 50 m ramp is a real retail function, `ACRender::get_alpha_for_z`
> (0x006b6230). It is, however, dead for the surfaces #226 targets: retail
> evaluates it only in `D3DPolyRender::DrawPolyInternal` (immediate polygons)
> and only when the static `noFadeDetail` (0x00820e38, initialised 1) is 0;
> built meshes light with `Diffuse.a = 1`. The finding's conclusion (remove
> it; no register row) stands; the provenance claim is corrected.
`RetailDetailTextureContract.FullDetailDistanceMetres = 10f` /
`ZeroDetailDistanceMetres = 50f` drive `mesh_detail.vert:83`
(`clamp((50 z) / 40, 0, 1)`). The pseudocode note presents "full through
@ -104,6 +116,11 @@ keep it and file the register row naming it an acdream adaptation.
### F3 — MEDIUM (premise): #226 ports retail's two-pass fallback; which path real hardware ran is undetermined
> **ANSWERED (VM2, 2026-08-22):** `m_caps.bCanDoSinglePassDetailing = 1`,
> `trysinglepass = 1` on the owner's GPU — retail runs the single-pass path.
> The reviewer's "probably the fallback" was wrong. See
> `2026-08-22-vm2-retail-detail-path-cdb.md`; re-ported at VM1.
`SetDetailSurfaceInternal` sets the `DSTCOLOR + INVSRCALPHA` framebuffer blend
**only when `stage == 0`** (the two-pass fallback). With
`m_caps.bCanDoSinglePassDetailing`, `RenderMeshSubset` and `landPolyDraw`
@ -138,6 +155,10 @@ every other tool's controls. Schedule as a slice.
### F5 — MEDIUM (docs): closeout claims exceed their evidence
> **CLOSED (VM4, 2026-08-22):** (a)(d) corrected in place in the AR plan, the
> Stage-2 connected report, the Track A report and the findings doc, each with a
> dated "VM4 correction" note rather than a silent rewrite.
a. "Pins the pre-campaign … framebuffer SHA-256" — see F1.
b. "The user's turning hitch reproduces on the retail path" is drawn from
`connected-dense-town-20260822-133727` (retail/off: CPU p50 **27.8 ms**,