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>
6.2 KiB
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:
- 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.
- 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). - 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.
LandscapeDetailTexturesis a real, separate preference, 0 here. Landscape detail is off by preference, not only by theChangeRegionliteral; there is no Options row for it in the 2013 client. Terrain detail remains out of #226's scope.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 ofDSTCOLOR + INVSRCALPHA; mesh_detail.fragoutputsvec4(detail.rgb, detail.a * diffuseAlpha), wherediffuseAlphais the base subset's vertex/material alpha (1 for opaque subsets; the translucent subsets already carry it);- neutral point:
detail.a == 0(no blend), notdetail.rgb == detail.a; RetailDetailTextureContract.FramebufferFactorbecomeslerp(1, detail.rgb / base, detail.a)semantics — rewrite the helper and its tests aroundExpected(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.