# 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.