Opus narrow re-review ofae651312: 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 since05970306. - 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>
14 KiB
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 §§1–2. 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:
- The Options checkbox writes
RenderPrefs.EnvironmentDetailTextures. Render::UpdateFromPreferences(0x0054d850) explicitly changesCurrent_Render_LandscapeDetailTexturesto0and callsSmartBox::SetDetailTexturing(smartbox, 0, environmentEnabled)at0x0054d9f3.SmartBox::SetDetailTexturing(0x00451df0) forwardsLScape::SetDetailTexturing(lscape, landscape, enabled, enabled, 0).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):
noFadeDetailgatesget_alpha_for_zto 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=ZEROcorrection mode and no brightening two-pass fallback hidden behind the retail checkbox. - Physics, collision, walkability, and geometry are untouched.