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 | | Slice | Closes | Gate |
|---|---|---| |---|---|---|
| VM0 | F1 — default-path invariance | **CLOSED 2026-08-22 — PASS**; [report](../research/2026-08-22-vm0-default-path-invariance.md) | | 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) | | 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** | | VM3 | F4 — linear-light post stack | automated + **owner visual** |
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | docs | | 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** | | VM6 | Foliage wind | automated + **owner visual** |
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** | | 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. against the baseline it establishes.
--- ---
@ -147,7 +147,7 @@ already converges the live category texture to its 1.033 mean factor.
**Implementation** **Implementation**
- Delete the two constants and `vDetailFade` from `mesh_detail.vert/.frag` - 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 - Confirm the detail texture is uploaded with a full mip chain and sampled
LINEAR/LINEAR/LINEAR, WRAP — that *is* retail's attenuation; test it. LINEAR/LINEAR/LINEAR, WRAP — that *is* retail's attenuation; test it.
- Rename `RetailDetailTextureContract` members so nothing un-anchored is - 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** - If the owner later wants a ramp back, it returns as a **pack setting**
(enhancement), never on the default path. (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; **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; 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. 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 opaque subsets this is identical (both reduce to the base's own alpha
gating nothing else downstream); on translucent building/EnvCell subsets it gating nothing else downstream); on translucent building/EnvCell subsets it
is a bounded difference in how much the SUBSEQUENT alpha-queue compositing 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. row rather than a new one.
## Darkening, not brightening ## 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 /// one-layer 2-D array so it uses the same backend-neutral table contract
/// as every other world texture. /// as every other world texture.
/// </summary> /// </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( internal readonly record struct RetailDetailTextureBinding(
GpuTextureSlot TextureSlot, GpuTextureSlot TextureSlot,
float Tiling, float Tiling,
@ -430,7 +440,7 @@ public sealed class TerrainAtlas : IDisposable
// ChangeRegion and the only reachable SmartBox caller keep // ChangeRegion and the only reachable SmartBox caller keep
// landscape (0) and object (3) disabled, so do not create or expose // landscape (0) and object (3) disabled, so do not create or expose
// those categories here. // those categories here.
detailSampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat); detailSampler = device.CreateSampler(DetailSamplerDescription);
if (terrainDesc is { Count: > 1 }) if (terrainDesc is { Count: > 1 })
{ {
buildingDetail = TryCreateDetailTexture( 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( MethodInfo method = typeof(TerrainAtlas).GetMethod(
"TryCreateDetailTexture", "TryCreateDetailTexture",
@ -92,7 +95,7 @@ public sealed class TerrainAtlasDetailTextureTests
// point-filtered UI sampler. // point-filtered UI sampler.
GpuRecordedTextureRegistration registration = Assert.Single( GpuRecordedTextureRegistration registration = Assert.Single(
device.Calls.OfType<GpuRecordedTextureRegistration>()); 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.MinFilter);
Assert.Equal(GpuFilter.Linear, registration.Sampler.MagFilter); Assert.Equal(GpuFilter.Linear, registration.Sampler.MagFilter);
Assert.Equal(GpuMipFilter.Linear, registration.Sampler.MipFilter); Assert.Equal(GpuMipFilter.Linear, registration.Sampler.MipFilter);