# Retail building and environment detail texturing — #226 port note **Date:** 2026-08-21 **Status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED 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 two-pass pseudocode Retail has both a single-pass multitexture route and a two-pass fallback. The Vulkan port uses the fallback because it preserves the already-accepted base pass byte-for-byte and expresses the retail framebuffer blend directly. ```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 zMetres = positive_view_space_depth_in_metres fade = clamp((50 m - zMetres) / (50 m - 10 m), 0, 1) // full through 10 m; linear 10–50 m; exactly zero at/after 50 m detail = sample(categoryTexture, baseUv * categoryTiling) src.rgb = detail.rgb * fade src.a = detail.a * fade 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 = DEST_COLOR destination = ONE_MINUS_SRC_ALPHA ``` Scaling **both** RGB and alpha by the fade is load-bearing. The resulting framebuffer multiplier is: ```text factor = 1 + fade * (detail.rgb - detail.a) ``` Thus fade zero is an exact no-op and the full-strength neutral point is `detail.rgb == detail.a` channel-by-channel. It is not 0.5 gray. ### 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. ## Brightening decision The port keeps retail's `DEST_COLOR + ONE_MINUS_SRC_ALPHA` verbatim. The findings measured factors **1.177**, **1.204**, and **1.033** for the three Dereth textures; the live Dereth building/environment category uses the 1.033-factor texture. That slight brightening is intentional retail parity, not an acceptance failure. Changing the destination factor to `ZERO` would be a visual correction rather than a port. Exposing both behaviors behind one retail checkbox would also make the option ambiguous. If a roughening-corrected material is wanted later, it belongs as an explicitly named opt-in enhancement/shader-pack policy with a registered divergence. It is not part of #226. ## 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. - Detail is full through positive view depth 10 m, fades linearly over 10–50 m, and is an exact no-op at and beyond 50 m. - Category source, 256 x 256 size, tiling 4, repeat addressing, and linear mip sampling match the measured Dereth data. - The retail 1.033 live-category brightening is expected. There is no `dst=ZERO` correction mode hidden behind the retail checkbox. - Physics, collision, walkability, and geometry are untouched.