10 KiB
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,
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 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.
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:
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=ZEROcorrection mode hidden behind the retail checkbox. - Physics, collision, walkability, and geometry are untouched.