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 <noreply@anthropic.com>
This commit is contained in:
parent
4dbbe8fead
commit
fe085cadaa
6 changed files with 228 additions and 4 deletions
|
|
@ -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 |
|
| VM0 | F1 — default-path invariance | automated + Fable report |
|
||||||
| VM1 | F2 — invented detail fade | automated |
|
| 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** |
|
| VM3 | F4 — linear-light post stack | automated + **owner visual** |
|
||||||
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | docs |
|
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | docs |
|
||||||
| VM5 | F8 — volumetric banding; F7 filed | automated |
|
| VM5 | F8 — volumetric banding; F7 filed | automated |
|
||||||
| VM6 | Foliage wind | automated + **owner visual** |
|
| VM6 | Foliage wind | automated + **owner visual** |
|
||||||
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
|
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
|
||||||
|
|
||||||
Order is VM0 → VM1 → VM4 → VM5 → VM3 → VM6 → VM2 (whenever the owner has
|
Order is VM2 (done) → VM0 → VM1 → VM4 → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured
|
||||||
retail up) → VM7. VM0 goes first because everything after it is measured
|
|
||||||
against the baseline it establishes.
|
against the baseline it establishes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -114,7 +113,7 @@ explains it.
|
||||||
`DrawBuilding`/`DrawEnvCell`/`RenderMeshSubset`/`SetDetailSurfaceInternal`
|
`DrawBuilding`/`DrawEnvCell`/`RenderMeshSubset`/`SetDetailSurfaceInternal`
|
||||||
carry no distance term; attenuation is the LINEAR mip chain.
|
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.
|
already converges the live category texture to its 1.033 mean factor.
|
||||||
|
|
||||||
**Implementation**
|
**Implementation**
|
||||||
|
|
@ -155,6 +154,8 @@ review doc.
|
||||||
|
|
||||||
**Acceptance:** the value is recorded with the binary GUID and the date.
|
**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)
|
## VM3 — Linear-light post stack (F4)
|
||||||
|
|
||||||
**Why:** bloom threshold, ACES, Rec.709 luma, saturation and the 0.5
|
**Why:** bloom threshold, ACES, Rec.709 luma, saturation and the 0.5
|
||||||
|
|
|
||||||
136
docs/research/2026-08-22-vm2-retail-detail-path-cdb.md
Normal file
136
docs/research/2026-08-22-vm2-retail-detail-path-cdb.md
Normal file
|
|
@ -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.
|
||||||
26
docs/research/evidence/2026-08-22-vm2-cdb-readings.txt
Normal file
26
docs/research/evidence/2026-08-22-vm2-cdb-readings.txt
Normal file
|
|
@ -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
|
||||||
7
tools/cdb/vm2-detail-caps-runner.ps1
Normal file
7
tools/cdb/vm2-detail-caps-runner.ps1
Normal file
|
|
@ -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"
|
||||||
43
tools/cdb/vm2-detail-caps.cdb
Normal file
43
tools/cdb/vm2-detail-caps.cdb
Normal file
|
|
@ -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
|
||||||
11
tools/cdb/vm2-trysinglepass.cdb
Normal file
11
tools/cdb/vm2-trysinglepass.cdb
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue