acdream/docs/research/2026-08-22-vm2-retail-detail-path-cdb.md
Erik fe085cadaa 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>
2026-08-22 18:23:27 +02:00

6.2 KiB
Raw Blame History

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.