From fe085cadaadd94cba8159fed4ca48321efcf3410 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 22 Aug 2026 18:23:27 +0200 Subject: [PATCH] research(vm2): retail runs SINGLE-PASS detail texturing - the #226 port targets the fallback Live cdb read on the PDB-paired retail client (owner's AMD GPU): m_caps.bCanDoSinglePassDetailing = 1, trysinglepass = 1, MaxSimultaneousTextures = 8, bTexOpDotProduct3 = 1, LandscapeDetailTextures = 0, EnvironmentDetailTextures = 1, landscape/object detail surfaces null, building/environment non-null, tiling 4 everywhere. So retail's detail combine on modern hardware is the texture-stage path in D3DPolyRender::SetSurface (stage0 alpha PREMODULATE, stage1 colour BLENDCURRENTALPHA): lerp(base*diffuse, detail.rgb, detail.a*diffuse.a) - a mild DARKENING (~-10% mid-tones with 0x06006D58), not the DSTCOLOR+INVSRCALPHA brightening that Campaign AR ported and that the 2026-08-21 findings doc + AR review both analysed. Those described the fallback for adapters without D3DTEXOPCAPS_PREMODULATE; the reviewer's guess that consumer drivers rarely advertised it was wrong. VM1 now carries the re-port (SRCALPHA+INVSRCALPHA, detail.rgb / detail.a*diffuseAlpha, neutral at a==0) together with the fade removal. No distance fade exists on either path. Scripts are read-only attaches with no breakpoints. Co-Authored-By: Claude Fable 5 --- .../plans/2026-08-22-visualmaster-campaign.md | 9 +- .../2026-08-22-vm2-retail-detail-path-cdb.md | 136 ++++++++++++++++++ .../evidence/2026-08-22-vm2-cdb-readings.txt | 26 ++++ tools/cdb/vm2-detail-caps-runner.ps1 | 7 + tools/cdb/vm2-detail-caps.cdb | 43 ++++++ tools/cdb/vm2-trysinglepass.cdb | 11 ++ 6 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 docs/research/2026-08-22-vm2-retail-detail-path-cdb.md create mode 100644 docs/research/evidence/2026-08-22-vm2-cdb-readings.txt create mode 100644 tools/cdb/vm2-detail-caps-runner.ps1 create mode 100644 tools/cdb/vm2-detail-caps.cdb create mode 100644 tools/cdb/vm2-trysinglepass.cdb diff --git a/docs/plans/2026-08-22-visualmaster-campaign.md b/docs/plans/2026-08-22-visualmaster-campaign.md index 468d44c0..250215c5 100644 --- a/docs/plans/2026-08-22-visualmaster-campaign.md +++ b/docs/plans/2026-08-22-visualmaster-campaign.md @@ -52,15 +52,14 @@ moves a pack-off pixel outside its declared mask is a bug, not a tuning. |---|---|---| | VM0 | F1 — default-path invariance | automated + Fable report | | VM1 | F2 — invented detail fade | automated | -| VM2 | F3 — which retail detail path ran | **owner action** (launch retail) | +| 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 VM0 → VM1 → VM4 → VM5 → VM3 → VM6 → VM2 (whenever the owner has -retail up) → VM7. VM0 goes first because everything after it is measured +Order is VM2 (done) → VM0 → VM1 → VM4 → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured against the baseline it establishes. --- @@ -114,7 +113,7 @@ explains it. `DrawBuilding`/`DrawEnvCell`/`RenderMeshSubset`/`SetDetailSurfaceInternal` carry no distance term; attenuation is the LINEAR mip chain. -**Decision (Fable, retail-faithful rule):** remove the ramp. Mip averaging +**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** @@ -155,6 +154,8 @@ review doc. **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 diff --git a/docs/research/2026-08-22-vm2-retail-detail-path-cdb.md b/docs/research/2026-08-22-vm2-retail-detail-path-cdb.md new file mode 100644 index 00000000..45228740 --- /dev/null +++ b/docs/research/2026-08-22-vm2-retail-detail-path-cdb.md @@ -0,0 +1,136 @@ +# VM2 — which detail-texturing path does retail actually run? (cdb, live) + +**Date:** 2026-08-22 · **Campaign:** VM slice VM2 · **Status:** CLOSED — answered +**Method:** read-only cdb attach to the live PDB-paired retail client +(`C:\Turbine\Asheron's Call\acclient.exe`, v11.4186, PDB GUID +`9e847e2f-777c-4bd9-886c-22256bb87f32`, verified `MATCH` by +`tools/pdb-extract/check_exe_pdb.py`), in-world on the owner's AMD GPU. +Scripts: `tools/cdb/vm2-detail-caps.cdb` (+ runner) and +`tools/cdb/vm2-trysinglepass.cdb`. No breakpoints; `qd` at top level. + +## The question + +Campaign AR's #226 port (`mesh_detail.frag`, pipeline blend +`DSTCOLOR + INVSRCALPHA`) reproduces the path retail takes when +`ACRender::SetDetailSurfaceInternal(0)` is called — the **two-pass +framebuffer fallback**. `RenderMeshSubset` (0x0059ca10) and `landPolyDraw` +(0x006b6320) only take that path when +`trysinglepass == 0 || !m_caps.bCanDoSinglePassDetailing`. Otherwise they +call `SetDetailSurfaceInternal(1)` and the combine is the texture-stage +setup in `D3DPolyRender::SetSurface` (0x0059c4d0). The review (F3) said the +fallback was "probably" what players saw. That was a guess. This settles it. + +## The readings (verbatim from the logs) + +``` +acclient!RenderDevice::render_device->m_caps + +0x008 MaxSimultaneousTextures : 8 + +0x00c MaxTextureBlendStages : 8 + +0x01c bCanDoSinglePassDetailing : 1 <-- THE ANSWER + +0x01d bTexOpDotProduct3 : 1 + +0x01e bTexOpBumpEnvMap : 1 + +trysinglepass (file-static, decomp 0x00820e40 / 0x00835c04 / 0x00835c10) +00820e40 00000001 +00835c04 00000001 +00835c10 00000001 + +acclient!Render::m_RenderPrefs + +0x004 LandscapeDetailTextures : 0 + +0x005 EnvironmentDetailTextures : 1 + +0x006 MultiPassAlpha : 1 + +acclient!Render::landscape_detail_surface = 0x00000000 (OFF) +acclient!Render::building_detail_surface = 0x0332e6a8 (ON) +acclient!Render::environment_detail_surface = 0x03416de0 (ON) +acclient!Render::object_detail_surface = 0x00000000 (OFF) +acclient!Render::landscape_detail_tiling = 4 +acclient!Render::building_detail_tiling = 4 +acclient!Render::environment_detail_tiling = 4 +acclient!Render::object_detail_tiling = 4 +acclient!Render::curr_detail_src_blend = 5 (static default; unused on this path) +acclient!Render::curr_detail_dst_blend = 6 +``` + +## What this means + +**Retail on this hardware uses the single-pass texture-stage path.** The +framebuffer blend that Campaign AR ported — and that the 2026-08-21 findings +doc and the AR review both analysed as "retail's detail pass brightens" — +is the fallback for adapters that cannot advertise +`D3DTEXOPCAPS_PREMODULATE`. A modern AMD driver advertises it. The reviewer's +recollection that consumer drivers rarely did was **wrong**; recorded here so +nobody repeats it. + +The single-pass combine, from `D3DPolyRender::SetSurface` with the detail +flag set (args are `(stage, op, arg1, arg2)`; `0 = DIFFUSE`, `1 = CURRENT`, +`2 = TEXTURE`): + +``` +stage 0 colour = MODULATE(TEXTURE, DIFFUSE) = base.rgb * diffuse.rgb +stage 0 alpha = PREMODULATE(DIFFUSE, DIFFUSE) = diffuse.a * detail.a (premodulate = multiply by the NEXT stage's texture) +stage 1 colour = BLENDCURRENTALPHA(TEXTURE, CURRENT) = detail.rgb * f + current.rgb * (1 - f), f = stage-0 alpha +stage 1 alpha = MODULATE(TEXTURE, CURRENT) = detail.a * current.a +stage 2 DISABLE +``` + +So the pixel retail actually draws is + +``` +lerp(base * diffuse, detail.rgb, detail.a * diffuse.a) +``` + +a **blend toward the detail colour by the detail alpha** — not +`dest × (detail + 1 − α)`. With the live category texture `0x06006D58` +(mean rgb 0.165, mean α 0.132) and opaque diffuse (α = 1), the average +effect is `≈ 0.868 × base + 0.022`: a mild **darkening** noise of roughly +−10 % on mid-tones. Rougher and darker — the intended look — not brighter. + +Consequences: + +1. The community remark "they did it backwards, looks like it reflects + more" describes the **fallback** path. On hardware like the owner's it + does not apply. +2. Campaign AR's #226 port reproduces the wrong path for every modern GPU. + It must be re-ported (below). The existing `RetailDetailTextureContract` + "brightening is expected" statement and the findings doc §2 table are + true only of the fallback and must say so (VM4). +3. There is **no distance fade on either path** (VM1 stands): the stage-1 + sampler is LINEAR/LINEAR/LINEAR with WRAP; attenuation is the mip chain + converging to the texture mean. +4. `LandscapeDetailTextures` is a real, separate preference, **0** here. + Landscape detail is off by preference, not only by the `ChangeRegion` + literal; there is no Options row for it in the 2013 client. Terrain + detail remains out of #226's scope. +5. `bTexOpDotProduct3 = 1`, `bTexOpBumpEnvMap = 1`: the DOT3 machinery is + available to retail on this hardware, which makes "what uses the BumpMap + path" (findings §7 q4) a live question, still untraced. + +## The re-port (folds into VM1; it is small) + +Because the lerp is expressible as a framebuffer blend, the existing +separate detail replay pass stays; only its pixel contract changes: + +- pipeline blend: `SRCALPHA + INVSRCALPHA`, `ADD` (the pair retail stores + as 5/6) instead of `DSTCOLOR + INVSRCALPHA`; +- `mesh_detail.frag` outputs `vec4(detail.rgb, detail.a * diffuseAlpha)`, + where `diffuseAlpha` is the base subset's vertex/material alpha (1 for + opaque subsets; the translucent subsets already carry it); +- neutral point: `detail.a == 0` (no blend), not `detail.rgb == detail.a`; +- `RetailDetailTextureContract.FramebufferFactor` becomes + `lerp(1, detail.rgb / base, detail.a)` semantics — rewrite the helper and + its tests around `Expected(base, detail, diffuseAlpha)`; +- the 10 m / 50 m fade is deleted as planned (VM1). + +A register row is **not** needed for the single-pass port — it is the +retail path. A row IS needed if the fallback is ever exposed (it should +not be). + +## Closed / corrected by this note + +- Review F3: answered — single-pass. The review's "probably fallback" was + wrong. +- Findings doc `2026-08-21-terrain-and-atmospheric-rendering-findings.md` §2 + "retail's own detail pass BRIGHTENS": true for the fallback only; the + hardware path darkens. VM4 amends the doc. +- AR plan + #226 pseudocode note: same amendment. diff --git a/docs/research/evidence/2026-08-22-vm2-cdb-readings.txt b/docs/research/evidence/2026-08-22-vm2-cdb-readings.txt new file mode 100644 index 00000000..ee98af6f --- /dev/null +++ b/docs/research/evidence/2026-08-22-vm2-cdb-readings.txt @@ -0,0 +1,26 @@ +00866354 acclient!Render::landscape_detail_surface = 0x00000000 +0081eca4 acclient!Render::curr_detail_dst_blend = 6 +0081eca0 acclient!Render::curr_detail_src_blend = 5 +0081ecb8 acclient!Render::curr_detail_tiling = 4 +00866364 acclient!Render::curr_detail_surface = 0x00000000 +0081eca8 acclient!Render::landscape_detail_tiling = 4 +0081ecb0 acclient!Render::building_detail_tiling = 4 +00866360 acclient!Render::object_detail_surface = 0x00000000 +0081ecb4 acclient!Render::object_detail_tiling = 4 +0081ecac acclient!Render::environment_detail_tiling = 4 +00866358 acclient!Render::building_detail_surface = 0x0332e6a8 +0086635c acclient!Render::environment_detail_surface = 0x03416de0 +Type does not have given member error at 'm_D3DCaps.MaxSimultaneousTextures' +Type does not have given member error at 'm_D3DCaps.MaxTextureBlendStages' + +0x008 MaxSimultaneousTextures : 8 + +0x00c MaxTextureBlendStages : 8 + +0x010 MaxSimultaneousRenderTargets : 4 + +0x01c bCanDoSinglePassDetailing : 1 + +0x01d bTexOpDotProduct3 : 1 + +0x01e bTexOpBumpEnvMap : 1 +00820e40 00000001 +00835c04 00000001 +00835c10 00000001 + +0x004 LandscapeDetailTextures : 0 + +0x005 EnvironmentDetailTextures : 1 + +0x006 MultiPassAlpha : 1 diff --git a/tools/cdb/vm2-detail-caps-runner.ps1 b/tools/cdb/vm2-detail-caps-runner.ps1 new file mode 100644 index 00000000..ee2fa0ba --- /dev/null +++ b/tools/cdb/vm2-detail-caps-runner.ps1 @@ -0,0 +1,7 @@ +# Campaign VM slice VM2 runner. Retail acclient.exe (2013 PDB-paired build) must be running and in-world. +$cdb = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" +$script = "$PSScriptRoot\vm2-detail-caps.cdb" +$p = Get-Process -Name acclient -ErrorAction SilentlyContinue +if (-not $p) { Write-Error "acclient.exe is not running"; exit 1 } +Write-Host "attaching to acclient pid $($p.Id) ..." +& $cdb -pn acclient.exe -cf $script *>&1 | Tee-Object -FilePath "$PSScriptRoot\..\..\artifacts\vm2\vm2-detail-caps.console.log" diff --git a/tools/cdb/vm2-detail-caps.cdb b/tools/cdb/vm2-detail-caps.cdb new file mode 100644 index 00000000..754fe76f --- /dev/null +++ b/tools/cdb/vm2-detail-caps.cdb @@ -0,0 +1,43 @@ +* Campaign VM slice VM2 - which detail-texturing path does retail run on this GPU? +* Read-only: attach, dump the capability + detail state, detach. No breakpoints. +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-blackburn-5333f0\artifacts\vm2\vm2-detail-caps.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe +.echo === module / pdb identity === +lmv m acclient +.echo === symbol resolution === +x acclient!RenderDevice::render_device +x acclient!Render::*detail* +x acclient!Render::m_RenderPrefs +x acclient!trysinglepass +.echo === THE ANSWER: single-pass detailing capability === +?? acclient!RenderDevice::render_device->m_caps.bCanDoSinglePassDetailing +?? acclient!RenderDevice::render_device->m_caps.bTexOpDotProduct3 +?? acclient!RenderDevice::render_device->m_caps.bTexOpBumpEnvMap +?? acclient!RenderDevice::render_device->m_caps.MaxSimultaneousTextures +?? acclient!RenderDevice::render_device->m_caps.MaxTextureBlendStages +.echo === raw D3D caps (TextureOpCaps: 0x8000 = BLENDCURRENTALPHA, 0x10000 = PREMODULATE, 0x4000 = DOTPRODUCT3) === +?? acclient!RenderDevice::render_device->m_D3DCaps.TextureOpCaps +?? acclient!RenderDevice::render_device->m_D3DCaps.MaxSimultaneousTextures +?? acclient!RenderDevice::render_device->m_D3DCaps.MaxTextureBlendStages +?? acclient!RenderDevice::render_device->m_D3DCaps.PixelShaderVersion +.echo === full m_caps dump === +dt acclient!RenderDevice poi(acclient!RenderDevice::render_device) m_caps. +.echo === trysinglepass global === +dd acclient!trysinglepass L1 +.echo === detail category state (null = category off) === +?? acclient!Render::landscape_detail_surface +?? acclient!Render::building_detail_surface +?? acclient!Render::environment_detail_surface +?? acclient!Render::object_detail_surface +?? acclient!Render::landscape_detail_tiling +?? acclient!Render::building_detail_tiling +?? acclient!Render::environment_detail_tiling +?? acclient!Render::object_detail_tiling +?? acclient!Render::curr_detail_src_blend +?? acclient!Render::curr_detail_dst_blend +?? acclient!Render::m_RenderPrefs.EnvironmentDetailTextures +.echo === done, detaching === +.logclose +qd diff --git a/tools/cdb/vm2-trysinglepass.cdb b/tools/cdb/vm2-trysinglepass.cdb new file mode 100644 index 00000000..2ef03ce2 --- /dev/null +++ b/tools/cdb/vm2-trysinglepass.cdb @@ -0,0 +1,11 @@ +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-blackburn-5333f0\artifacts\vm2\vm2-trysinglepass.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.reload /f acclient.exe +.echo === trysinglepass candidates (decomp: 00820e40 / 00835c04 / 00835c10, all init 1) === +dd 00820e40 L1 +dd 00835c04 L1 +dd 00835c10 L1 +.echo === RenderPrefs === +dt acclient!Render::m_RenderPrefs +.logclose +qd