fix(rendering): preserve opaque building coverage under detail MSAA (#473)

This commit is contained in:
Erik 2026-09-05 08:07:51 +02:00
parent d35ed1aece
commit bf23673f3d
7 changed files with 387 additions and 46 deletions

View file

@ -41,7 +41,8 @@ remain closed. See
## #473 — Owner gate: exterior buildings have missing roof/wall sections ## #473 — Owner gate: exterior buildings have missing roof/wall sections
**Status:** IN-PROGRESS — owner visual gate FAIL, 2026-09-05. **Status:** IN-PROGRESS — bounded code repair complete; owner visual re-gate
pending, 2026-09-05.
At clean campaign `02219318a`, Release 0 warnings / 0 errors, the owner At clean campaign `02219318a`, Release 0 warnings / 0 errors, the owner
reports broken exterior buildings in Holtburg and the cathedral, while reports broken exterior buildings in Holtburg and the cathedral, while
@ -52,7 +53,12 @@ A reachable draw-state defect is now identified (packet section42): detail
final alpha is wrongly converted into MSAA sample coverage on opaque Wb final alpha is wrongly converted into MSAA sample coverage on opaque Wb
exteriors. The owner's 4x/A2C=true log and installed detail alpha24..46/255 exteriors. The owner's 4x/A2C=true log and installed detail alpha24..46/255
exercise that path. Interior EnvCell disables A2C. A bounded pipeline repair exercise that path. Interior EnvCell disables A2C. A bounded pipeline repair
and actual exterior re-gate remain; no claim that all symptoms are fixed yet. now forces the shared detail-active opaque Wb command onto the non-A2C opaque
pipeline while retaining pure Clip's AP-240 quality-selected arm. Actual
ordered and classic grouped recording tests cover ordinary/atmospheric 1x/4x,
including a mixed ordinary/building instance command, and the old-arm mutation
fails 12/12 discriminators. The actual exterior re-gate remains; no claim that
all symptoms are fixed yet.
Evidence: `research/2026-09-01-overhaul/owner-gate-20260905-holtburg-fail.png`; Evidence: `research/2026-09-01-overhaul/owner-gate-20260905-holtburg-fail.png`;
manual session `logs/owner-gate-20260905-02219318a/`, PID18836, verified manual session `logs/owner-gate-20260905-02219318a/`, PID18836, verified

View file

@ -278,6 +278,7 @@ builder, reducing the current active total to **160**.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| ~~AP-245~~ | **FILED AND CODE-RETIRED 2026-09-05 by #473 / Campaign OVERHAUL §42.** S5-c4's shared detail-active Wb selector reused the quality-selected opaque pipeline for `RetailSetSurfaceBlend.Opaque`. With 4x MSAA and A2C enabled, the shader's retail final detail alpha `X = a*qA*qA` therefore became sample coverage even though retail's ordinary opaque SetSurface state is ONE/ZERO without alpha test. The selector now uses the non-A2C `pipelines.Opaque` for every detail-bearing opaque command; pure Clip retains AP-240's `opaquePipeline`. A classic command may contain both ordinary and building-detail instances because detail category is per instance rather than part of `GroupKey`; such a command is correctly non-A2C as a whole while its shader still filters detail per instance. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForMaterial`); `tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs` | The correction is one shared material-selection arm reached by the actual ordered and classic grouped submitters. Recording tests cover ordinary/atmospheric 1x/4x descriptors, draw order/count, depth and detail pushes; detail unavailable/disabled and quality A2C=false stay unchanged. | None for the retired code divergence. The owner exterior gate remains FAIL until the repaired exact binary passes the required real re-gate; this row does not claim GPU or owner acceptance. | `D3DPolyRender::SetSurface @0x0059C4D0` opaque ONE/ZERO arm; Campaign OVERHAUL §42 |
| AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 425083425303; Campaign OVERHAUL S5-c4 §23.2 | | AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 425083425303; Campaign OVERHAUL S5-c4 §23.2 |
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` | | AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` | | AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |

View file

@ -696,7 +696,7 @@ Update immediately when a slice changes state. Chat is not the ledger.
| S5-#470 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL A/B PROVISIONAL PASS.** Campaign implementation stack `15a796c3a` -> `7506e5f14` -> `b333edb4f`; route `19b44e5e3`; reviewed scratch `51f974da4` -> `2cad9c84` -> `98c004aa7`; packet §§2730. | G4 UNPASSED | Retail/behavior pass 2/10 and production pass 4/10 closed the two evidence-only findings. Fresh campaign Release 0W/0E and focused 48/48; exact scratch hermetic 16,976/16,976 and canonical InstalledDat 386/10/1, manifests 30/30. Pinned gate `logs/selfgate-20260905-032132-s5-470-pinned-shadow-ab-r1`: retail/off -> High -> retail/off, 3/3 PNGs, exit 0, graceful, no client; visual PASS provisional. The stale recipe-8 pre-route launch is excluded and recorded in §30. | | S5-#470 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL A/B PROVISIONAL PASS.** Campaign implementation stack `15a796c3a` -> `7506e5f14` -> `b333edb4f`; route `19b44e5e3`; reviewed scratch `51f974da4` -> `2cad9c84` -> `98c004aa7`; packet §§2730. | G4 UNPASSED | Retail/behavior pass 2/10 and production pass 4/10 closed the two evidence-only findings. Fresh campaign Release 0W/0E and focused 48/48; exact scratch hermetic 16,976/16,976 and canonical InstalledDat 386/10/1, manifests 30/30. Pinned gate `logs/selfgate-20260905-032132-s5-470-pinned-shadow-ab-r1`: retail/off -> High -> retail/off, 3/3 PNGs, exit 0, graceful, no client; visual PASS provisional. The stale recipe-8 pre-route launch is excluded and recorded in §30. |
| S5-#469 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL TRANSITION PROVISIONAL PASS.** Contract `809887524`; reviewed scratch `c09b6cf0f`; campaign implementation `94ddde69a`; route `62efc72cb`; packet §§3132. | G4 UNPASSED | Retail/deviation pass 1/10 and production pass 2/10 both PASS with no finding. The two atmospheric vertex receivers keep retail's authored unnormalized `uLights` direction across every shadow gate; celestial direction remains for opt-in shadow/volumetric projection; IA-24 corrected in the same commit. Exact scratch: Release 0W/0E, focused 126/126, Vulkan 2/2, hermetic 16,983/16,983, InstalledDat 386/10/1. Fresh campaign Release 0W/0E, focused 82/82, Vulkan 2/2. Gate `logs/selfgate-20260905-040449-s5-469-lighting-transition-r1`: five PNGs, active 2,500-caster/four-cascade High rows, no near-black relight, exit 0/graceful, no client; visual PASS provisional. | | S5-#469 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL TRANSITION PROVISIONAL PASS.** Contract `809887524`; reviewed scratch `c09b6cf0f`; campaign implementation `94ddde69a`; route `62efc72cb`; packet §§3132. | G4 UNPASSED | Retail/deviation pass 1/10 and production pass 2/10 both PASS with no finding. The two atmospheric vertex receivers keep retail's authored unnormalized `uLights` direction across every shadow gate; celestial direction remains for opt-in shadow/volumetric projection; IA-24 corrected in the same commit. Exact scratch: Release 0W/0E, focused 126/126, Vulkan 2/2, hermetic 16,983/16,983, InstalledDat 386/10/1. Fresh campaign Release 0W/0E, focused 82/82, Vulkan 2/2. Gate `logs/selfgate-20260905-040449-s5-469-lighting-transition-r1`: five PNGs, active 2,500-caster/four-cascade High rows, no near-black relight, exit 0/graceful, no client; visual PASS provisional. |
| S5-c5 | **CLOSED + LANDED 2026-09-05; LEAD GRAPHICAL SMOKE PROVISIONAL PASS.** Contract `b77989c32`; campaign `bf53e2ad6` -> `e625dc4e6` -> `1b7ee4e58`; reviewed scratch tip `158656f0d`; packet §§3335. | G4 UNPASSED | Retail/deletion pass 1/10 PASS. Production pass 2/10 found one static-field hole in the owner guard; fix round 1 reproduced both static-owner mutations, and production pass 3/10 PASS. Exact scratch hermetic 16,921/16,921, canonical InstalledDat 368/9/1, manifests 30/30. Fresh campaign Release 0W/0E, App 146/146, Core 8/8. A stale recipe-8 preflight is excluded; a non-destructive recipe-10 bake produced 2,237,865 keys with zero failures. Corrected gate `logs/selfgate-20260905-052130-s5-c5-landed-v10`: five PNGs, exit 0/graceful, no fatal/deleted-prefix match, no client left; geometry matches S5-c4, visual PASS provisional. | | S5-c5 | **CLOSED + LANDED 2026-09-05; LEAD GRAPHICAL SMOKE PROVISIONAL PASS.** Contract `b77989c32`; campaign `bf53e2ad6` -> `e625dc4e6` -> `1b7ee4e58`; reviewed scratch tip `158656f0d`; packet §§3335. | G4 UNPASSED | Retail/deletion pass 1/10 PASS. Production pass 2/10 found one static-field hole in the owner guard; fix round 1 reproduced both static-owner mutations, and production pass 3/10 PASS. Exact scratch hermetic 16,921/16,921, canonical InstalledDat 368/9/1, manifests 30/30. Fresh campaign Release 0W/0E, App 146/146, Core 8/8. A stale recipe-8 preflight is excluded; a non-destructive recipe-10 bake produced 2,237,865 keys with zero failures. Corrected gate `logs/selfgate-20260905-052130-s5-c5-landed-v10`: five PNGs, exit 0/graceful, no fatal/deleted-prefix match, no client left; geometry matches S5-c4, visual PASS provisional. |
| S5 | **OWNER VISUAL GATE FAIL — #473 exterior roof/wall sections missing (2026-09-05).** c1c5 plus #470/#469 landed; prior self-PASS is not owner acceptance. C1 offline evidence complete; C1c test correction frozen pending review. | G4 | Packet §§7 and 1242 bind. Owner's clean02219318a Release/recipe10 gate failed Holtburg/cathedral exteriors; Facility interiors appear correct. §42 contracts the identified detail-active opaque A2C pipeline repair. The 4x/A2C=true owner path converts low detail alpha into lost coverage; a tested repair and correctly framed exterior re-gate remain. Prior screenshots did not cover complete exteriors. After repair/reviews/re-gate: C1c review/landing, real geometry/membership hash, full-lane closure, C2 and full owner G4 remain. Never merge main before G4. | | S5 | **OWNER VISUAL GATE FAIL — #473 code-repaired and review-closed; exterior re-gate pending (2026-09-05).** c1c5 plus #470/#469 landed; prior self-PASS is not owner acceptance. C1 offline evidence complete; C1c test correction frozen pending review. | G4 | Packet §§7 and 1242 bind. §42 repairs detail-active opaque A2C coverage loss at the owner's 4x/A2C=true settings. Both independent review lenses PASS; 45 focused/46 register-inclusive/158 adjacent tests pass; restored old-arm mutation fails12/12. All52 affected base/LOD prepared payloads match fresh extraction. Correctly framed full Holtburg/cathedral exterior PNG re-gate remains; prior images were inadequate. Then C1c review/landing, real geometry/membership hash, full-lane closure, C2 and full owner G4 remain. Never merge main before G4. |
--- ---

View file

@ -4010,3 +4010,66 @@ PASS/FAIL; close only the lead's own session gracefully. Lead PASS remains
provisional until the owner re-gates. Keep #473 open if rendered sections provisional until the owner re-gates. Keep #473 open if rendered sections
remain missing, even if the narrow tests pass. No merge main; C1c and all remain missing, even if the narrow tests pass. No merge main; C1c and all
remaining closeout/G4 obligations resume only after this failure is handled. remaining closeout/G4 obligations resume only after this failure is handled.
### 42.4 Implementation return — uncommitted, pending review and real re-gate
At base `d35ed1aec`, the bounded production repair changes only
`PipelineForMaterial`'s detail-active Opaque arm from `opaquePipeline` to
`pipelines.Opaque`; the pure Clip arm and every shader, prepared-data,
geometry, LOD, queue and recipe path are unchanged. The actual ordered and
classic grouped recording paths now cover ordinary and atmospheric pipeline
sets at 1x and 4x. They pin exact non-A2C/A2C descriptors, opaque depth state,
draw order/count and detail push fields. The classic proof also includes one
real mixed command with detail categories `[0,1]`: because category is not in
`GroupKey`, the whole detail-bearing command uses non-A2C while the shader's
per-instance category still controls detail sampling. Detail unavailable,
detail disabled and quality A2C=false remain on their pre-repair selections.
The old-arm mutation (`Opaque => opaquePipeline`) failed all 12 selected
ordered/classic cases. The first ordered failure expected
`wb-mesh-opaque` at pipeline transcript position 2 but received
`wb-mesh-opaque-a2c`; the first classic failure expected
`wb-mesh-atmospheric-opaque-1x` at position 1 but received
`wb-mesh-atmospheric-opaque-a2c-1x`. The mixed-command discriminator likewise
expected non-A2C and received A2C. The one-line repair was restored and the
complete `OrderPreservingSubmitterTests` class passed 45/45 in Release.
`dotnet build AcDream.slnx -c Release --no-restore` then completed with zero
warnings and zero errors. A final no-build run of that class plus the existing
register/count guard passed 46/46. `git diff --check` is clean. No client was
launched; JSON preflights before each build/test found no AcDream, retail or
debugger process and ACE UDP9000 PID13340. The owner gate remains FAIL and the
required exact-binary exterior re-gate is pending sequential review and lead
execution.
### 42.5 Lead review closure — graphical acceptance still pending
Sequential independent OpenAI review round1 passed both lenses without
findings: Rawls checked named SetSurface, paired PE bytes, installed detail
alpha, callers and retained material behavior; Wegener checked production
reachability, state/order/ownership, actual dispatcher tests and gate honesty.
The lead independently read the complete six-file diff, verified the same
retail byte ranges and TRX mutation failures, and ran the adjacent existing
material suite: 158/158 passed, zero skipped. Exact result files are under
`tests/AcDream.App.Tests/TestResults/`: `s5-473-old-arm-mutation.trx` (0/12),
`s5-473-focused-restored-final.trx` (45/45),
`s5-473-focused-and-register-final.trx` (46/46), and
`s5-473-lead-adjacent-material.trx` (158/158).
The tested candidate App.dll and the test directory's App.dll were identical:
6F8890AA5E7FA16641EB5E9AB8CEDAC748E9CD5084853964EDCB5512EBE48E71.
This is pre-commit candidate identity, not the subsequent graphical binary.
Lead independently expanded the installed A9B4/F418 base/nonzero-LOD union
and compared fresh canonical extraction against the actual recipe10 package:
all52 complete ObjectMeshDataSerializer payloads match, no missing or corrupt
read and no differing payload. This rules out that package/extractor mismatch
for these meshes, not GPU upload or final coverage. It is not a replacement
for C1's real placed geometry/membership witness.
No review finding or implementation fix round was needed. Mixed classic
commands use one non-A2C opaque pipeline for all instances if any has detail;
only wholly unarmed commands/global detail-off preserve quality-selected A2C.
The owner expressly authorized closing/testing. Their original process had
already exited gracefully without lead input. Lead may now commit, rebuild
Release with no client present and execute the correctly framed exterior gate.
Owner FAIL remains authoritative until that rendered result is inspected;
there is no G4 or physical-GPU PASS in this review closure.

View file

@ -58,9 +58,10 @@ public sealed unsafe partial class WbDrawDispatcher
/// <summary> /// <summary>
/// The four pipeline buckets a translucency kind resolves to, independent /// The four pipeline buckets a translucency kind resolves to, independent
/// of any live <see cref="MeshPipelineSet"/> instance. <see cref="AlphaToCoverage"/> /// of any live <see cref="MeshPipelineSet"/> instance. <see cref="AlphaToCoverage"/>
/// selects between <c>Opaque</c> and <c>OpaqueAlphaToCoverage</c> uniformly /// selects between <c>Opaque</c> and <c>OpaqueAlphaToCoverage</c> for the
/// for the whole submission — it never varies per command — so merge-run /// default opaque bucket. A solo detail-category command may then select
/// legality only needs to know WHICH bucket a command falls in, not which /// its exact material pipeline (including non-A2C Opaque), so merge-run
/// legality needs the bucket plus the existing detail boundary, not the
/// concrete <see cref="IGpuPipeline"/> that resolves to. That is what lets /// concrete <see cref="IGpuPipeline"/> that resolves to. That is what lets
/// <see cref="BuildOrderedMergeRuns"/> stay pure CPU logic, testable /// <see cref="BuildOrderedMergeRuns"/> stay pure CPU logic, testable
/// without a live GPU device — the same separation /// without a live GPU device — the same separation

View file

@ -858,7 +858,7 @@ public sealed unsafe partial class WbDrawDispatcher
RetailSetSurfaceMaterialState material, RetailSetSurfaceMaterialState material,
IGpuPipeline opaquePipeline) => material.Blend switch IGpuPipeline opaquePipeline) => material.Blend switch
{ {
RetailSetSurfaceBlend.Opaque => opaquePipeline, RetailSetSurfaceBlend.Opaque => pipelines.Opaque,
RetailSetSurfaceBlend.StraightAlpha => material.AlphaTestEnabled RetailSetSurfaceBlend.StraightAlpha => material.AlphaTestEnabled
? pipelines.AlphaBlendDepthWrite ? pipelines.AlphaBlendDepthWrite
: pipelines.AlphaBlend, : pipelines.AlphaBlend,

View file

@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu;
@ -288,58 +289,201 @@ public sealed class OrderPreservingSubmitterTests
} }
[Theory] [Theory]
[InlineData(false, 200f / 255f)] [InlineData(false, 1)]
[InlineData(true, 100f / 255f)] [InlineData(false, 4)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace( [InlineData(true, 1)]
bool paletted, [InlineData(true, 4)]
float expectedReference) public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
bool atmospheric,
int sampleCount)
{ {
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true); foreach (bool paletted in new[] { false, true })
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1),
MakeCommand(
2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: paletted)),
MakeCommand(3));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
string opaque = $"{prefix}-opaque{suffix}";
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal((0u, 0f, 0f), DetailFields(runs[0].Constants));
Assert.Equal((77u, 3.5f, 0f), DetailFields(runs[1].Constants));
Assert.Equal(
(77u, 3.5f, paletted ? 100f / 255f : 200f / 255f),
DetailFields(runs[2].Constants));
Assert.Equal((0u, 0f, 0f), DetailFields(runs[3].Constants));
Assert.Equal(
[opaque, opaqueA2c, opaque, opaqueA2c, opaqueA2c],
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
.Select(call => call.PipelineName)
.ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
}
}
[Theory]
[InlineData(false, 1)]
[InlineData(false, 4)]
[InlineData(true, 1)]
[InlineData(true, 4)]
public void ClassicGrouped_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
bool atmospheric,
int sampleCount)
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf( ExecuteClassicGroups(
MakeCommand(0), fx,
MakeCommand(1, detailCategory: 1), MakeClassicGroup(0, detailCategory: 0),
MakeCommand( MakeClassicGroup(1, detailCategory: 1),
MakeClassicGroup(
2, 2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1, detailCategory: 1,
translucency: TranslucencyKind.ClipMap,
materialState: RetailSetSurfaceMaterialState.Resolve( materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap, SurfaceType.Base1ClipMap,
texturePresent: true, texturePresent: true,
textureHasPalette: paletted)), textureHasPalette: false)),
MakeCommand(3)); MakeClassicGroup(3, detailCategory: 0));
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
string opaque = $"{prefix}-opaque{suffix}";
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
var transcript = DecodePipelineRuns(fx.Device);
Assert.Equal([opaqueA2c, opaque, opaqueA2c, opaqueA2c],
transcript.Select(entry => entry.Pipeline));
Assert.All(transcript, entry => Assert.Equal(1u, entry.Draw.DrawCount));
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[0].Constants));
Assert.Equal((77u, 3.5f, 0f), DetailFields(transcript[1].Constants));
Assert.Equal((77u, 3.5f, 200f / 255f), DetailFields(transcript[2].Constants));
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[3].Constants));
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
}
[Theory]
[InlineData(false, 1)]
[InlineData(false, 4)]
[InlineData(true, 1)]
[InlineData(true, 4)]
public void ClassicGrouped_MixedOrdinaryAndBuildingInstances_UsesNonCoverageForWholeCommand(
bool atmospheric,
int sampleCount)
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw();
WbDrawDispatcher.InstanceGroup mixed = MakeClassicGroup(0, detailCategory: 0);
AppendClassicInstance(mixed, index: 1, detailCategory: 1);
ExecuteClassicGroups(fx, mixed);
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
Assert.Equal($"{prefix}-opaque{suffix}", entry.Pipeline);
Assert.Equal(1u, entry.Draw.DrawCount);
Assert.Equal((77u, 3.5f, 0f), DetailFields(entry.Constants));
Assert.Equal([0u, 1u], mixed.DetailCategories);
Assert.Equal(2, mixed.Matrices.Count);
}
[Theory]
[InlineData(false, true, true)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public void PrepareThenDraw_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
bool detailAvailable,
bool detailEnabled,
bool alphaToCoverage)
{
using var fx = new DispatcherFixture(
detailAvailable: detailAvailable,
detailEnabled: detailEnabled,
sampleCount: 4,
alphaToCoverage: alphaToCoverage);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream); PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); bool detailActive = detailAvailable && detailEnabled;
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)], string selected = alphaToCoverage ? "wb-mesh-opaque-a2c" : "wb-mesh-opaque";
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal((0u, 0f), (runs[0].Constants.TextureIndexA, runs[0].Constants.ParamA));
Assert.Equal((77u, 3.5f), (runs[1].Constants.TextureIndexA, runs[1].Constants.ParamA));
Assert.Equal((77u, 3.5f), (runs[2].Constants.TextureIndexA, runs[2].Constants.ParamA));
Assert.Equal((0u, 0f), (runs[3].Constants.TextureIndexA, runs[3].Constants.ParamA));
Assert.Equal( Assert.Equal(
[ ["wb-mesh-opaque", selected, detailActive ? "wb-mesh-opaque" : selected],
"wb-mesh-opaque-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
],
fx.Device.Calls.OfType<GpuRecordedPipelineBind>() fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
.Select(call => call.PipelineName) .Select(call => call.PipelineName));
.ToArray()); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count()); Assert.Equal([(0, 1), (1, 1)], runs.Select(run => (run.Start, run.Count)));
Assert.Equal(expectedReference, runs[2].Constants.ParamB); Assert.Equal((0u, 0f, 0f), DetailFields(runs[0].Constants));
GpuPipelineDescription a2c = fx.Device.CreatedPipelines Assert.Equal(
.Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x") detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
.Description; DetailFields(runs[1].Constants));
Assert.Equal(GpuBlendMode.None, a2c.Blend); }
Assert.True(a2c.AlphaToCoverage);
Assert.True(a2c.Depth.Test); [Theory]
Assert.True(a2c.Depth.Write); [InlineData(false, true, true)]
Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare); [InlineData(true, false, true)]
[InlineData(true, true, false)]
public void ClassicGrouped_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
bool detailAvailable,
bool detailEnabled,
bool alphaToCoverage)
{
using var fx = new DispatcherFixture(
detailAvailable: detailAvailable,
detailEnabled: detailEnabled,
sampleCount: 4,
alphaToCoverage: alphaToCoverage);
using DrawScope draw = fx.BeginDraw();
ExecuteClassicGroups(fx, MakeClassicGroup(0, detailCategory: 1));
bool detailActive = detailAvailable && detailEnabled;
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
Assert.Equal(alphaToCoverage && !detailActive
? "wb-mesh-opaque-a2c"
: "wb-mesh-opaque", entry.Pipeline);
Assert.Equal(1u, entry.Draw.DrawCount);
Assert.Equal(
detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
DetailFields(entry.Constants));
} }
[Fact] [Fact]
@ -832,6 +976,130 @@ public sealed class OrderPreservingSubmitterTests
private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) => private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) =>
[.. DecodeRuns(device).Select(r => (r.Start, r.Count))]; [.. DecodeRuns(device).Select(r => (r.Start, r.Count))];
private static (uint TextureIndexA, float ParamA, float ParamB) DetailFields(
GpuPushConstants constants) =>
(constants.TextureIndexA, constants.ParamA, constants.ParamB);
private static void AssertOpaquePipeline(
RecordingGpuDevice device,
string name,
bool atmospheric,
int sampleCount,
bool alphaToCoverage)
{
GpuPipelineDescription description = device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == name)
.Description;
Assert.Equal(atmospheric ? "mesh_atmospheric" : "mesh_modern", description.Shaders.Name);
Assert.Equal(GpuBlendMode.None, description.Blend);
Assert.Equal(alphaToCoverage, description.AlphaToCoverage);
Assert.Equal(sampleCount, description.SampleCount);
Assert.True(description.Depth.Test);
Assert.True(description.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, description.Depth.Compare);
}
private static WbDrawDispatcher.InstanceGroup MakeClassicGroup(
int index,
uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.Opaque,
RetailSetSurfaceMaterialState? materialState = null)
{
var group = new WbDrawDispatcher.InstanceGroup
{
FirstIndex = (uint)index * 3,
BaseVertex = index * 4,
IndexCount = 3,
TextureSlot = new GpuTextureSlot((uint)index),
TextureLayer = 0,
Translucency = translucency,
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
SurfaceOpacity = 1f,
CullMode = CullMode.CounterClockwise,
FoliageFlags = 0,
};
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
group.SubmissionOrders.Add(index);
group.Slots.Add(0);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
group.IndoorFlags.Add(0);
group.DetailCategories.Add(detailCategory);
group.Opacities.Add(1f);
group.SelectionLighting.Add(Vector2.Zero);
return group;
}
private static void AppendClassicInstance(
WbDrawDispatcher.InstanceGroup group,
int index,
uint detailCategory)
{
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
group.SubmissionOrders.Add(index);
group.Slots.Add(0);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
group.IndoorFlags.Add(0);
group.DetailCategories.Add(detailCategory);
group.Opacities.Add(1f);
group.SelectionLighting.Add(Vector2.Zero);
}
private static void ExecuteClassicGroups(
DispatcherFixture fixture,
params WbDrawDispatcher.InstanceGroup[] groups)
{
fixture.Dispatcher.BeginFrame(frameSlot: 0);
MethodInfo execute = typeof(WbDrawDispatcher).GetMethod(
"ExecuteClassifiedGroups",
BindingFlags.Instance | BindingFlags.NonPublic)!;
execute.Invoke(
fixture.Dispatcher,
[
Matrix4x4.Identity,
Vector3.Zero,
0u,
groups,
WbDrawDispatcher.EntitySet.All,
groups.Length,
groups.Length,
false,
false,
]);
}
private static List<(
string Pipeline,
GpuPushConstants Constants,
GpuRecordedMultiDrawIndirect Draw)> DecodePipelineRuns(RecordingGpuDevice device)
{
var transcript = new List<(
string Pipeline,
GpuPushConstants Constants,
GpuRecordedMultiDrawIndirect Draw)>();
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
for (int i = 0; i < calls.Count; i++)
{
if (calls[i] is not GpuRecordedMultiDrawIndirect draw)
continue;
string? pipeline = null;
GpuPushConstants? constants = null;
for (int prior = i - 1; prior >= 0 && (pipeline is null || constants is null); prior--)
{
if (pipeline is null && calls[prior] is GpuRecordedPipelineBind bind)
pipeline = bind.PipelineName;
if (constants is null && calls[prior] is GpuRecordedPushConstants push)
constants = push.Constants;
}
Assert.NotNull(pipeline);
Assert.NotNull(constants);
transcript.Add((pipeline!, constants!.Value, draw));
}
return transcript;
}
private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns( private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns(
RecordingGpuDevice device) RecordingGpuDevice device)
{ {
@ -892,6 +1160,7 @@ public sealed class OrderPreservingSubmitterTests
bool atmospheric = false, bool atmospheric = false,
bool receiverBindingAvailable = true, bool receiverBindingAvailable = true,
int sampleCount = 1, int sampleCount = 1,
bool alphaToCoverage = true,
RecordingGpuDevice? device = null) RecordingGpuDevice? device = null)
{ {
Device = device ?? new RecordingGpuDevice(); Device = device ?? new RecordingGpuDevice();
@ -923,6 +1192,7 @@ public sealed class OrderPreservingSubmitterTests
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16) new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default, : default,
buildingDetailEnabled: () => detailEnabled); buildingDetailEnabled: () => detailEnabled);
Dispatcher.AlphaToCoverage = alphaToCoverage;
if (atmospheric) if (atmospheric)
{ {
var source = new BindableAtmosphericSource(receiverBindingAvailable); var source = new BindableAtmosphericSource(receiverBindingAvailable);