acdream/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md
Erik dcdd102824 docs(vm1): closeout - AP-232 for the translucent detail blend weight; sampler test pins the production constant
Opus narrow re-review of ae651312: 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 since
  05970306.
- 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>
2026-08-22 22:50:06 +02:00

267 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 §§12. 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.