acdream/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md

10 KiB
Raw Blame History

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 §§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 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 1050 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 1050 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.