acdream/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md
Erik dcdd102824 docs(vm1): closeout - AP-232 for the translucent detail blend weight; sampler test pins the production constant
Opus narrow re-review of ae651312: APPROVE. Closes its three residuals:
- AP-232 filed: retail's single-pass stage-1 OUTPUT alpha
  (MODULATE(TEXTURE, CURRENT) @0x0059c549) is the blend weight for a
  translucent subset; acdream's two-draw model is exact for opaque
  subsets (fog identity pinned) and a bounded weight difference on
  translucent ones. Distinct from AP-34 (queue order). Owed since
  05970306.
- TerrainAtlas.DetailSamplerDescription names the production sampler
  (WRAP/LINEAR x3 per ACRender::SetDetailSurfaceInternal @0x006b6280);
  the test now asserts that constant's properties instead of a
  test-local copy.
- Plan VM1 section: fragment now described as fogged; VM1 marked CLOSED
  with the Holtburg measurement (+2.17/+0.57/+0.16 vs predicted
  +2.2/+0.66/+0.16) and the detail-on cost (+0.3-0.5 ms CPU at Arwic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 22:50:06 +02:00

14 KiB
Raw Blame History

Retail building and environment detail texturing — #226 port note

Date: 2026-08-21 · Amended: 2026-08-22 (Campaign VM, VM1) Status: IMPLEMENTED + CONNECTED-VISUAL-VERIFIED; re-ported to retail's single-pass detail combine by VM1 after VM2's live cdb read proved real hardware never takes the two-pass framebuffer fallback this note originally described. The "Exact two-pass pseudocode" and "Brightening decision" sections below are corrected in place; everything about the reachable setting/caller chain, the authored source data, and material coverage is unchanged and still applies.

This note is the implementation handoff requested by #226. The measurements below come from the already-completed 2026-08-21 terrain and atmospheric rendering findings, especially §§12. They are cited here rather than re-derived. The reachable preference/caller chain is also recorded in 2026-07-10 detail texturing. The A2 terrain-normal verdict and A3 subdivision disposition are recorded in the companion Terrain fidelity Track A report.

User-visible target and reachable caller trace

The issue title used to say “landscape,” but the Sept-2013 retail client does not expose live landscape detail through this option:

  1. The Options checkbox writes RenderPrefs.EnvironmentDetailTextures.
  2. Render::UpdateFromPreferences (0x0054d850) explicitly changes Current_Render_LandscapeDetailTextures to 0 and calls SmartBox::SetDetailTexturing(smartbox, 0, environmentEnabled) at 0x0054d9f3.
  3. SmartBox::SetDetailTexturing (0x00451df0) forwards LScape::SetDetailTexturing(lscape, landscape, enabled, enabled, 0).
  4. LScape::ChangeRegion (0x00506cb0) independently installs the same category state: (0, EnvDetail, EnvDetail, 0).

The four positions are landscape (0), building (1), environment/EnvCell (2), and ordinary object (3). The only reachable named-retail preference caller forces categories 0 and 3 off. DrawPartCell also clears ordinary-object detail. Therefore #226's scene target is building shells and interior EnvCell geometry, not outdoor terrain, scenery, creatures, or players. This also explains why acdream's existing checkbox is labelled “Building Detail Textures.”

Authored source, size, and sampling

Detail data is reached through Region(0x13000000).TerrainInfo.LandSurfaces.TexMerge.TerrainDesc[category]:

SurfaceTextureId = TerrainDesc[category].TerrainTex.DetailTextureId
tiling           = TerrainDesc[category].TerrainTex.DetailTexTiling
RenderSurfaceId  = SurfaceTexture(SurfaceTextureId).Textures[0]
rgba             = decode(RenderSurface(RenderSurfaceId), level 0)

For Dereth, enabled categories 1 and 2 both resolve 0x05001787 -> 0x06006D58, a 256 x 256 A8R8G8B8 texture, with tiling 4. The complete measured Dereth population is three textures across 33 entries: 0x050012AF -> 0x060037D2 (64 x 64, 29 entries), 0x05001786 -> 0x06006D57 (256 x 256, two), and the enabled-category texture above (256 x 256, two). See the findings §2 table.

Retail uses wrap addressing in U and V and linear minification, magnification, and mip filtering. Detail UV is baseUv * tiling. The port therefore uploads each live category as a one-layer RGBA8 texture array with a full mip chain and the existing repeat/linear world sampler.

Exact single-pass pseudocode

Retail has both a single-pass multitexture route and a two-pass framebuffer fallback for adapters that cannot advertise D3DTEXOPCAPS_PREMODULATE. VM2's live cdb read on the PDB-paired retail client found m_caps.bCanDoSinglePassDetailing = 1 and the file-static trysinglepass = 1 on real (AMD) hardware, so D3DPolyRender::RenderMeshSubset (0x0059ca10) never falls back for built meshes — every loaded CGfxObj sets use_built_mesh = 1 (CGfxObj::InitLoad 0x005346b0). The port below matches the path players actually saw.

enabled = DisplaySettings.BuildingDetailTextures   // existing setting; no new option

buildingDetail    = load_category(TerrainDesc[1])
environmentDetail = load_category(TerrainDesc[2])

for each retail built-mesh material subset:
    draw_existing_base_subset_unchanged()

    if enabled and subset belongs to a building or EnvCell:
        draw the same subset with its category detail texture
        // transparent/additive/inverse-alpha: detail follows its base
        // immediately, before the next delayed-alpha subset

for each replayed fragment:
    reject ordinary objects / landscape / scenery
    accept opaque, ClipMap, alpha, additive and inverse-alpha subsets

    // No distance term. ACRender::get_alpha_for_z (0x006b6230) is only
    // evaluated in D3DPolyRender::DrawPolyInternal (0x0059d7c0, the
    // immediate-polygon path) and only when the static noFadeDetail
    // (0x00820e38, initialised to 1) is 0 — unreachable for built meshes.
    // Attenuation is the LINEAR mip chain converging to the texture mean.

    detail = sample(categoryTexture, baseUv * categoryTiling)
    diffuseAlpha = base_subset_diffuse_alpha   // 1 for opaque; the
                                                // translucency-fade multiplier
                                                // for a fading subset.
                                                // tmpmaterial.Diffuse.a = 1f
                                                // (0x0059cb99) is the
                                                // burnedInStaticLights < 0 &&
                                                // *(render_device+0x7e4) == 0
                                                // branch in RenderMeshSubset;
                                                // the other branch leaves
                                                // diffuse FromVertex. Either
                                                // way the opaque->1 /
                                                // fading->opacity mapping
                                                // still holds.

    // D3DPolyRender::SetSurface (0x0059c4d0) texture-stage setup:
    //   stage 0 colour = MODULATE(TEXTURE, DIFFUSE)          = base.rgb * diffuse.rgb
    //   stage 0 alpha  = PREMODULATE(DIFFUSE, DIFFUSE)       = diffuseAlpha * detail.a
    //   stage 1 colour = BLENDCURRENTALPHA(TEXTURE, CURRENT) = lerp(current.rgb, detail.rgb, stage0.a)
    src.rgb = detail.rgb
    src.a   = detail.a * diffuseAlpha

    depth test  = EQUAL opaque; LESS_OR_EQUAL transparent
    depth write = preserve base class             // ON opaque; OFF transparent
    alpha-to-coverage = OFF                        // detail alpha is blend input
    blend op    = ADD
    source      = SRC_ALPHA
    destination = ONE_MINUS_SRC_ALPHA

The pixel this produces is lerp(base * diffuse, detail.rgb, detail.a * diffuseAlpha) — a blend toward the detail colour by detail.a * diffuseAlpha:

result = base * (1 - detail.a * diffuseAlpha) + detail.rgb * (detail.a * diffuseAlpha)

detail.a * diffuseAlpha == 0 is an exact no-op (fully-transparent detail texel, or a translucency fade that has reached zero). At detail.a * diffuseAlpha == 1 the result is exactly the detail colour. There is no "neutral gray" point — this is a lerp, not the fallback's multiplicative 1 + fade * (detail.rgb - detail.a) factor.

Built-mesh material coverage and order

The land-polygon SurfaceType & 4 exclusion does not narrow this built-mesh port. Named-retail RenderDeviceD3D::DrawEnvCell (0x0059f170) and DrawBuilding (0x0059f2a0) install curr_detail_surface before calling D3DPolyRender::DrawMesh. DrawMesh (0x0059d4a0) bypasses delayed-alpha queuing while that surface is installed and passes detail enabled to RenderMeshSubset (0x0059ca10) for each material subset. The fallback then redraws that exact subset with the detail surface before proceeding. Therefore ClipMap, straight-alpha, additive, and inverse-alpha built-mesh subsets are included alongside plain opaque ones.

The Vulkan port first filters the opaque object command stream to coalesced runs containing at least one category-1 building instance; nonbuilding-only commands never reach the detail pipeline. A mixed instanced command remains in the replay and mesh_detail rejects its ordinary instances individually. The accepted opaque path stays batched, while transparent subsets preserve immediate base/detail adjacency. Their separate detail pipeline keeps depth writes disabled, matching the base subset's accepted depth contract. This prevents another shell/object contribution from being composited between the base and its detail contribution.

Opaque detail uses depth compare EQUAL against the exact geometry just written by the base pass. Vulkan depth is per sample, so on MSAA ClipMap edges the detail affects only samples whose base alpha-to-coverage mask wrote depth. The detail pipeline itself deliberately keeps alpha-to-coverage off: detail alpha controls ONE_MINUS_SRC_ALPHA in the retail blend and is not the base coverage mask. Transparent bases do not write depth, so their adjacent detail uses LESS_OR_EQUAL with depth writes still off.

One bounded ordering seam is explicit: retail bypasses its delayed-alpha queue while curr_detail_surface is installed, whereas acdream retains its already- authoritative shared alpha-queue order and inserts the detail draw immediately after the corresponding base draw. This does not narrow material coverage or change base coverage/blend/depth behavior; it avoids making the checkbox reorder the default transparent scene. The connected acceptance matrix must still exercise overlapping transparent building/EnvCell surfaces.

Also unmodelled: retail's stage-1 OUTPUT alpha — MODULATE(TEXTURE, CURRENT) (0x0059c549) — which for a delayed-alpha subset becomes the framebuffer blend weight the alpha queue composites that subset with. acdream instead draws the base subset with its own alpha and a second, separately blended draw weighted by detail.a * diffuseAlpha (the pipeline in VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail)). For opaque subsets this is identical (both reduce to the base's own alpha gating nothing else downstream); on translucent building/EnvCell subsets it is a bounded difference in how much the SUBSEQUENT alpha-queue compositing sees, registered as its own row AP-232 (blend WEIGHT on translucent subsets), distinct from AP-34 (queue ORDER) row rather than a new one.

Darkening, not brightening

The earlier version of this note read the two-pass fallback's DEST_COLOR + ONE_MINUS_SRC_ALPHA as retail's blend and reported the findings doc's 1.177, 1.204, and 1.033 framebuffer-multiplier measurements for the three Dereth textures as intentional retail brightening. VM2 showed that factor formula belongs to the fallback only, which real hardware does not run. The single-pass lerp above has the opposite sign: with the live Dereth building/environment category texture (mean rgb 0.165, mean alpha 0.132) and opaque diffuse (diffuseAlpha = 1), the combine is ≈ 0.868 × base + 0.022 — a mild darkening of roughly 10% on mid-tones, not a brightening. This is retail's actual on-screen behavior on the hardware the game shipped on; it is not a visual correction and carries no opt-out. See RetailDetailTextureContract and the VM2 note for the exact numbers.

What the reverted experiment got wrong

The experiment described by c25d6186 was never committed as renderer code; it was reverted from the worktree with git checkout. Its useful failure record remains in that issue commit. It differed from the verified contract in five material ways:

  • It targeted outdoor landscape, while the live setting enables building and environment categories and forces landscape off.
  • It built a per-terrain-type texture array, while retail selects one category-scoped surface and scalar tiling for each draw path.
  • It used base * detail * 2 (MODULATE2X) instead of retail's framebuffer blend.
  • It assumed 128 gray was neutral; retail neutral is RGB equal to alpha.
  • Its acceptance prohibited an overall brightness change, although retail's measured blend intentionally brightens these textures.

The old OpenGL-specific array/bindless wiring is also not reusable in the current Vulkan-only RHI.

Corrected acceptance

  • With BuildingDetailTextures=false, no detail replay is submitted and the current base rendering remains unchanged.
  • With it true, toggling the existing Options checkbox visibly changes buildings and interior/EnvCell surfaces without a restart. The connected 2026-08-21 Facility Hub A/B/A gate applied the real Config checkbox on -> off -> restored-on and captured the same nearby walls/floor after each transition. Static right-wall mean absolute RGB error was 2.132 for on/off versus 0.007 for original-on/restored-on; the floor row was 3.385 versus 0.013. The persisted setting was observed false during B, restored true, and the session ended with ACE-confirmed graceful logout.
  • Outdoor terrain, ordinary scenery/objects, creatures, and players do not gain this overlay.
  • Every built building/EnvCell material subset is eligible: opaque, ClipMap, straight alpha, additive, and inverse alpha. Transparent base/detail draws remain adjacent in acdream's authoritative shared alpha order.
  • Opaque object replay submits only command runs containing a building; mixed commands are filtered per instance. Depth equality inherits the base pass's per-sample ClipMap coverage without applying A2C to detail alpha.
  • There is no distance fade (VM1, VM2): noFadeDetail gates get_alpha_for_z to the immediate-polygon path only, which built meshes never reach. Attenuation is the sampler's linear mip chain converging to the texture mean; a building reads the same well past 50 m as it does at 10 m, not a hard step.
  • Category source, 256 x 256 size, tiling 4, repeat addressing, and linear mip sampling match the measured Dereth data.
  • The retail single-pass combine darkens the live category texture by roughly 10% on mid-tones (VM2); this is expected. There is no dst=ZERO correction mode and no brightening two-pass fallback hidden behind the retail checkbox.
  • Physics, collision, walkability, and geometry are untouched.