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>
This commit is contained in:
Erik 2026-08-22 22:50:06 +02:00
parent ae6513126e
commit dcdd102824
5 changed files with 24 additions and 8 deletions

File diff suppressed because one or more lines are too long

View file

@ -77,7 +77,7 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level.
| Slice | Closes | Gate |
|---|---|---|
| VM0 | F1 — default-path invariance | **CLOSED 2026-08-22 — PASS**; [report](../research/2026-08-22-vm0-default-path-invariance.md) |
| VM1 | F2 — invented detail fade | automated |
| VM1 | F2/F3 — #226 single-pass re-port + fade removal | **CLOSED 2026-08-22**`05970306`, `388457a7`, `ae651312`, closeout; Opus APPROVE |
| VM2 | F3 — which retail detail path ran | **CLOSED 2026-08-22** — single-pass; see [cdb note](../research/2026-08-22-vm2-retail-detail-path-cdb.md) |
| VM3 | F4 — linear-light post stack | automated + **owner visual** |
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | docs |
@ -85,7 +85,7 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level.
| VM6 | Foliage wind | automated + **owner visual** |
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
Order is VM2 (done) → VM0 (done) → VM1 → VM4 → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured
Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 → VM5 → VM3 → VM6 → VM7. VM0 goes first because everything after it is measured
against the baseline it establishes.
---
@ -147,7 +147,7 @@ already converges the live category texture to its 1.033 mean factor.
**Implementation**
- Delete the two constants and `vDetailFade` from `mesh_detail.vert/.frag`
(`mesh_detail.vert:83`); the fragment outputs `detail.rgba` unscaled.
(`mesh_detail.vert:83`); the fragment outputs `applyFog(detail.rgb)` with alpha `detail.a * instanceOpacity` — fogged so the two-draw result collapses to retail's fog-after-combine pixel (VM1 review fix).
- Confirm the detail texture is uploaded with a full mip chain and sampled
LINEAR/LINEAR/LINEAR, WRAP — that *is* retail's attenuation; test it.
- Rename `RetailDetailTextureContract` members so nothing un-anchored is
@ -157,6 +157,8 @@ already converges the live category texture to its 1.033 mean factor.
- If the owner later wants a ramp back, it returns as a **pack setting**
(enhancement), never on the default path.
**OUTCOME (2026-08-22): CLOSED.** Four commits: the single-pass re-port + fade removal (`05970306`); the interior shell/detail passes binding their own instance opacity — a pre-campaign stale-slot read (`388457a7`); the Opus fix round — the detail contribution is fogged so the two-draw result collapses to retail's fog-after-combine pixel, mip/sampler test, doc caveats (`ae651312`); and the closeout (AP-232 for the translucent-subset blend weight, the sampler test pinned to the production constant). Measured on Holtburg buildings: predicted +2.2/+0.66/+0.16 levels vs measured +2.17/+0.57/+0.16 (`artifacts/vm1`, grass/sky controls 0.00). Detail-on costs +0.3-0.5 ms CPU / +0.1 ms GPU at dense Arwic, still under the pre-campaign baseline. Opus dual-lens review: APPROVE WITH FIXES -> fix round -> narrow re-review APPROVE.
**Acceptance:** no reference to a distance fade remains in src or the note;
VM0's masked comparison re-run shows the only change is on detail surfaces;
at >50 m a building reads the same as at 60 m (mips), not a hard step.

View file

@ -193,7 +193,7 @@ draw weighted by `detail.a * diffuseAlpha` (the pipeline in
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, already covered by the existing AP-34 shared-alpha-queue divergence
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

View file

@ -34,6 +34,16 @@ public sealed class TerrainAtlas : IDisposable
/// one-layer 2-D array so it uses the same backend-neutral table contract
/// as every other world texture.
/// </summary>
/// <summary>
/// Retail's detail-stage sampler: <c>ACRender::SetDetailSurfaceInternal</c>
/// (0x006b6280) sets WRAP/WRAP addressing and LINEAR/LINEAR/LINEAR filtering
/// on the detail stage. The LINEAR mip chain is retail's only distance
/// attenuation for built meshes (VM1), so this constant is load-bearing and
/// pinned by <c>TerrainAtlasDetailTextureTests</c>.
/// </summary>
internal static readonly GpuSamplerDescription DetailSamplerDescription =
GpuSamplerDescription.WorldRepeat;
internal readonly record struct RetailDetailTextureBinding(
GpuTextureSlot TextureSlot,
float Tiling,
@ -430,7 +440,7 @@ public sealed class TerrainAtlas : IDisposable
// ChangeRegion and the only reachable SmartBox caller keep
// landscape (0) and object (3) disabled, so do not create or expose
// those categories here.
detailSampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat);
detailSampler = device.CreateSampler(DetailSamplerDescription);
if (terrainDesc is { Count: > 1 })
{
buildingDetail = TryCreateDetailTexture(

View file

@ -65,7 +65,10 @@ public sealed class TerrainAtlasDetailTextureTests
},
};
IGpuSampler sampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat);
// The sampler is created from the PRODUCTION constant so that changing
// TerrainAtlas's choice (e.g. to a clamp sampler) fails the property
// assertions below rather than being forwarded unnoticed.
IGpuSampler sampler = device.CreateSampler(TerrainAtlas.DetailSamplerDescription);
MethodInfo method = typeof(TerrainAtlas).GetMethod(
"TryCreateDetailTexture",
@ -92,7 +95,7 @@ public sealed class TerrainAtlasDetailTextureTests
// point-filtered UI sampler.
GpuRecordedTextureRegistration registration = Assert.Single(
device.Calls.OfType<GpuRecordedTextureRegistration>());
Assert.Equal(GpuSamplerDescription.WorldRepeat, registration.Sampler);
Assert.Equal(TerrainAtlas.DetailSamplerDescription, registration.Sampler);
Assert.Equal(GpuFilter.Linear, registration.Sampler.MinFilter);
Assert.Equal(GpuFilter.Linear, registration.Sampler.MagFilter);
Assert.Equal(GpuMipFilter.Linear, registration.Sampler.MipFilter);