# 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](2026-08-22-vm2-retail-detail-path-cdb.md) 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`](2026-08-21-terrain-and-atmospheric-rendering-findings.md), 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`](2026-07-10-detail-texturing.md). The A2 terrain-normal verdict and A3 subdivision disposition are recorded in the companion [`Terrain fidelity Track A report`](2026-08-21-terrain-fidelity-track-a-report.md). ## 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]`: ```text 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](2026-08-22-vm2-retail-detail-path-cdb.md) 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. ```text 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`: ```text 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`](../../src/AcDream.App/Rendering/RetailDetailTextureContract.cs) 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.