fix(render): complete S4 chunk 2 final alpha parity round
Final allowed fix round for S4-c2 on cc8e5677a. This lands every item in the campaign packet section 12 without adding a flush site, shader, distance, overflow recovery draw, or graphical-client run. R2-1 particle row 5: ParticleRenderer now constructs and owns the actual particle-mesh-opaque pipeline using the existing particle_mesh shaders and layout, Blend=None, depth test/write enabled with WorldCompare, dynamic per-batch cull, clockwise front face, and no alpha-to-coverage. The production dispatch selects it for an opaque-classified mesh particle whose clamped material alpha is 1.0. Nonopaque mesh pipelines remain depth-write-off. The production route keeps cached reserve/immediate delegates and the warmed append/immediate paths allocate 0 B. R2-2 EnvCell exact per-subset routing: ObjectMeshManager carries Content's TextureBatchData.RetailSurfaceMask onto ObjectRenderBatch at the real upload boundary. EnvCellRenderer scans the active prepared cell snapshot and feeds each real transparent batch's exact mask through RetailAlphaMeshRouter. Pure 0x08 Base1ClipMap reaches CLIP, 0x02 alpha-family reaches ALPHA, and table Immediate subsets draw at the cell turn. Separate fixed-route draw sources coalesce to at most one token per (cell,list) and filtered replay draws only that list's subsets; a mixed cell contributes to both lists without duplicate replay. Detail-on routes eligible subsets immediately with the detail pass. The warmed dispatch/source allocation pins measure 0 B; the production scan/filter is covered behaviorally and uses only retained scratch/enumerators (static allocation audit). R2-3 capacity cleanup: RetailAlphaQueue registers a source before the 3,000 entry capacity return. A source whose first append is rejected is therefore reset by flush, EndFrame, or abort, but its rejected payload is never prepared or drawn. R2-4 production proof and prose: both actual Wb submit sites are exercised; the particle tests call the production dispatcher and inspect the constructed owner's production pipeline/selector; EnvCell tests upload real Content batch masks through ObjectMeshManager and drain real filtered MDI calls. The A1 positive proof executes WorldSceneRenderer's real outdoor frame owner through RetailPViewRenderer.DrawInside and RetailAlphaQueue.EndFrame and observes [DrawBuilding x N, RenderNormalMode] with no LandscapeFlush. The packet and register now state the varying retail first-for-list truth and the exact per-subset EnvCell/AP-238, visible AP-239 compositing, and AP-240 feeder scope. Physical active register counts remain AP=159 and AD=92. Final clean-state gates (actual output): - Release solution build: Build succeeded; 0 Warning(s); 0 Error(s). - Hermetic solution filter: every project green, 16,728 passed / 0 failed / 0 skipped total; AcDream.App.Tests 6,875/6,875. - InstalledDat: 255 passed / 10 failed / 1 skipped / 266 total, exactly the allowed identities: TowerAscent_StaircaseStaysConeVisible_EveryStep; MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets (#383); EveryAuthoredInvisibleWidget_StartsHiddenAcrossAllLayouts (#383); Oh_doorway_still_first_frame_diff (#458); and the six AlphaFlushCounts_{CathedralArrival,CathedralLeak,CathedralStairArch, FoundryDeep,HoltburgDoorwayStill,TerraceEdge}_MatchesRetailFrame2. All six AlphaFlushSites_* pass in the same lane. - VulkanShaderDescriptorContractTests + VulkanShaderManifestTests + RenderPackSpirvValidatorTests: 32 passed / 0 failed / 0 skipped. - Corrected queue/router/walk/driver/particle/Wb/EnvCell/PView production filter: 211 passed / 0 failed / 0 skipped. - Explicit warmed production allocation pins: 2 passed; both measure 0 B. - Register: physical AP-238/AP-239/AP-240/AD-120/~~AP-34~~ rows each count exactly 1; active physical rows AP=159 and AD=92. - git diff --check: PASS. Production mutation checks (each applied, run to the named first failure, and exactly reversed before the final gates): 1. Restoring the particle row-5 throw fails OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState first with InvalidOperationException: mutation: row 5 unreachable. 2. Deleting row 5's immediate callback fails that same production-dispatch test's first collection assertion: expected [(Mesh, 11, True)], actual []. 3. Constructing the actual owner pipeline with depthWrite:false fails ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription first at Assert.True(description.Depth.Write): expected true, actual false. 4. Mapping the production selector back to _meshAlphaPipeline fails that same test first at Assert.Same: expected particle-mesh-opaque, actual particle-mesh-alpha. 5. Dropping RetailSurfaceMask at the real ObjectMeshManager upload boundary fails the mixed-cell production scan first: expected Clip | Alpha, actual Immediate. 6. Hardcoding the uploaded EnvCell scan to MaskAlphaFamily fails the mixed-cell production scan first: expected Clip | Alpha, actual Alpha; the pure-mask pin also reports expected Clip, actual Alpha. 7. Inverting the production detail predicate fails the detail-on production pin first: expected Immediate, actual Clip. 8. Removing both EnvCell replay filters fails the mixed production drain's real MDI assertions: each call expected DrawCount 1, actual DrawCount 2. 9. Moving RegisterSource below the full-capacity return fails every RejectedFirstUseSource_IsCleanedWithoutPrepareOrDraw row (flush, EndFrame, abort) at the first ResetCount assertion: expected 1, actual 0. 10. Hardcoding detailSurfaceActive=false at the real Wb dispatch fails both production submit-site tests at their first queue-count assertion: expected 0, actual 1. 11. Restoring RetailPViewRenderer's removed outdoor LandscapeFlush call fails OutdoorProductionPView_DrainsBuildingThenRenderNormalModeWithoutLandscapeFlush first at the real drain sequence: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding, LandscapeFlush, RenderNormalMode]. 12. Deleting WorldSceneRenderer's final EndFrame owner drain fails that same full-path A1 test first: expected [DrawBuilding, RenderNormalMode], actual [DrawBuilding]. Not done/deferred: none. No retail conflict or infeasible contract item was found. No graphical client was launched. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
9ccb61a8ec
commit
0aa166aa09
14 changed files with 1779 additions and 224 deletions
|
|
@ -330,9 +330,9 @@ research and is no longer active.
|
||||||
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
|
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
|
||||||
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
||||||
| ~~AP-34~~ | **RETIRED 2026-09-04 (S4-c2 fix round 1) — residuals AP-238 / AP-239 / AP-240.** Historical: a single acdream-only distance-sorted alpha queue stood in for retail's two independent per-`DrawMesh`-invocation FIFO lists (CLIP/ALPHA, capacity 3000 each) drained at retail's four normal-world `FlushAlphaList` sites (`DrawBuilding`/`DrawBlock`/`PView::DrawCells`/`RenderNormalMode`). S4-c2 (`048d5b12f`) replaced it with `RetailAlphaQueue`'s exact two-list FIFO port of `D3DPolyRender::AddMeshToAlphaList`'s append-only/capacity-drop behavior, routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s five-row branch table, under retail's exact no-op threshold rule — no acdream-only sort remains anywhere on the alpha path. | retired | — | — | `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `D3DPolyRender::DrawMesh` 0x0059D4A0 |
|
| ~~AP-34~~ | **RETIRED 2026-09-04 (S4-c2 fix round 1) — residuals AP-238 / AP-239 / AP-240.** Historical: a single acdream-only distance-sorted alpha queue stood in for retail's two independent per-`DrawMesh`-invocation FIFO lists (CLIP/ALPHA, capacity 3000 each) drained at retail's four normal-world `FlushAlphaList` sites (`DrawBuilding`/`DrawBlock`/`PView::DrawCells`/`RenderNormalMode`). S4-c2 (`048d5b12f`) replaced it with `RetailAlphaQueue`'s exact two-list FIFO port of `D3DPolyRender::AddMeshToAlphaList`'s append-only/capacity-drop behavior, routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s five-row branch table, under retail's exact no-op threshold rule — no acdream-only sort remains anywhere on the alpha path. | retired | — | — | `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `D3DPolyRender::DrawMesh` 0x0059D4A0 |
|
||||||
| AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER at the S4-c2 fix round 1 (2026-09-04) — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main` by an unrelated #132 fix; ids are never reused, per A2).** One of TWO residuals AP-34's closure left open (the other is AP-240's CLIP-list-for-ordinary-content gap) — NOT "the ONE residual" as originally claimed. `RetailAlphaQueue` is retail's own two independent FIFO lists (CLIP/ALPHA), routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s branch table and drained at retail's four normal-world `FlushAlphaList` sites. Fix round 1 (M6) then wired EnvCell's transparent shell into that SAME table: `RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell` routes through `RetailAlphaMeshRouter.Route` with the shell's mask (always 0x02, alpha-family) — detail ON takes Row 1 (immediate, unchanged from before); detail OFF now takes Row 3 (Append) instead of drawing immediately, appending ONE token per CELL via the new `EnvCellAlphaDrawSource`, replayed through `EnvCellRenderer.RenderTransparentOrdered` at the queue's next flush. The residual is now PURELY the token GRANULARITY, not "never through the shared queue" (that claim is retired) — no "substantial separate architecture change" was needed; `RenderTransparentOrdered` already replayed a cell's transparent batch on demand. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (`TransparentDetailEnabled`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | EnvCell's per-cell transparent batch is homogeneously mask 0x02 (EnvCell shells never carry a ClipMap bit in this path), so one atomic per-cell FIFO token is consistent with how WbDrawDispatcher/ParticleRenderer already submit at instance granularity, not per-D3D-surface-subset; a particle or ordinary GfxObj instance appended between two cell tokens keeps its own position (the queue's adjacent-only batching invariant) — only interleaving BETWEEN two subsets of the SAME cell's own batch is unavailable, and nothing needs that today. | A single EnvCell's transparent shell (glass, water, grates) with multiple distinct subsets composites those subsets in strict draw-call order RELATIVE TO EACH OTHER (no visual change from before this round); the coarser per-cell token only matters if a future feature needs to interleave other content between two subsets of that SAME cell's own batch. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0 |
|
| AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER at fix rounds 1 and 2 — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main`; ids are never reused).** `RetailAlphaQueue` is retail's own independent CLIP/ALPHA FIFO pair. Fix round 2 now retains each real EnvCell `ObjectRenderBatch`'s `TextureBatchData.RetailSurfaceMask` and routes every transparent subset independently through `RetailAlphaMeshRouter`: canonical `0x08` Base1ClipMap subsets enter CLIP, `0x02` alpha-family subsets enter ALPHA, table-Immediate subsets draw at the cell turn, and detail-active eligible subsets remain immediate with detail. A mixed cell can therefore contribute to both lists. The remaining residual is only token granularity: acdream coalesces to at most ONE token per `(cell,list)` and replays a list-filtered subset view, where retail appends one entry per surface subset. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` / `.Rhi.cs` (`GetTransparentRoutes`, `RouteTransparentBatch`, filtered `RenderTransparentOrdered`); `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` (`ObjectRenderBatch.RetailSurfaceMask`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs` | Per-list replay preserves the exact subset class and the queue's adjacent-only batching preserves interleaving around other sources; the approximation exists only within one cell/list when several same-list subsets are represented by one token. | At a genuine depth-overlap, collapsing multiple same-list subsets to one `(cell,list)` token can visibly change compositing if other translucent content should fall between those subsets in retail's FIFO. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; canonical installed-DAT case `F4180104` surface `08000BFF` mask `0x08` |
|
||||||
| AP-239 | **Filed 2026-09-04 (S4-c2); renumbered from the colliding id "AP-237" at the S4-c2 fix round 1 (2026-09-04, A2) — ids are never reused; the dangling "AP-236b" citation at `RetailAlphaMeshRouter.cs` (which never matched either filed id) is corrected to this one in the same fix.** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` queue-routing mask (spec: alpha-family bits always win; else ClipMap; else Translucent) from acdream's already-collapsed `TranslucencyKind`, which was built for a DIFFERENT priority chain (`TranslucencyKindExtensions.FromSurfaceType`'s blend-STATE selection: a "Translucent override" step checks Translucent+ClipMap-without-alpha-family FIRST and calls it `AlphaBlend`). A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04) bits but no Alpha/InvAlpha/Additive bit therefore reconstructs to mask 0x02 (routes to ALPHA) here, while retail's real `ConstructMesh` mask is 0x08 (routes to CLIP) — `GroupKey` does not retain the raw ClipMap bit needed to distinguish the two `AlphaBlend` origins, and threading it through `GfxObjSubMesh`/`GroupKey`/every mesh-classification call site was judged out of this bounded chunk's scope. **S4-c2 fix round 1 (M7, 2026-09-04): the original cited example was DISPROVEN and replaced.** Cloud GfxObj `0x01004C35`'s surface `0x08000023` is `Type=0x10114` (ADDITIVE\|ALPHA\|TRANSLUCENT\|BASE1_CLIPMAP) — its alpha-family bits win in BOTH retail's real priority and this reconstruction, so it reproduces IDENTICALLY (mask 0x02 either way) and never demonstrated the gap. A 2026-09-04 scan of every Portal-dat `Surface` (6,152 scanned) found 27 REAL instances of Translucent+Base1ClipMap with no alpha-family bit (`Type=0x14`); cross-referencing the Portal-dat `GfxObj` table found surface `0x08000015` referenced by GfxObj `0x010001EC` — a real, confirmed instance, not the disproven cloud one. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`'s own doc comment); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`'s Translucent-override step) | Compositing-ORDER only (which FIFO list a subset enters), never a blend-mode or visual difference — the affected surface still renders alpha-blended exactly as retail's `SetSurface` configures it; only its position in the shared drain order can differ at a narrow overlap with other CLIP-list content. 27 of 6,152 scanned surfaces (2026-09-04) carry this shape — narrow, but not as narrow as "only the cloud." | A translucent+clipmap surface (e.g. GfxObj `0x010001EC`) interleaves with ordinary ALPHA-list content instead of CLIP-list content at a rare compositing overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 (the Translucent-override blend selection acdream's `TranslucencyKind` already matches) |
|
| AP-239 | **Filed 2026-09-04 (S4-c2); renumbered from the colliding id "AP-237" at fix round 1.** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` routing mask from acdream's already-collapsed `TranslucencyKind`. A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04), but no Alpha/InvAlpha/Additive bit, collapses to `AlphaBlend` and reconstructs as mask `0x02` (ALPHA), while retail retains mask `0x08` (CLIP). `GroupKey` does not retain the raw ClipMap bit required to distinguish the two origins. The original cloud example was disproven; a 2026-09-04 scan found 27 real `Type=0x14` surfaces among 6,152, including surface `0x08000015` referenced by GfxObj `0x010001EC`. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`) | The affected surface keeps the same blend state, but choosing the wrong FIFO changes compositing order; at a genuine overlap that order difference can be visibly different. | A translucent+clipmap surface (e.g. GfxObj `0x010001EC`) interleaves with ALPHA-list content instead of CLIP-list content and can composite differently at a depth overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 |
|
||||||
| AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2).** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` (`WbDrawDispatcher.cs`) keeps every clip-mapped subset out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue` entirely; retail's `ConstructMesh` gives clip-mapped surfaces mask 0x08 → `DrawMesh` row 3 → the CLIP list, and the S4-c2 capture evidence (six `*.alphadepth.log` files) shows 4,183 of 10,556 `AM` lines on CLIP (e.g. `(DrawBuilding,0,158,452)`: 158 real CLIP entries acdream's ordinary-content path reports as 0). Clip-mapped GfxObj/scenery/building subsets instead draw IMMEDIATELY on the `OpaqueAlphaToCoverage` pipeline (the pre-S4/VisualMaster-era translucency decision — not a pipeline change this round) and never enter retail's CLIP FIFO at all; the only CLIP-list feeders left after S4-c2 are clip-mapped MESH PARTICLES (M1's fix: `ParticleRenderer` applies no `IsOpaque`-style pre-filter, so a mesh particle classified `TranslucencyKind.ClipMap` genuinely reaches mask 0x08 and can append to CLIP). | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskClipMap` — the only reachable feeder is `ParticleRenderer`) | Depth-writing alpha-to-coverage cutouts composite correctly regardless of draw order for the overwhelming majority of clip-mapped content (doors, windows, foliage) — the divergence is compositing ORDER at a narrow overlap with genuine CLIP-list content (clip-mapped particles), not a missing/wrong cutout. Changing this is a VisualMaster-era pipeline decision, out of this bounded chunk's scope. | Clip-mapped GfxObj/scenery/building content composites in draw-call order relative to other opaque-pipeline content instead of interleaving through retail's deferred CLIP drain with clip-mapped particles/EnvCell content — edges differ only at a narrow overlap, never a missing cutout. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0 (row 3); `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 |
|
| AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2); narrowed at fix round 2.** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` keeps clip-mapped GfxObj/scenery/building subsets out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue`; retail gives those subsets mask `0x08` and appends them to CLIP. They instead draw immediately on acdream's `OpaqueAlphaToCoverage` pipeline. Fix round 2 proves CLIP has two other production feeder families: clip-mapped mesh particles and exact-mask EnvCell subsets (canonical `F4180104` / surface `08000BFF`). The residual is therefore only ordinary GfxObj/scenery/building content, not an empty or particle-only CLIP list. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`RouteTransparentBatch`); `src/AcDream.App/Rendering/ParticleRenderer.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | The cutout itself remains present, but immediate opaque-path placement differs from retail's deferred FIFO order. Changing it is a VisualMaster-era pipeline decision beyond this bounded chunk. | At an overlap with deferred CLIP content from particles or EnvCells, clip-mapped GfxObj/scenery/building edges can visibly composite in the wrong relative order. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 |
|
||||||
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 |
|
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 |
|
||||||
| AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same `IndoorObjectReceivesTorches(ParentCellId)` predicate (`(id & 0xFFFF) >= 0x0100`): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's `useSunlight` gate — `DrawMeshInternal` (0x0059f398) calls `minimize_object_lighting` only `if (Render::useSunlight == 0)`, and `PView::DrawCells` (0x005a4840) calls `useSunlightSet(1)` (0x005a485a) for the outdoor stage and `useSunlightSet(0)` (0x005a49f3) for the interior-cell stage. **#142 (2026-06-20):** the sun gate is now PER-INSTANCE in the shader (binding=6 `instanceIndoor[]` flag in `mesh_modern.vert`, filled by `AppendCurrentLightSet`) — it was previously a per-FRAME global keyed on the PLAYER cell (`UpdateSunFromSky`). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). **Residual:** the `ebp_2` second seen-outside test in `CellManager::ChangePosition` (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IndoorObjectReceivesTorches`, `ComputeEntityLightSet`, `AppendCurrentLightSet`, `_instIndoorSsbo`/`_indoorData`/`InstanceGroup.IndoorFlags`); `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (binding=6 `instanceIndoor[]` gate on sun loop); per-frame sun `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.UpdateSunFromSky`) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail `ChangePosition`). | The `ebp_2` unaudited test in `ChangePosition` could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | `useSunlight` gate `DrawMeshInternal` 0x0059f398; `useSunlightSet` 0x0054d450; per-stage `PView::DrawCells` 0x005a4840 (`useSunlightSet(1)` 0x005a485a / `useSunlightSet(0)` 0x005a49f3); `minimize_object_lighting` 0x0054d480; `CellManager::ChangePosition` 0x004559B0 (ambient + seen_outside) |
|
| AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same `IndoorObjectReceivesTorches(ParentCellId)` predicate (`(id & 0xFFFF) >= 0x0100`): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's `useSunlight` gate — `DrawMeshInternal` (0x0059f398) calls `minimize_object_lighting` only `if (Render::useSunlight == 0)`, and `PView::DrawCells` (0x005a4840) calls `useSunlightSet(1)` (0x005a485a) for the outdoor stage and `useSunlightSet(0)` (0x005a49f3) for the interior-cell stage. **#142 (2026-06-20):** the sun gate is now PER-INSTANCE in the shader (binding=6 `instanceIndoor[]` flag in `mesh_modern.vert`, filled by `AppendCurrentLightSet`) — it was previously a per-FRAME global keyed on the PLAYER cell (`UpdateSunFromSky`). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). **Residual:** the `ebp_2` second seen-outside test in `CellManager::ChangePosition` (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IndoorObjectReceivesTorches`, `ComputeEntityLightSet`, `AppendCurrentLightSet`, `_instIndoorSsbo`/`_indoorData`/`InstanceGroup.IndoorFlags`); `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (binding=6 `instanceIndoor[]` gate on sun loop); per-frame sun `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.UpdateSunFromSky`) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail `ChangePosition`). | The `ebp_2` unaudited test in `ChangePosition` could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | `useSunlight` gate `DrawMeshInternal` 0x0059f398; `useSunlightSet` 0x0054d450; per-stage `PView::DrawCells` 0x005a4840 (`useSunlightSet(1)` 0x005a485a / `useSunlightSet(0)` 0x005a49f3); `minimize_object_lighting` 0x0054d480; `CellManager::ChangePosition` 0x004559B0 (ambient + seen_outside) |
|
||||||
| AP-35 | Point/spot lights are now PER-VERTEX Gouraud (`pointContribution` ~line 153 of `mesh_modern.vert`) matching retail's `SetStaticLightingVertexColors` bake path. Half-Lambert wrap (`(1/1.5)·(N·D + 0.5·d)`) AND norm distance attenuation (`distsq>1 ? distsq·d : d`) ARE ported (A7 Fix A, `aa94ced`). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: `src/AcDream.Core/Lighting/LightBake.cs`, locked by `tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs`. **Residual (two parts):** (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's `SelectForObject` keeps only the 8 NEAREST reaching point/spot lights per object/cell (`MaxLightsPerObject=8`, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution` ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. `LightInfoLoader.cs:81` folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing `accumulateLights` would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); `SetStaticLightingVertexColors` 0x0059cfe0; static_light_factor 0x00820e24 |
|
| AP-35 | Point/spot lights are now PER-VERTEX Gouraud (`pointContribution` ~line 153 of `mesh_modern.vert`) matching retail's `SetStaticLightingVertexColors` bake path. Half-Lambert wrap (`(1/1.5)·(N·D + 0.5·d)`) AND norm distance attenuation (`distsq>1 ? distsq·d : d`) ARE ported (A7 Fix A, `aa94ced`). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: `src/AcDream.Core/Lighting/LightBake.cs`, locked by `tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs`. **Residual (two parts):** (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's `SelectForObject` keeps only the 8 NEAREST reaching point/spot lights per object/cell (`MaxLightsPerObject=8`, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution` ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. `LightInfoLoader.cs:81` folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing `accumulateLights` would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); `SetStaticLightingVertexColors` 0x0059cfe0; static_light_factor 0x00820e24 |
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ namespace AcDream.App.Rendering;
|
||||||
/// natively and at no cost.</para>
|
/// natively and at no cost.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>What differs from the GL arm, and why.</b> The imperative
|
/// <para><b>What differs from the GL arm, and why.</b> The imperative
|
||||||
/// <c>glBlendFunc</c> switch becomes five PIPELINES (two billboard blends, three
|
/// <c>glBlendFunc</c> switch becomes six PIPELINES (two billboard blends,
|
||||||
/// mesh blends) because core Vulkan 1.3 does not make blend dynamic; the
|
/// opaque plus three mesh blends) because core Vulkan 1.3 does not make blend dynamic; the
|
||||||
/// per-flight VAO/VBO pool disappears because every ring allocation inside a
|
/// per-flight VAO/VBO pool disappears because every ring allocation inside a
|
||||||
/// frame is already distinct memory that lives until the frame retires; the
|
/// frame is already distinct memory that lives until the frame retires; the
|
||||||
/// binding-9 texture table is not bound at all, because the device owns the
|
/// binding-9 texture table is not bound at all, because the device owns the
|
||||||
|
|
@ -49,6 +49,7 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
|
|
||||||
private IGpuPipeline? _billboardAlphaPipeline;
|
private IGpuPipeline? _billboardAlphaPipeline;
|
||||||
private IGpuPipeline? _billboardAdditivePipeline;
|
private IGpuPipeline? _billboardAdditivePipeline;
|
||||||
|
private IGpuPipeline? _meshOpaquePipeline;
|
||||||
private IGpuPipeline? _meshAlphaPipeline;
|
private IGpuPipeline? _meshAlphaPipeline;
|
||||||
private IGpuPipeline? _meshAdditivePipeline;
|
private IGpuPipeline? _meshAdditivePipeline;
|
||||||
private IGpuPipeline? _meshInversePipeline;
|
private IGpuPipeline? _meshInversePipeline;
|
||||||
|
|
@ -154,6 +155,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||||
_alphaQueue = alphaQueue;
|
_alphaQueue = alphaQueue;
|
||||||
_alphaSource = new AlphaDrawSource(this);
|
_alphaSource = new AlphaDrawSource(this);
|
||||||
|
_reserveDeferredParticleDraw = ReserveDispatchDeferredParticle;
|
||||||
|
_drawImmediateParticle = DrawImmediateParticleSubmissionRhi;
|
||||||
long scratchBudget = alphaScratchBudgetBytes
|
long scratchBudget = alphaScratchBudgetBytes
|
||||||
?? AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
?? AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||||
AcDream.App.Rendering.Residency.ResidencyBudgetOptions.Default.AlphaScratchBytes)
|
AcDream.App.Rendering.Residency.ResidencyBudgetOptions.Default.AlphaScratchBytes)
|
||||||
|
|
@ -219,12 +222,14 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
if (_meshAdapter?.MeshManager?.GlobalBuffer is null)
|
if (_meshAdapter?.MeshManager?.GlobalBuffer is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_meshOpaquePipeline = CreateMeshParticlePipeline(
|
||||||
|
device, "particle-mesh-opaque", GpuBlendMode.None, depthWrite: true, sampleCount);
|
||||||
_meshAlphaPipeline = CreateMeshParticlePipeline(
|
_meshAlphaPipeline = CreateMeshParticlePipeline(
|
||||||
device, "particle-mesh-alpha", GpuBlendMode.StraightAlpha, sampleCount);
|
device, "particle-mesh-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, sampleCount);
|
||||||
_meshAdditivePipeline = CreateMeshParticlePipeline(
|
_meshAdditivePipeline = CreateMeshParticlePipeline(
|
||||||
device, "particle-mesh-additive", GpuBlendMode.Additive, sampleCount);
|
device, "particle-mesh-additive", GpuBlendMode.Additive, depthWrite: false, sampleCount);
|
||||||
_meshInversePipeline = CreateMeshParticlePipeline(
|
_meshInversePipeline = CreateMeshParticlePipeline(
|
||||||
device, "particle-mesh-inverse", GpuBlendMode.InverseAlpha, sampleCount);
|
device, "particle-mesh-inverse", GpuBlendMode.InverseAlpha, depthWrite: false, sampleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -273,6 +278,7 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
IGpuDevice device,
|
IGpuDevice device,
|
||||||
string name,
|
string name,
|
||||||
GpuBlendMode blend,
|
GpuBlendMode blend,
|
||||||
|
bool depthWrite,
|
||||||
int sampleCount) =>
|
int sampleCount) =>
|
||||||
device.CreatePipeline(new GpuPipelineDescription
|
device.CreatePipeline(new GpuPipelineDescription
|
||||||
{
|
{
|
||||||
|
|
@ -281,7 +287,7 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
VertexLayout = MeshVertexLayout,
|
VertexLayout = MeshVertexLayout,
|
||||||
Topology = GpuPrimitiveTopology.TriangleList,
|
Topology = GpuPrimitiveTopology.TriangleList,
|
||||||
Blend = blend,
|
Blend = blend,
|
||||||
Depth = new GpuDepthState(Test: true, Write: false, WorldDepthContract.WorldCompare),
|
Depth = new GpuDepthState(Test: true, Write: depthWrite, WorldDepthContract.WorldCompare),
|
||||||
Cull = GpuCullMode.None,
|
Cull = GpuCullMode.None,
|
||||||
FrontFace = GpuFrontFace.Clockwise,
|
FrontFace = GpuFrontFace.Clockwise,
|
||||||
AlphaToCoverage = false,
|
AlphaToCoverage = false,
|
||||||
|
|
@ -366,7 +372,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
instances.Buffer,
|
instances.Buffer,
|
||||||
instances.OffsetBytes,
|
instances.OffsetBytes,
|
||||||
(uint)_meshRunScratch.Count,
|
(uint)_meshRunScratch.Count,
|
||||||
firstInstance: 0);
|
firstInstance: 0,
|
||||||
|
opaqueDepthState: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,7 +390,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
private void DrawImmediateParticleSubmissionRhi(
|
private void DrawImmediateParticleSubmissionRhi(
|
||||||
Matrix4x4 viewProjection,
|
Matrix4x4 viewProjection,
|
||||||
ParticleSubmissionKind kind,
|
ParticleSubmissionKind kind,
|
||||||
int drawIndex)
|
int drawIndex,
|
||||||
|
bool opaqueDepthState)
|
||||||
{
|
{
|
||||||
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
IGpuPassEncoder encoder = _scope!.RequireEncoder();
|
||||||
IGpuFrame frame = RequireRhiFrame();
|
IGpuFrame frame = RequireRhiFrame();
|
||||||
|
|
@ -417,7 +425,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
instances.Buffer,
|
instances.Buffer,
|
||||||
instances.OffsetBytes,
|
instances.OffsetBytes,
|
||||||
instanceCount: 1,
|
instanceCount: 1,
|
||||||
firstInstance: 0);
|
firstInstance: 0,
|
||||||
|
opaqueDepthState: opaqueDepthState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawInstancesRhi(
|
private void DrawInstancesRhi(
|
||||||
|
|
@ -502,12 +511,13 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
IGpuBuffer instanceBuffer,
|
IGpuBuffer instanceBuffer,
|
||||||
uint instanceOffsetBytes,
|
uint instanceOffsetBytes,
|
||||||
uint instanceCount,
|
uint instanceCount,
|
||||||
uint firstInstance)
|
uint firstInstance,
|
||||||
|
bool opaqueDepthState = false)
|
||||||
{
|
{
|
||||||
if (instanceCount == 0)
|
if (instanceCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
encoder.BindPipeline(PipelineForMeshBlend(ResolveMeshBlend(batch)));
|
encoder.BindPipeline(PipelineForMeshBlend(ResolveMeshBlend(batch), opaqueDepthState));
|
||||||
encoder.SetPushConstants(new GpuPushConstants
|
encoder.SetPushConstants(new GpuPushConstants
|
||||||
{
|
{
|
||||||
ViewProjection = viewProjection,
|
ViewProjection = viewProjection,
|
||||||
|
|
@ -550,12 +560,41 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
firstInstance);
|
firstInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IGpuPipeline PipelineForMeshBlend(TranslucencyKind blend) => blend switch
|
internal readonly record struct MeshParticlePipelineState(
|
||||||
|
GpuBlendMode Blend,
|
||||||
|
GpuDepthState Depth);
|
||||||
|
|
||||||
|
internal static MeshParticlePipelineState ResolveMeshParticlePipelineState(
|
||||||
|
TranslucencyKind blend,
|
||||||
|
bool opaqueDepthState) =>
|
||||||
|
opaqueDepthState
|
||||||
|
? new MeshParticlePipelineState(
|
||||||
|
GpuBlendMode.None,
|
||||||
|
new GpuDepthState(Test: true, Write: true, WorldDepthContract.WorldCompare))
|
||||||
|
: new MeshParticlePipelineState(
|
||||||
|
blend switch
|
||||||
|
{
|
||||||
|
TranslucencyKind.Additive => GpuBlendMode.Additive,
|
||||||
|
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
|
||||||
|
_ => GpuBlendMode.StraightAlpha,
|
||||||
|
},
|
||||||
|
new GpuDepthState(Test: true, Write: false, WorldDepthContract.WorldCompare));
|
||||||
|
|
||||||
|
private IGpuPipeline PipelineForMeshBlend(
|
||||||
|
TranslucencyKind blend,
|
||||||
|
bool opaqueDepthState)
|
||||||
{
|
{
|
||||||
TranslucencyKind.Additive => _meshAdditivePipeline!,
|
MeshParticlePipelineState state = ResolveMeshParticlePipelineState(
|
||||||
TranslucencyKind.InvAlpha => _meshInversePipeline!,
|
blend,
|
||||||
_ => _meshAlphaPipeline!,
|
opaqueDepthState);
|
||||||
};
|
return state.Blend switch
|
||||||
|
{
|
||||||
|
GpuBlendMode.None => _meshOpaquePipeline!,
|
||||||
|
GpuBlendMode.Additive => _meshAdditivePipeline!,
|
||||||
|
GpuBlendMode.InverseAlpha => _meshInversePipeline!,
|
||||||
|
_ => _meshAlphaPipeline!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI form of <see cref="ApplyMeshCullMode"/>. <c>FrontFace</c> is
|
/// The RHI form of <see cref="ApplyMeshCullMode"/>. <c>FrontFace</c> is
|
||||||
|
|
@ -708,7 +747,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
meshInstances,
|
meshInstances,
|
||||||
_preparedMeshInstances.OffsetBytes,
|
_preparedMeshInstances.OffsetBytes,
|
||||||
(uint)(i - meshRunStart),
|
(uint)(i - meshRunStart),
|
||||||
meshBaseInstance);
|
meshBaseInstance,
|
||||||
|
opaqueDepthState: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -756,6 +796,8 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
_billboardAlphaPipeline = null;
|
_billboardAlphaPipeline = null;
|
||||||
Attempt(() => _billboardAdditivePipeline?.Dispose());
|
Attempt(() => _billboardAdditivePipeline?.Dispose());
|
||||||
_billboardAdditivePipeline = null;
|
_billboardAdditivePipeline = null;
|
||||||
|
Attempt(() => _meshOpaquePipeline?.Dispose());
|
||||||
|
_meshOpaquePipeline = null;
|
||||||
Attempt(() => _meshAlphaPipeline?.Dispose());
|
Attempt(() => _meshAlphaPipeline?.Dispose());
|
||||||
_meshAlphaPipeline = null;
|
_meshAlphaPipeline = null;
|
||||||
Attempt(() => _meshAdditivePipeline?.Dispose());
|
Attempt(() => _meshAdditivePipeline?.Dispose());
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
private readonly ParticleSystem _particles;
|
private readonly ParticleSystem _particles;
|
||||||
private readonly RetailAlphaQueue? _alphaQueue;
|
private readonly RetailAlphaQueue? _alphaQueue;
|
||||||
private readonly AlphaDrawSource _alphaSource;
|
private readonly AlphaDrawSource _alphaSource;
|
||||||
|
private readonly ReserveDeferredParticleDraw _reserveDeferredParticleDraw;
|
||||||
|
private readonly DrawImmediateParticle _drawImmediateParticle;
|
||||||
|
private DeferredParticleDraw _dispatchDeferredParticle;
|
||||||
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
||||||
private readonly Dictionary<int, ParticleGfxInfo> _particleGfxInfoByEmitter = new();
|
private readonly Dictionary<int, ParticleGfxInfo> _particleGfxInfoByEmitter = new();
|
||||||
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
||||||
|
|
@ -185,6 +188,14 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
=> owner.ResetDeferredAlpha();
|
=> owner.ResetDeferredAlpha();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal delegate int ReserveDeferredParticleDraw();
|
||||||
|
|
||||||
|
internal delegate void DrawImmediateParticle(
|
||||||
|
Matrix4x4 viewProjection,
|
||||||
|
ParticleSubmissionKind kind,
|
||||||
|
int drawIndex,
|
||||||
|
bool opaqueDepthState);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts one render frame. Wb point-of-use recovery is limited to one
|
/// Starts one render frame. Wb point-of-use recovery is limited to one
|
||||||
/// request per missing GfxObj even though portal slicing may invoke Draw
|
/// request per missing GfxObj even though portal slicing may invoke Draw
|
||||||
|
|
@ -332,8 +343,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
/// mask 0x00 is a real, reachable input here.
|
/// mask 0x00 is a real, reachable input here.
|
||||||
///
|
///
|
||||||
/// <para>M1(b): retail's has_alpha (<c>CMaterial::CheckAlphaValues</c>
|
/// <para>M1(b): retail's has_alpha (<c>CMaterial::CheckAlphaValues</c>
|
||||||
/// @0x005396a0 — Ghidra-verified 2026-09-04: has_alpha=0 only when
|
/// @0x005396a0 — paired-binary verified 2026-09-04: any channel below
|
||||||
/// Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else has_alpha=1)
|
/// 1.0 sets has_alpha; values at or above 1.0 leave it clear)
|
||||||
/// is driven, for a particle, by <c>CMaterial::SetTranslucencySimple</c>
|
/// is driven, for a particle, by <c>CMaterial::SetTranslucencySimple</c>
|
||||||
/// @0x005396f0 — the only production writer reached from a particle's
|
/// @0x005396f0 — the only production writer reached from a particle's
|
||||||
/// live translucency (<c>CPhysicsPart::SetTranslucency</c> <-
|
/// live translucency (<c>CPhysicsPart::SetTranslucency</c> <-
|
||||||
|
|
@ -344,8 +355,10 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
/// CURRENT alpha isn't 1.0 (fully opaque)". acdream already carries that
|
/// CURRENT alpha isn't 1.0 (fully opaque)". acdream already carries that
|
||||||
/// exact interpolated value — <c>Lerp(p.StartAlpha, p.EndAlpha, tLife)</c>,
|
/// exact interpolated value — <c>Lerp(p.StartAlpha, p.EndAlpha, tLife)</c>,
|
||||||
/// baked into the top byte of <paramref name="meshColorArgb"/> by
|
/// baked into the top byte of <paramref name="meshColorArgb"/> by
|
||||||
/// <c>Color32</c> (ParticleSystem.cs:1039-1041,1453) — so no new state is
|
/// <c>Color32</c> (ParticleSystem.cs:1039-1041,1453). Production clamps
|
||||||
/// needed. Billboards never reach row 4 (their mask 0x02 always
|
/// particle alpha to [0,1], so top-byte != 0xff is behaviorally identical
|
||||||
|
/// to retail's below-1 predicate within this explicitly bounded domain;
|
||||||
|
/// no new state is needed. Billboards never reach row 4 (their mask 0x02 always
|
||||||
/// satisfies row 3), so <paramref name="meshColorArgb"/> only matters for
|
/// satisfies row 3), so <paramref name="meshColorArgb"/> only matters for
|
||||||
/// mesh particles.</para>
|
/// mesh particles.</para>
|
||||||
///
|
///
|
||||||
|
|
@ -353,8 +366,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
/// screen particles use the independent DrawOrdered path entirely, never
|
/// screen particles use the independent DrawOrdered path entirely, never
|
||||||
/// this method) and never installs a detail surface; MultiPassAlpha
|
/// this method) and never installs a detail surface; MultiPassAlpha
|
||||||
/// stays false (Must Not: no environment override) — Row 2 is therefore
|
/// stays false (Must Not: no environment override) — Row 2 is therefore
|
||||||
/// provably unreachable here. Rows 1/5 (Immediate) ARE reachable for an
|
/// provably unreachable here. Row 5 (Immediate) is reachable for an
|
||||||
/// Opaque-classified mesh-particle batch with no material alpha.</para>
|
/// Opaque-classified mesh-particle batch with no material alpha; Row 1 is
|
||||||
|
/// not, because this site never installs a detail surface.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static RetailAlphaMeshDecision RouteParticleSubmission(
|
internal static RetailAlphaMeshDecision RouteParticleSubmission(
|
||||||
ParticleSubmissionKind kind, TranslucencyKind meshTranslucency, uint meshColorArgb)
|
ParticleSubmissionKind kind, TranslucencyKind meshTranslucency, uint meshColorArgb)
|
||||||
|
|
@ -394,50 +408,78 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
||||||
_meshDrawListScratch[submission.DrawIndex],
|
_meshDrawListScratch[submission.DrawIndex],
|
||||||
viewProjection);
|
viewProjection);
|
||||||
|
|
||||||
RetailAlphaMeshDecision decision = submission.Kind == ParticleSubmissionKind.Billboard
|
_dispatchDeferredParticle = deferred;
|
||||||
? RouteParticleSubmission(submission.Kind, default, default)
|
TranslucencyKind translucency = submission.Kind == ParticleSubmissionKind.Mesh
|
||||||
: RouteParticleSubmission(
|
? _meshDrawListScratch[submission.DrawIndex].Batch.Translucency
|
||||||
submission.Kind,
|
: default;
|
||||||
_meshDrawListScratch[submission.DrawIndex].Batch.Translucency,
|
uint colorArgb = submission.Kind == ParticleSubmissionKind.Mesh
|
||||||
_meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb);
|
? _meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb
|
||||||
|
: default;
|
||||||
|
DeferToRetailAlphaQueue(
|
||||||
|
submission.Kind,
|
||||||
|
translucency,
|
||||||
|
colorArgb,
|
||||||
|
queue,
|
||||||
|
_alphaSource,
|
||||||
|
_reserveDeferredParticleDraw,
|
||||||
|
_drawImmediateParticle,
|
||||||
|
viewProjection,
|
||||||
|
submission.DrawIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (decision.Action)
|
/// <summary>
|
||||||
|
/// The production one-submission dispatch used by the scene-particle
|
||||||
|
/// loop. It owns both the queue append and the row-5 immediate callback,
|
||||||
|
/// so tests can discriminate the former throw/deleted-call mutations
|
||||||
|
/// without replacing the router with a pure surrogate.
|
||||||
|
/// </summary>
|
||||||
|
internal static RetailAlphaMeshDecision DeferToRetailAlphaQueue(
|
||||||
|
ParticleSubmissionKind kind,
|
||||||
|
TranslucencyKind meshTranslucency,
|
||||||
|
uint meshColorArgb,
|
||||||
|
RetailAlphaQueue queue,
|
||||||
|
IRetailAlphaDrawSource source,
|
||||||
|
ReserveDeferredParticleDraw reserveDeferred,
|
||||||
|
DrawImmediateParticle drawImmediate,
|
||||||
|
Matrix4x4 viewProjection,
|
||||||
|
int drawIndex)
|
||||||
|
{
|
||||||
|
RetailAlphaMeshDecision decision = RouteParticleSubmission(
|
||||||
|
kind,
|
||||||
|
meshTranslucency,
|
||||||
|
meshColorArgb);
|
||||||
|
switch (decision.Action)
|
||||||
|
{
|
||||||
|
case RetailAlphaMeshAction.Append:
|
||||||
{
|
{
|
||||||
case RetailAlphaMeshAction.Append:
|
int token = reserveDeferred();
|
||||||
{
|
queue.TryAppend(decision.List, source, token, decision.OverrideClipmap);
|
||||||
int token = _deferredAlpha.Count;
|
break;
|
||||||
_deferredAlpha.Add(deferred);
|
}
|
||||||
// Capacity overflow (spec §5): dropped, no recovery.
|
case RetailAlphaMeshAction.Immediate:
|
||||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
// At this hardcoded no-detail/no-multipass site Immediate is
|
||||||
break;
|
// DrawMesh row 5. Retail SetSurface uses ONE/ZERO, disables
|
||||||
}
|
// blending, and depth-writes; select that pipeline rather
|
||||||
|
// than the ordinary alpha mesh pipeline.
|
||||||
case RetailAlphaMeshAction.Immediate:
|
drawImmediate(viewProjection, kind, drawIndex, opaqueDepthState: true);
|
||||||
// M1(c): rows 1/5 — draw NOW, through the same
|
break;
|
||||||
// per-instance draw code DrawOrderedRhi uses for one
|
case RetailAlphaMeshAction.AppendClipAndImmediate:
|
||||||
// submission (no new renderer). A8: no sort — retail's
|
{
|
||||||
// immediate RenderMeshSubset draws in mesh/submission
|
int token = reserveDeferred();
|
||||||
// order, so this draws exactly this submission, right
|
queue.TryAppend(decision.List, source, token, decision.OverrideClipmap);
|
||||||
// where DrawMesh was called.
|
drawImmediate(viewProjection, kind, drawIndex, opaqueDepthState: false);
|
||||||
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case RetailAlphaMeshAction.AppendClipAndImmediate:
|
|
||||||
{
|
|
||||||
// Row 2: provably unreachable at this call site
|
|
||||||
// (MultiPassAlpha stays false), but a router row is a
|
|
||||||
// data-driven outcome, never an invariant to crash on
|
|
||||||
// (feedback_retail_dispatch_is_data_driven) — handled in
|
|
||||||
// full, exactly as spec §4 row 2 describes: append to
|
|
||||||
// CLIP AND ALSO draw immediately.
|
|
||||||
int token = _deferredAlpha.Count;
|
|
||||||
_deferredAlpha.Add(deferred);
|
|
||||||
queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap);
|
|
||||||
DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReserveDispatchDeferredParticle()
|
||||||
|
{
|
||||||
|
int token = _deferredAlpha.Count;
|
||||||
|
_deferredAlpha.Add(_dispatchDeferredParticle);
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawOrdered(ICamera camera)
|
private void DrawOrdered(ICamera camera)
|
||||||
|
|
|
||||||
|
|
@ -94,25 +94,21 @@ internal interface IRetailAlphaDrawSource
|
||||||
/// retail's per-entry <c>clip</c> byte (<c>SetSurface</c>'s alpha-blend-vs-
|
/// retail's per-entry <c>clip</c> byte (<c>SetSurface</c>'s alpha-blend-vs-
|
||||||
/// clip-test arm selector).
|
/// clip-test arm selector).
|
||||||
///
|
///
|
||||||
/// <para>S4-c2 fix round 1 (M4): retail's per-entry <c>new</c>
|
/// <para>S4-c2 fix round 2: retail's per-entry <c>new</c>
|
||||||
/// ("first-for-this-list") byte is NOT modeled here. Retail's quantifier is
|
/// ("first-for-this-list") byte is NOT modeled here, but it is not
|
||||||
/// per-<c>DrawMesh</c>-INVOCATION: <c>DrawMesh</c> @0x0059d4a0 sets both
|
/// always true. <c>DrawMesh</c> @0x0059d4a0 sets the CLIP and ALPHA flags
|
||||||
/// list flags true at ENTRY (<c>0059d4cc arg3 = 1; 0059d4d0 var_c = 1</c>)
|
/// true at ENTRY and clears each independently after the first successful
|
||||||
/// and clears each independently after its OWN first append to that list
|
/// append to that list within that one <c>DrawMesh</c> call. A call can walk
|
||||||
/// WITHIN THAT CALL (<c>0059d5ef if (var_4_1 == 0) var_c = 0; else arg3 =
|
/// several surface subsets and append more than once, including to both
|
||||||
/// 0;</c>) — one retail subset can append to only one list once, so this
|
/// lists; the capture proves the varying value (10,556 <c>AM</c> lines:
|
||||||
/// flag is trivially true for every subset retail ever appends; the
|
/// <c>new=1</c> on 9,685, <c>new=0</c> on 871). Acdream's retained submission
|
||||||
/// captures confirm it (10,556 <c>AM</c> lines, <c>new=1</c> on 9,685 of
|
/// units do not have a one-to-one relationship with retail <c>DrawMesh</c>
|
||||||
/// them — impossible under "first since the last drain", which acdream's
|
/// invocations (ordinary instances and the accepted EnvCell `(cell,list)`
|
||||||
/// deleted <c>isFirstForList = target.Count == 0</c> computed instead). One
|
/// token can aggregate several subsets), so synthesizing the bit would be
|
||||||
/// acdream append already IS one full <c>DrawMesh</c> invocation (one
|
/// false precision. No Vulkan draw reads the flag: every prepared batch
|
||||||
/// instance, one call), so the truthful port of an always-true flag with no
|
/// binds its own material and matrix state. Omitting this unread capture-
|
||||||
/// acdream reader (Vulkan's per-batch material/matrix binding makes
|
/// optimization bit is therefore the documented modern-backend adaptation,
|
||||||
/// retail's per-entry material-capture optimization a no-op here — see
|
/// not a claim that retail's value cannot vary.</para>
|
||||||
/// <see cref="RetailAlphaQueue"/>'s class doc comment) is to not carry it at
|
|
||||||
/// all, rather than keep a field whose value can never vary and whose only
|
|
||||||
/// possible test is therefore vacuous
|
|
||||||
/// (<c>feedback_every_new_pin_must_be_shown_to_fail</c>).</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly record struct RetailAlphaEntry(
|
internal readonly record struct RetailAlphaEntry(
|
||||||
IRetailAlphaDrawSource Source,
|
IRetailAlphaDrawSource Source,
|
||||||
|
|
@ -174,8 +170,11 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
private int[] _sourceDrawOffsets = new int[4];
|
private int[] _sourceDrawOffsets = new int[4];
|
||||||
private readonly RetainedScratchCapacityPolicy _scratchPolicy;
|
private readonly RetainedScratchCapacityPolicy _scratchPolicy;
|
||||||
private readonly long _scratchBudgetBytes;
|
private readonly long _scratchBudgetBytes;
|
||||||
|
private readonly Action<RetailAlphaFlushSite>? _drainObserver;
|
||||||
|
|
||||||
internal RetailAlphaQueue(long? scratchBudgetBytes = null)
|
internal RetailAlphaQueue(
|
||||||
|
long? scratchBudgetBytes = null,
|
||||||
|
Action<RetailAlphaFlushSite>? drainObserver = null)
|
||||||
{
|
{
|
||||||
long budget = scratchBudgetBytes
|
long budget = scratchBudgetBytes
|
||||||
?? AlphaScratchBudgetProfile.Create(
|
?? AlphaScratchBudgetProfile.Create(
|
||||||
|
|
@ -183,6 +182,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(budget);
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(budget);
|
||||||
_scratchBudgetBytes = budget;
|
_scratchBudgetBytes = budget;
|
||||||
_scratchPolicy = new RetainedScratchCapacityPolicy(budget);
|
_scratchPolicy = new RetainedScratchCapacityPolicy(budget);
|
||||||
|
_drainObserver = drainObserver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCollecting { get; private set; }
|
public bool IsCollecting { get; private set; }
|
||||||
|
|
@ -221,8 +221,8 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
/// caller (<see cref="RetailAlphaMeshRouter"/>'s decision) supplies
|
/// caller (<see cref="RetailAlphaMeshRouter"/>'s decision) supplies
|
||||||
/// <paramref name="overrideClipmap"/>. Retail's per-entry "first for
|
/// <paramref name="overrideClipmap"/>. Retail's per-entry "first for
|
||||||
/// this list" flag is not modeled — see <see cref="RetailAlphaEntry"/>'s
|
/// this list" flag is not modeled — see <see cref="RetailAlphaEntry"/>'s
|
||||||
/// own doc comment (M4) for why it is trivially true for every subset
|
/// own doc comment for why the varying retail bit has no faithful
|
||||||
/// retail ever appends and therefore carries no information here.
|
/// one-to-one producer or consumer in this retained Vulkan path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool TryAppend(
|
internal bool TryAppend(
|
||||||
RetailAlphaList list,
|
RetailAlphaList list,
|
||||||
|
|
@ -237,11 +237,17 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
throw new ArgumentOutOfRangeException(nameof(token));
|
throw new ArgumentOutOfRangeException(nameof(token));
|
||||||
|
|
||||||
List<RetailAlphaEntry> target = list == RetailAlphaList.Clip ? _clip : _alpha;
|
List<RetailAlphaEntry> target = list == RetailAlphaList.Clip ? _clip : _alpha;
|
||||||
|
// All production callers reserve source-owned payload before this
|
||||||
|
// call. Retail drops an entry at capacity, but the source still has
|
||||||
|
// frame-local payload that must be released at the next drain/abort.
|
||||||
|
// Register before the capacity return so a source whose FIRST append
|
||||||
|
// is rejected participates in that cleanup without preparing or
|
||||||
|
// drawing the rejected token.
|
||||||
|
RegisterSource(source);
|
||||||
if (target.Count >= ListCapacity)
|
if (target.Count >= ListCapacity)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
target.Add(new RetailAlphaEntry(source, token, overrideClipmap));
|
target.Add(new RetailAlphaEntry(source, token, overrideClipmap));
|
||||||
RegisterSource(source);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,14 +270,17 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Flush(RetailAlphaFlushSite site, float threshold)
|
public void Flush(RetailAlphaFlushSite site, float threshold)
|
||||||
{
|
{
|
||||||
_ = site; // site distinguishes call sites for tracing/tests only —
|
|
||||||
// the drain algorithm itself does not depend on which one.
|
|
||||||
if (!IsCollecting)
|
if (!IsCollecting)
|
||||||
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
|
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
|
||||||
|
|
||||||
if (_clip.Count < threshold * ListCapacity && _alpha.Count < threshold * ListCapacity)
|
if (_clip.Count < threshold * ListCapacity && _alpha.Count < threshold * ListCapacity)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Optional construction-time observer for the concrete outdoor-frame
|
||||||
|
// ownership gate. It sees only real drains (after retail's threshold
|
||||||
|
// early return), never alters the drain algorithm, and production
|
||||||
|
// composition leaves it null.
|
||||||
|
_drainObserver?.Invoke(site);
|
||||||
DrainAndReset();
|
DrainAndReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
internal interface IEnvCellImmediateDrawSink
|
||||||
|
{
|
||||||
|
void DrawImmediate(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive);
|
||||||
|
}
|
||||||
|
|
||||||
// S3 chunk 4 fix round 2 (L2): RetailPViewFramebufferSize / IRetailPViewFramebufferSource
|
// S3 chunk 4 fix round 2 (L2): RetailPViewFramebufferSize / IRetailPViewFramebufferSource
|
||||||
// / SilkRetailPViewFramebufferSource are deleted — their only consumer was
|
// / SilkRetailPViewFramebufferSource are deleted — their only consumer was
|
||||||
// the RHI surface's NDC-to-pixel scissor conversion, and that scissor
|
// the RHI surface's NDC-to-pixel scissor conversion, and that scissor
|
||||||
|
|
@ -41,7 +49,7 @@ internal sealed class RetailPViewCellSource : IRetailPViewCellSource
|
||||||
/// is written once; the four places it touches graphics state directly are owned
|
/// is written once; the four places it touches graphics state directly are owned
|
||||||
/// by <see cref="IWorldPassSurface"/>.
|
/// by <see cref="IWorldPassSurface"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed partial class RetailPViewPassExecutor
|
internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSink
|
||||||
{
|
{
|
||||||
private readonly IWorldPassSurface _surface;
|
private readonly IWorldPassSurface _surface;
|
||||||
private readonly IRenderFrameGlState _frameGlState;
|
private readonly IRenderFrameGlState _frameGlState;
|
||||||
|
|
@ -57,7 +65,15 @@ internal sealed partial class RetailPViewPassExecutor
|
||||||
private readonly WorldRenderDiagnostics _diagnostics;
|
private readonly WorldRenderDiagnostics _diagnostics;
|
||||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||||
public RetailPViewPassExecutor(
|
private readonly EnvCellAlphaDrawSource _envCellClipAlphaSource;
|
||||||
|
private readonly EnvCellAlphaDrawSource _envCellBlendAlphaSource;
|
||||||
|
|
||||||
|
internal delegate void RenderImmediateEnvCellRoute(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive);
|
||||||
|
|
||||||
|
public RetailPViewPassExecutor(
|
||||||
IWorldPassSurface surface,
|
IWorldPassSurface surface,
|
||||||
IRenderFrameGlState frameGlState,
|
IRenderFrameGlState frameGlState,
|
||||||
ClipFrame clipFrame,
|
ClipFrame clipFrame,
|
||||||
|
|
@ -87,6 +103,12 @@ public RetailPViewPassExecutor(
|
||||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||||
_terrainDiagnostics = terrainDiagnostics
|
_terrainDiagnostics = terrainDiagnostics
|
||||||
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
||||||
|
_envCellClipAlphaSource = new EnvCellAlphaDrawSource(
|
||||||
|
_envCells.RenderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute.Clip);
|
||||||
|
_envCellBlendAlphaSource = new EnvCellAlphaDrawSource(
|
||||||
|
_envCells.RenderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute.Alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginFrame()
|
public void BeginFrame()
|
||||||
|
|
@ -168,50 +190,126 @@ public RetailPViewPassExecutor(
|
||||||
/// S4-c2 fix round 1 (M6, contract C4): retail <c>DrawEnvCell</c>
|
/// S4-c2 fix round 1 (M6, contract C4): retail <c>DrawEnvCell</c>
|
||||||
/// @0x0059f1c2 installs the environment detail surface (may be null)
|
/// @0x0059f1c2 installs the environment detail surface (may be null)
|
||||||
/// before <c>DrawMesh</c> @0x0059f212 and clears it @0x0059f21a — detail
|
/// before <c>DrawMesh</c> @0x0059f212 and clears it @0x0059f21a — detail
|
||||||
/// ON makes the subset take Row 1 (immediate, WITH detail —
|
/// ON makes eligible subsets take Row 1 (immediate, WITH detail —
|
||||||
/// <see cref="EnvCellRenderer.TransparentDetailEnabled"/>'s own doc
|
/// <see cref="EnvCellRenderer.TransparentDetailEnabled"/>'s own doc
|
||||||
/// comment); detail OFF takes Row 3 (append; mask 0x02 alpha-family for
|
/// comment); detail OFF still routes each real subset by the exact
|
||||||
/// an EnvCell transparent shell surface). Routed through the SAME table
|
/// <c>TextureBatchData.RetailSurfaceMask</c> retained on
|
||||||
/// every other alpha submitter uses so the decision is the table's, not
|
/// <c>ObjectRenderBatch</c>. A pure Base1ClipMap mask 0x08 reaches CLIP;
|
||||||
/// a hand-coded branch. <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
/// alpha-family 0x02 reaches ALPHA; a table-Immediate mask stays at the
|
||||||
/// already replays a cell's transparent batch on demand, so the queue
|
/// cell turn. The queue coalesces only to one token per (cell,list), the
|
||||||
/// side (<see cref="EnvCellAlphaDrawSource"/>) needs no new deferred-
|
/// accepted AP-238 granularity residual.
|
||||||
/// replay abstraction — one token per CELL (coarser than retail's own
|
|
||||||
/// per-subset entries; AP-236's residual documents that granularity gap).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SubmitOrDrawTransparentCellShell(uint cellId)
|
internal void SubmitOrDrawTransparentCellShell(uint cellId)
|
||||||
{
|
{
|
||||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
bool detailSurfaceActive = _envCells.TransparentDetailEnabled;
|
||||||
currentlyDrawingSky: false,
|
EnvCellTransparentRoute routes = _envCells.GetTransparentRoutes(
|
||||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
cellId,
|
||||||
detailSurfaceActive: _envCells.TransparentDetailEnabled,
|
detailSurfaceActive);
|
||||||
multiPassAlpha: false,
|
DispatchTransparentCellShell(
|
||||||
subsetMask: RetailAlphaMeshRouter.MaskAlphaFamily,
|
cellId,
|
||||||
materialHasAlpha: false);
|
routes,
|
||||||
|
detailSurfaceActive,
|
||||||
if (decision.Action == RetailAlphaMeshAction.Immediate)
|
_alpha,
|
||||||
{
|
_envCellClipAlphaSource,
|
||||||
_singleCellListScratch.Clear();
|
_envCellBlendAlphaSource,
|
||||||
_singleCellListScratch.Add(cellId);
|
this);
|
||||||
_envCells.RenderTransparentOrdered(_singleCellListScratch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Row 2 (AppendClipAndImmediate) needs MultiPassAlpha, fixed false
|
|
||||||
// above — provably unreachable, same reasoning as
|
|
||||||
// WbDrawDispatcher.SubmitToAlphaQueue's ordinary-content case.
|
|
||||||
_envCellAlphaSource ??= new EnvCellAlphaDrawSource(_envCells.RenderTransparentOrdered);
|
|
||||||
int token = _envCellAlphaSource.AddPendingCellId(cellId);
|
|
||||||
_alpha.TryAppend(decision.List, _envCellAlphaSource, token, decision.OverrideClipmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EnvCellAlphaDrawSource? _envCellAlphaSource;
|
|
||||||
|
|
||||||
private readonly List<uint> _singleCellListScratch = new(1);
|
private readonly List<uint> _singleCellListScratch = new(1);
|
||||||
|
|
||||||
|
private void DrawImmediateEnvCellRoute(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
_singleCellListScratch.Clear();
|
||||||
|
_singleCellListScratch.Add(cellId);
|
||||||
|
_envCells.RenderTransparentOrdered(
|
||||||
|
_singleCellListScratch,
|
||||||
|
route,
|
||||||
|
detailSurfaceActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IEnvCellImmediateDrawSink.DrawImmediate(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive) =>
|
||||||
|
DrawImmediateEnvCellRoute(cellId, route, detailSurfaceActive);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Production dispatch for one cell after <see cref="EnvCellRenderer"/>
|
||||||
|
/// scanned its real transparent batches. Separate source identities keep
|
||||||
|
/// CLIP and ALPHA replay filters attached to their tokens. Immediate
|
||||||
|
/// subsets draw at the cell turn; each deferred list receives at most one
|
||||||
|
/// token for this cell.
|
||||||
|
/// </summary>
|
||||||
|
internal static void DispatchTransparentCellShell(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute routes,
|
||||||
|
bool detailSurfaceActive,
|
||||||
|
RetailAlphaQueue queue,
|
||||||
|
EnvCellAlphaDrawSource clipSource,
|
||||||
|
EnvCellAlphaDrawSource alphaSource,
|
||||||
|
IEnvCellImmediateDrawSink renderImmediate)
|
||||||
|
{
|
||||||
|
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
|
||||||
|
{
|
||||||
|
renderImmediate.DrawImmediate(
|
||||||
|
cellId,
|
||||||
|
EnvCellTransparentRoute.Immediate,
|
||||||
|
detailSurfaceActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||||
|
{
|
||||||
|
int token = clipSource.AddPendingCellId(cellId);
|
||||||
|
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||||
|
{
|
||||||
|
int token = alphaSource.AddPendingCellId(cellId);
|
||||||
|
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Delegate adapter retained for focused route/allocation tests;
|
||||||
|
/// production calls the interface overload above with <c>this</c>, so no
|
||||||
|
/// per-cell delegate conversion or retained callback enters the owner.</summary>
|
||||||
|
internal static void DispatchTransparentCellShell(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute routes,
|
||||||
|
bool detailSurfaceActive,
|
||||||
|
RetailAlphaQueue queue,
|
||||||
|
EnvCellAlphaDrawSource clipSource,
|
||||||
|
EnvCellAlphaDrawSource alphaSource,
|
||||||
|
RenderImmediateEnvCellRoute renderImmediate)
|
||||||
|
{
|
||||||
|
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
|
||||||
|
{
|
||||||
|
renderImmediate(
|
||||||
|
cellId,
|
||||||
|
EnvCellTransparentRoute.Immediate,
|
||||||
|
detailSurfaceActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||||
|
{
|
||||||
|
int token = clipSource.AddPendingCellId(cellId);
|
||||||
|
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||||
|
{
|
||||||
|
int token = alphaSource.AddPendingCellId(cellId);
|
||||||
|
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The queue-facing half of <see cref="SubmitOrDrawTransparentCellShell"/>:
|
/// The queue-facing half of <see cref="SubmitOrDrawTransparentCellShell"/>:
|
||||||
/// retains the cell ids submitted this frame in append order and replays
|
/// retains the cell ids submitted to one fixed list in append order and replays
|
||||||
/// a drained slice through <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
/// a drained slice through <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
|
||||||
/// (production wiring) — one token per cell, never per subset (M6's
|
/// (production wiring) — one token per cell, never per subset (M6's
|
||||||
/// documented granularity residual). <see cref="IRetailAlphaDrawSource.PrepareAlphaDraws"/>'s
|
/// documented granularity residual). <see cref="IRetailAlphaDrawSource.PrepareAlphaDraws"/>'s
|
||||||
|
|
@ -224,7 +322,14 @@ public RetailPViewPassExecutor(
|
||||||
/// <c>EnvCellAlphaDrawSourceTests</c> can pin the queue mechanics
|
/// <c>EnvCellAlphaDrawSourceTests</c> can pin the queue mechanics
|
||||||
/// without standing up a GPU-backed renderer.
|
/// without standing up a GPU-backed renderer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class EnvCellAlphaDrawSource(Action<IReadOnlyList<uint>> renderTransparentOrdered)
|
internal delegate void RenderEnvCellsByRoute(
|
||||||
|
IReadOnlyList<uint> cellIds,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive);
|
||||||
|
|
||||||
|
internal sealed class EnvCellAlphaDrawSource(
|
||||||
|
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute route)
|
||||||
: IRetailAlphaDrawSource
|
: IRetailAlphaDrawSource
|
||||||
{
|
{
|
||||||
private readonly List<uint> _pendingCellIds = new();
|
private readonly List<uint> _pendingCellIds = new();
|
||||||
|
|
@ -254,14 +359,17 @@ public RetailPViewPassExecutor(
|
||||||
_drawScratch.Clear();
|
_drawScratch.Clear();
|
||||||
for (int i = 0; i < drawCount; i++)
|
for (int i = 0; i < drawCount; i++)
|
||||||
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
||||||
renderTransparentOrdered(_drawScratch);
|
renderTransparentOrdered(_drawScratch, route, detailSurfaceActive: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetAlphaSubmissions()
|
public void ResetAlphaSubmissions()
|
||||||
{
|
{
|
||||||
_pendingCellIds.Clear();
|
_pendingCellIds.Clear();
|
||||||
_preparedCellIds.Clear();
|
_preparedCellIds.Clear();
|
||||||
|
_drawScratch.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal int PendingCount => _pendingCellIds.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@ using DatReaderWriter.Enums;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum EnvCellTransparentRoute : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Immediate = 1 << 0,
|
||||||
|
Clip = 1 << 1,
|
||||||
|
Alpha = 1 << 2,
|
||||||
|
All = Immediate | Clip | Alpha,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed partial class EnvCellRenderer :
|
public sealed partial class EnvCellRenderer :
|
||||||
IDisposable,
|
IDisposable,
|
||||||
IEnvCellLandblockPublisher
|
IEnvCellLandblockPublisher
|
||||||
|
|
@ -803,7 +813,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
public void Render(WbRenderPass renderPass)
|
public void Render(WbRenderPass renderPass)
|
||||||
{
|
{
|
||||||
// WB EnvCellRenderManager.cs:396:
|
// WB EnvCellRenderManager.cs:396:
|
||||||
RenderCore(renderPass, null, null);
|
RenderCore(renderPass, null, null, EnvCellTransparentRoute.All, detailSurfaceActive: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -817,7 +827,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
||||||
=> RenderCore(renderPass, filter, null);
|
=> RenderCore(renderPass, filter, null, EnvCellTransparentRoute.All, detailSurfaceActive: false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws transparent cell shells in the supplied far-to-near PView order.
|
/// Draws transparent cell shells in the supplied far-to-near PView order.
|
||||||
|
|
@ -828,13 +838,40 @@ public sealed partial class EnvCellRenderer :
|
||||||
public void RenderTransparentOrdered(IReadOnlyList<uint> orderedCellIds)
|
public void RenderTransparentOrdered(IReadOnlyList<uint> orderedCellIds)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(orderedCellIds);
|
ArgumentNullException.ThrowIfNull(orderedCellIds);
|
||||||
RenderCore(WbRenderPass.Transparent, null, orderedCellIds);
|
RenderCore(
|
||||||
|
WbRenderPass.Transparent,
|
||||||
|
null,
|
||||||
|
orderedCellIds,
|
||||||
|
EnvCellTransparentRoute.All,
|
||||||
|
detailSurfaceActive: TransparentDetailEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replays only the transparent subsets whose exact retained retail mask
|
||||||
|
/// resolves to <paramref name="route"/>. S4-c2 uses this for its separate
|
||||||
|
/// CLIP/ALPHA queue tokens; the detail-on immediate turn uses the same
|
||||||
|
/// table with <paramref name="detailSurfaceActive"/> true.
|
||||||
|
/// </summary>
|
||||||
|
internal void RenderTransparentOrdered(
|
||||||
|
IReadOnlyList<uint> orderedCellIds,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(orderedCellIds);
|
||||||
|
RenderCore(
|
||||||
|
WbRenderPass.Transparent,
|
||||||
|
null,
|
||||||
|
orderedCellIds,
|
||||||
|
route,
|
||||||
|
detailSurfaceActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RenderCore(
|
private void RenderCore(
|
||||||
WbRenderPass renderPass,
|
WbRenderPass renderPass,
|
||||||
HashSet<uint>? filter,
|
HashSet<uint>? filter,
|
||||||
IReadOnlyList<uint>? orderedCellIds)
|
IReadOnlyList<uint>? orderedCellIds,
|
||||||
|
EnvCellTransparentRoute transparentRoute,
|
||||||
|
bool detailSurfaceActive)
|
||||||
{
|
{
|
||||||
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
||||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||||
|
|
@ -969,7 +1006,9 @@ public sealed partial class EnvCellRenderer :
|
||||||
drawCalls,
|
drawCalls,
|
||||||
allInstances,
|
allInstances,
|
||||||
_drawCallRanges,
|
_drawCallRanges,
|
||||||
renderPass);
|
renderPass,
|
||||||
|
transparentRoute,
|
||||||
|
detailSurfaceActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||||
|
|
@ -996,6 +1035,76 @@ public sealed partial class EnvCellRenderer :
|
||||||
public bool CellHasTransparent(uint cellId)
|
public bool CellHasTransparent(uint cellId)
|
||||||
=> _transparentCellIds.Contains(cellId);
|
=> _transparentCellIds.Contains(cellId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans the real prepared transparent render batches for one cell and
|
||||||
|
/// returns the set of outcomes from retail's shared DrawMesh table. A
|
||||||
|
/// cell may contribute one token to each deferred list; masks that resolve
|
||||||
|
/// Immediate remain at the cell turn.
|
||||||
|
/// </summary>
|
||||||
|
internal EnvCellTransparentRoute GetTransparentRoutes(
|
||||||
|
uint cellId,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
lock (_renderLock)
|
||||||
|
{
|
||||||
|
if (!_activeSnapshot.BatchedByCell.TryGetValue(cellId, out var groups))
|
||||||
|
return EnvCellTransparentRoute.None;
|
||||||
|
|
||||||
|
EnvCellTransparentRoute routes = EnvCellTransparentRoute.None;
|
||||||
|
foreach ((ulong gfxObjId, List<InstanceData> transforms) in groups)
|
||||||
|
{
|
||||||
|
if (transforms.Count == 0)
|
||||||
|
continue;
|
||||||
|
ObjectRenderData? renderData = _meshManager.TryGetRenderData(gfxObjId);
|
||||||
|
if (renderData is null || renderData.IsSetup)
|
||||||
|
continue;
|
||||||
|
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
|
||||||
|
{
|
||||||
|
ObjectRenderBatch batch = renderData.Batches[batchIndex];
|
||||||
|
if (batch.IsTransparent)
|
||||||
|
routes |= RouteTransparentBatch(batch, detailSurfaceActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact per-subset EnvCell route. Canonical installed-DAT witness
|
||||||
|
/// F4180104 / surface 08000BFF retains mask 0x08 and therefore reaches
|
||||||
|
/// CLIP when environment detail is absent; alpha-family mask 0x02 reaches
|
||||||
|
/// ALPHA. No collapsed <see cref="ObjectRenderBatch.Translucency"/> value
|
||||||
|
/// participates in this decision.
|
||||||
|
/// </summary>
|
||||||
|
internal static EnvCellTransparentRoute RouteTransparentBatch(
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||||
|
currentlyDrawingSky: false,
|
||||||
|
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||||
|
detailSurfaceActive: detailSurfaceActive,
|
||||||
|
multiPassAlpha: false,
|
||||||
|
subsetMask: batch.RetailSurfaceMask,
|
||||||
|
materialHasAlpha: false);
|
||||||
|
return decision.Action switch
|
||||||
|
{
|
||||||
|
RetailAlphaMeshAction.Immediate => EnvCellTransparentRoute.Immediate,
|
||||||
|
RetailAlphaMeshAction.Append => decision.List == RetailAlphaList.Clip
|
||||||
|
? EnvCellTransparentRoute.Clip
|
||||||
|
: EnvCellTransparentRoute.Alpha,
|
||||||
|
RetailAlphaMeshAction.AppendClipAndImmediate =>
|
||||||
|
EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Immediate,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(decision.Action)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesTransparentRoute(
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive) =>
|
||||||
|
(RouteTransparentBatch(batch, detailSurfaceActive) & route) != 0;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GetCellLightSet (A7 Fix D D-2 helper)
|
// GetCellLightSet (A7 Fix D D-2 helper)
|
||||||
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
||||||
|
|
@ -1074,7 +1183,9 @@ public sealed partial class EnvCellRenderer :
|
||||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
||||||
List<InstanceData> allInstances,
|
List<InstanceData> allInstances,
|
||||||
IReadOnlyList<DrawCallRange> drawCallRanges,
|
IReadOnlyList<DrawCallRange> drawCallRanges,
|
||||||
WbRenderPass renderPass)
|
WbRenderPass renderPass,
|
||||||
|
EnvCellTransparentRoute transparentRoute,
|
||||||
|
bool detailSurfaceActive)
|
||||||
{
|
{
|
||||||
// WB BaseObjectRenderManager.cs:710-713:
|
// WB BaseObjectRenderManager.cs:710-713:
|
||||||
if (drawCalls.Count == 0 || allInstances.Count == 0) return;
|
if (drawCalls.Count == 0 || allInstances.Count == 0) return;
|
||||||
|
|
@ -1115,6 +1226,13 @@ public sealed partial class EnvCellRenderer :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderPass == WbRenderPass.Transparent
|
||||||
|
&& transparentRoute != EnvCellTransparentRoute.All
|
||||||
|
&& !MatchesTransparentRoute(batch, transparentRoute, detailSurfaceActive))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
totalDraws++;
|
totalDraws++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1171,6 +1289,13 @@ public sealed partial class EnvCellRenderer :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderPass == WbRenderPass.Transparent
|
||||||
|
&& transparentRoute != EnvCellTransparentRoute.All
|
||||||
|
&& !MatchesTransparentRoute(batch, transparentRoute, detailSurfaceActive))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
||||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||||
_batchesByCullGroup[groupIndex];
|
_batchesByCullGroup[groupIndex];
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,14 @@ namespace AcDream.App.Rendering.Wb
|
||||||
public (int Width, int Height) TextureSize { get; set; }
|
public (int Width, int Height) TextureSize { get; set; }
|
||||||
public TextureFormat TextureFormat { get; set; }
|
public TextureFormat TextureFormat { get; set; }
|
||||||
public uint SurfaceId { get; set; }
|
public uint SurfaceId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>D3DPolyRender::ConstructMesh</c> @0x0059dfa0's exact
|
||||||
|
/// per-subset routing mask. The Content extraction preserves this on
|
||||||
|
/// <c>TextureBatchData</c>; S4-c2 carries it across the upload boundary
|
||||||
|
/// so EnvCell replay can choose CLIP versus ALPHA without reconstructing
|
||||||
|
/// the raw surface bits from <see cref="Translucency"/>.
|
||||||
|
/// </summary>
|
||||||
|
public byte RetailSurfaceMask { get; set; }
|
||||||
public TextureKey Key { get; set; }
|
public TextureKey Key { get; set; }
|
||||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||||
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
||||||
|
|
@ -2250,6 +2258,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
TextureIndex = textureIndex,
|
TextureIndex = textureIndex,
|
||||||
TextureSize = (format.Width, format.Height),
|
TextureSize = (format.Width, format.Height),
|
||||||
TextureFormat = format.Format,
|
TextureFormat = format.Format,
|
||||||
|
RetailSurfaceMask = batch.RetailSurfaceMask,
|
||||||
Translucency = batch.Translucency,
|
Translucency = batch.Translucency,
|
||||||
IsTransparent = batch.IsTransparent,
|
IsTransparent = batch.IsTransparent,
|
||||||
IsAdditive = batch.IsAdditive,
|
IsAdditive = batch.IsAdditive,
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,110 @@
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
|
using AcDream.Content;
|
||||||
|
using Chorizite.Core.Render.Enums;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using CullMode = DatReaderWriter.Enums.CullMode;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 fix round 1 (M6, contract C4): <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>
|
/// S4-c2 final EnvCell production-dispatch pins: exact retained per-batch mask,
|
||||||
/// is the queue-facing half of an EnvCell transparent shell's detail-off
|
/// one token per (cell,list), separate list-filtered replay sources, immediate
|
||||||
/// submission — one token per CELL, replayed through a caller-supplied
|
/// detail turns, interleaving, and warmed allocation behavior.
|
||||||
/// delegate (production wiring:
|
|
||||||
/// <see cref="AcDream.App.Rendering.Wb.EnvCellRenderer.RenderTransparentOrdered"/>)
|
|
||||||
/// at whatever flush site the shared <see cref="RetailAlphaQueue"/> drains
|
|
||||||
/// at. These tests exercise the REAL production class end-to-end against a
|
|
||||||
/// real queue — only the render destination is faked (a delegate recording
|
|
||||||
/// what it was asked to draw), so the GPU-backed <c>EnvCellRenderer</c>
|
|
||||||
/// never needs to be constructed to pin the queue mechanics.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnvCellAlphaDrawSourceTests
|
public sealed class EnvCellAlphaDrawSourceTests
|
||||||
{
|
{
|
||||||
/// <summary>Mutation check: if <c>SubmitOrDrawTransparentCellShell</c>
|
/// <summary>
|
||||||
/// drew the cell immediately instead of appending it when detail is off
|
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
|
||||||
/// (the M1/M3 bug class this chunk fixes elsewhere), the drawn list
|
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
|
||||||
/// would already contain an entry BEFORE <c>Flush</c> runs — the
|
/// hardcoding MaskAlphaFamily in the production batch classifier makes
|
||||||
/// <c>Assert.Empty(drawn)</c> line fails against that mutation.</summary>
|
/// the first assertion report Alpha instead of Clip.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt()
|
public void RetainedPerBatchMask_RoutesClipAndAlphaExactly()
|
||||||
{
|
{
|
||||||
var drawn = new List<IReadOnlyList<uint>>();
|
var clip = new ObjectRenderBatch { IsTransparent = true, RetailSurfaceMask = 0x08 };
|
||||||
var source = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
var alpha = new ObjectRenderBatch { IsTransparent = true, RetailSurfaceMask = 0x02 };
|
||||||
cells => drawn.Add(cells.ToArray()));
|
|
||||||
var queue = new RetailAlphaQueue();
|
|
||||||
|
|
||||||
queue.BeginFrame();
|
Assert.Equal(
|
||||||
int token = source.AddPendingCellId(0x1234u);
|
EnvCellTransparentRoute.Clip,
|
||||||
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, token, false));
|
EnvCellRenderer.RouteTransparentBatch(clip, detailSurfaceActive: false));
|
||||||
|
Assert.Equal(
|
||||||
|
EnvCellTransparentRoute.Alpha,
|
||||||
|
EnvCellRenderer.RouteTransparentBatch(alpha, detailSurfaceActive: false));
|
||||||
|
}
|
||||||
|
|
||||||
Assert.Empty(drawn);
|
/// <summary>
|
||||||
|
/// A mixed cell creates one token in each list and neither set draws before
|
||||||
|
/// the real queue drain. CLIP drains first and receives only the CLIP
|
||||||
|
/// replay filter; ALPHA receives only ALPHA. Mutation checks: inverting the
|
||||||
|
/// detail-off immediate predicate makes <c>Assert.Empty(log)</c> fail;
|
||||||
|
/// removing the replay filter makes either route assertion fail.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void MixedCell_DefersOneTokenPerList_AndReplayIsListFiltered()
|
||||||
|
{
|
||||||
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
0x08,
|
||||||
|
0x02);
|
||||||
|
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
||||||
|
ProductionEnvCellFixture.CellId,
|
||||||
|
detailSurfaceActive: false);
|
||||||
|
|
||||||
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
fixture.Queue.BeginFrame();
|
||||||
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
|
ProductionEnvCellFixture.CellId,
|
||||||
|
routes,
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
fixture.Queue,
|
||||||
|
fixture.ClipSource,
|
||||||
|
fixture.AlphaSource,
|
||||||
|
fixture.ImmediateSink);
|
||||||
|
|
||||||
IReadOnlyList<uint> singleDraw = Assert.Single(drawn);
|
Assert.Equal(EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Alpha, routes);
|
||||||
Assert.Equal(new uint[] { 0x1234u }, singleDraw);
|
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
queue.EndFrame();
|
Assert.Equal(1, fixture.Queue.ClipCount);
|
||||||
|
Assert.Equal(1, fixture.Queue.AlphaCount);
|
||||||
|
|
||||||
|
fixture.Queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||||
|
|
||||||
|
GpuRecordedMultiDrawIndirect[] draws =
|
||||||
|
[.. fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||||
|
Assert.Equal(2, draws.Length);
|
||||||
|
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
|
||||||
|
fixture.Queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetailOn_DrawsAtCellTurnWithDetail_AndQueuesNothing()
|
||||||
|
{
|
||||||
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
|
detailSurfaceActive: true,
|
||||||
|
0x08);
|
||||||
|
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
||||||
|
ProductionEnvCellFixture.CellId,
|
||||||
|
detailSurfaceActive: true);
|
||||||
|
fixture.Queue.BeginFrame();
|
||||||
|
|
||||||
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
|
ProductionEnvCellFixture.CellId,
|
||||||
|
routes,
|
||||||
|
detailSurfaceActive: true,
|
||||||
|
fixture.Queue,
|
||||||
|
fixture.ClipSource,
|
||||||
|
fixture.AlphaSource,
|
||||||
|
fixture.ImmediateSink);
|
||||||
|
|
||||||
|
Assert.Equal(EnvCellTransparentRoute.Immediate, routes);
|
||||||
|
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
|
||||||
|
Assert.True(fixture.ImmediateSink.LastDetailSurfaceActive);
|
||||||
|
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||||
|
Assert.Equal(0, fixture.Queue.PendingCount);
|
||||||
|
fixture.Queue.EndFrame();
|
||||||
|
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||||
|
|
@ -57,20 +122,260 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
|
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
|
||||||
{
|
{
|
||||||
var log = new List<string>();
|
var log = new List<string>();
|
||||||
var cellSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
var clipSource = Source(EnvCellTransparentRoute.Clip, log);
|
||||||
cells => log.Add($"cell:{string.Join(',', cells)}"));
|
var cellSource = Source(EnvCellTransparentRoute.Alpha, log);
|
||||||
var particleSource = new RecordingSource("particle", log);
|
var particleSource = new RecordingSource("particle", log);
|
||||||
var queue = new RetailAlphaQueue();
|
var queue = new RetailAlphaQueue();
|
||||||
|
|
||||||
queue.BeginFrame();
|
queue.BeginFrame();
|
||||||
int cell1Token = cellSource.AddPendingCellId(0x100u);
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell1Token, false));
|
0x100u, EnvCellTransparentRoute.Alpha, false, queue,
|
||||||
|
clipSource, cellSource, (_, _, _) => throw new InvalidOperationException());
|
||||||
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false));
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false));
|
||||||
int cell2Token = cellSource.AddPendingCellId(0x200u);
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell2Token, false));
|
0x200u, EnvCellTransparentRoute.Alpha, false, queue,
|
||||||
|
clipSource, cellSource, (_, _, _) => throw new InvalidOperationException());
|
||||||
queue.EndFrame();
|
queue.EndFrame();
|
||||||
|
|
||||||
Assert.Equal(new[] { "cell:256", "particle:7", "cell:512" }, log);
|
Assert.Equal(new[] { "Alpha:00000100", "particle:7", "Alpha:00000200" }, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionDispatch_WarmedImmediateAndFilteredSourcesDoNotAllocate()
|
||||||
|
{
|
||||||
|
var sink = new NoAllocRouteSink();
|
||||||
|
var clipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
sink.Render,
|
||||||
|
EnvCellTransparentRoute.Clip);
|
||||||
|
var alphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
sink.Render,
|
||||||
|
EnvCellTransparentRoute.Alpha);
|
||||||
|
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate = sink.DrawImmediate;
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
|
RunMixed(queue, clipSource, alphaSource, immediate);
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 1_000; i++)
|
||||||
|
RunMixed(queue, clipSource, alphaSource, immediate);
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunMixed(
|
||||||
|
RetailAlphaQueue queue,
|
||||||
|
RetailPViewPassExecutor.EnvCellAlphaDrawSource clipSource,
|
||||||
|
RetailPViewPassExecutor.EnvCellAlphaDrawSource alphaSource,
|
||||||
|
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate)
|
||||||
|
{
|
||||||
|
queue.BeginFrame();
|
||||||
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
|
0xF4180104u,
|
||||||
|
EnvCellTransparentRoute.Immediate
|
||||||
|
| EnvCellTransparentRoute.Clip
|
||||||
|
| EnvCellTransparentRoute.Alpha,
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
queue,
|
||||||
|
clipSource,
|
||||||
|
alphaSource,
|
||||||
|
immediate);
|
||||||
|
queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
|
||||||
|
EnvCellTransparentRoute expectedRoute,
|
||||||
|
List<string> log) =>
|
||||||
|
new(
|
||||||
|
(cells, route, detail) =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedRoute, route);
|
||||||
|
Assert.False(detail);
|
||||||
|
for (int i = 0; i < cells.Count; i++)
|
||||||
|
log.Add($"{route}:{cells[i]:X8}");
|
||||||
|
},
|
||||||
|
expectedRoute);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recording-GPU fixture for the production EnvCell scan, dispatch and
|
||||||
|
/// filtered replay path. The two retained masks enter through the real
|
||||||
|
/// Content upload boundary, not through hand-built App batches.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ProductionEnvCellFixture : IDisposable
|
||||||
|
{
|
||||||
|
public const uint CellId = 0xF4180104u;
|
||||||
|
private const ulong MeshId = 0x2_F4180104UL;
|
||||||
|
|
||||||
|
private readonly GpuDeviceFrameLifetime _frames;
|
||||||
|
private readonly VulkanWorldPassScope _scope;
|
||||||
|
private readonly ObjectMeshManager _meshManager;
|
||||||
|
private readonly IGpuPassEncoder _pass;
|
||||||
|
private readonly IDisposable _publication;
|
||||||
|
|
||||||
|
public ProductionEnvCellFixture(
|
||||||
|
bool detailSurfaceActive,
|
||||||
|
params byte[] retainedMasks)
|
||||||
|
{
|
||||||
|
Device = new RecordingGpuDevice();
|
||||||
|
_frames = new GpuDeviceFrameLifetime(Device);
|
||||||
|
_scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||||
|
_meshManager = new ObjectMeshManager(
|
||||||
|
new VulkanMeshPipelineDevice(Device.Retirement),
|
||||||
|
Device,
|
||||||
|
new NullPreparedAssetSource(),
|
||||||
|
NullLogger<ObjectMeshManager>.Instance);
|
||||||
|
|
||||||
|
var mesh = new ObjectMeshData
|
||||||
|
{
|
||||||
|
ObjectId = MeshId,
|
||||||
|
Vertices =
|
||||||
|
[
|
||||||
|
new VertexPositionNormalTexture { Position = new Vector3(0, 0, 0) },
|
||||||
|
new VertexPositionNormalTexture { Position = new Vector3(1, 0, 0) },
|
||||||
|
new VertexPositionNormalTexture { Position = new Vector3(0, 1, 0) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var batches = new List<TextureBatchData>(retainedMasks.Length);
|
||||||
|
for (int i = 0; i < retainedMasks.Length; i++)
|
||||||
|
{
|
||||||
|
batches.Add(new TextureBatchData
|
||||||
|
{
|
||||||
|
Key = new TextureKey { SurfaceId = 0x08000BFFu + (uint)i },
|
||||||
|
TextureData = new byte[8 * 8 * 4],
|
||||||
|
Indices = [0, 1, 2],
|
||||||
|
IsTransparent = true,
|
||||||
|
Translucency = AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
||||||
|
RetailSurfaceMask = retainedMasks[i],
|
||||||
|
CullMode = CullMode.Clockwise,
|
||||||
|
IsCellShell = true,
|
||||||
|
SourceSurfaceIndex = i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
|
||||||
|
Assert.NotNull(_meshManager.UploadMeshData(mesh));
|
||||||
|
|
||||||
|
Renderer = new EnvCellRenderer(
|
||||||
|
Device,
|
||||||
|
_frames,
|
||||||
|
_scope,
|
||||||
|
_meshManager,
|
||||||
|
new WbFrustum(),
|
||||||
|
detailSurfaceActive
|
||||||
|
? new TerrainAtlas.RetailDetailTextureBinding(
|
||||||
|
new GpuTextureSlot(99),
|
||||||
|
Tiling: 2f,
|
||||||
|
SurfaceTextureId: 1,
|
||||||
|
RenderSurfaceId: 2,
|
||||||
|
Width: 4,
|
||||||
|
Height: 4)
|
||||||
|
: default,
|
||||||
|
detailSurfaceActive ? DetailOn : DetailOff);
|
||||||
|
typeof(EnvCellRenderer)
|
||||||
|
.GetField("_activeSnapshot", System.Reflection.BindingFlags.NonPublic
|
||||||
|
| System.Reflection.BindingFlags.Instance)!
|
||||||
|
.SetValue(Renderer, new EnvCellVisibilitySnapshot
|
||||||
|
{
|
||||||
|
BatchedByCell = new Dictionary<uint, Dictionary<ulong, List<InstanceData>>>
|
||||||
|
{
|
||||||
|
[CellId] = new Dictionary<ulong, List<InstanceData>>
|
||||||
|
{
|
||||||
|
[MeshId] =
|
||||||
|
[
|
||||||
|
new InstanceData
|
||||||
|
{
|
||||||
|
Transform = Matrix4x4.Identity,
|
||||||
|
CellId = CellId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Queue = new RetailAlphaQueue();
|
||||||
|
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
Renderer.RenderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute.Clip);
|
||||||
|
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
Renderer.RenderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute.Alpha);
|
||||||
|
ImmediateSink = new ProductionImmediateSink(Renderer);
|
||||||
|
|
||||||
|
_frames.BeginFrame();
|
||||||
|
IGpuFrame frame = _frames.CurrentFrame!;
|
||||||
|
_pass = frame.BeginPass(
|
||||||
|
GpuPassDescription.BackbufferClear(
|
||||||
|
"s4-c2-envcell-production",
|
||||||
|
Vector4.Zero,
|
||||||
|
sampleCount: 1));
|
||||||
|
_publication = _scope.Publish(_pass);
|
||||||
|
Renderer.BeginFrame(frameSlot: 0);
|
||||||
|
Device.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecordingGpuDevice Device { get; }
|
||||||
|
|
||||||
|
public EnvCellRenderer Renderer { get; }
|
||||||
|
|
||||||
|
public RetailAlphaQueue Queue { get; }
|
||||||
|
|
||||||
|
public RetailPViewPassExecutor.EnvCellAlphaDrawSource ClipSource { get; }
|
||||||
|
|
||||||
|
public RetailPViewPassExecutor.EnvCellAlphaDrawSource AlphaSource { get; }
|
||||||
|
|
||||||
|
public ProductionImmediateSink ImmediateSink { get; }
|
||||||
|
|
||||||
|
private static bool DetailOn() => true;
|
||||||
|
|
||||||
|
private static bool DetailOff() => false;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_publication.Dispose();
|
||||||
|
_pass.Dispose();
|
||||||
|
Renderer.Dispose();
|
||||||
|
_meshManager.Dispose();
|
||||||
|
Device.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ProductionImmediateSink(EnvCellRenderer renderer) :
|
||||||
|
IEnvCellImmediateDrawSink
|
||||||
|
{
|
||||||
|
public int DrawCount { get; private set; }
|
||||||
|
|
||||||
|
public bool LastDetailSurfaceActive { get; private set; }
|
||||||
|
|
||||||
|
public void DrawImmediate(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
DrawCount++;
|
||||||
|
LastDetailSurfaceActive = detailSurfaceActive;
|
||||||
|
renderer.RenderTransparentOrdered([cellId], route, detailSurfaceActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
|
{
|
||||||
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
|
||||||
|
public CacheStats DecodedTextureCacheStats => default;
|
||||||
|
|
||||||
|
public PreparedAssetPresence Probe(
|
||||||
|
AcDream.Content.Pak.PakAssetType type,
|
||||||
|
uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Missing;
|
||||||
|
|
||||||
|
public PreparedAssetReadResult Read(
|
||||||
|
in PreparedAssetRequest request,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedAssetReadResult.Missing;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
|
private sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
|
||||||
|
|
@ -89,4 +394,21 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class NoAllocRouteSink
|
||||||
|
{
|
||||||
|
public void Render(
|
||||||
|
IReadOnlyList<uint> cells,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawImmediate(
|
||||||
|
uint cellId,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
bool detailSurfaceActive)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
|
using AcDream.Core.Vfx;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 fix round 1 (M1, blocking): <see cref="ParticleRenderer.RouteParticleSubmission"/>
|
/// S4-c2 particle routing and production-dispatch pins. The pure table tests
|
||||||
/// is the pure router-input derivation the blocking fix extracted from
|
/// remain narrow coverage; the two principal cases below call the actual
|
||||||
/// <c>DeferToRetailAlphaQueue</c> so it is directly testable without a GPU/
|
/// <see cref="ParticleRenderer.DeferToRetailAlphaQueue"/> dispatch that the
|
||||||
/// mesh-manager harness. Before this round, a mesh-particle batch classified
|
/// renderer's scene loop uses.
|
||||||
/// <see cref="TranslucencyKind.Opaque"/> (mask 0x00) reaching <c>DrawMesh</c>
|
|
||||||
/// row 5 (Immediate) hit an unconditional throw in the render loop — these
|
|
||||||
/// tests pin the two outcomes the fix made reachable.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ParticleRendererRouteTests
|
public sealed class ParticleRendererRouteTests
|
||||||
{
|
{
|
||||||
|
|
@ -24,16 +31,37 @@ public sealed class ParticleRendererRouteTests
|
||||||
/// derivation to a hardcoded `false` makes this assertion fail because
|
/// derivation to a hardcoded `false` makes this assertion fail because
|
||||||
/// the decision becomes Immediate instead of Append.</summary>
|
/// the decision becomes Immediate instead of Append.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_RoutesToAlphaAppend()
|
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_DefersAndDrawsOnlyAtFlush()
|
||||||
{
|
{
|
||||||
// Top byte (alpha) = 0x80 -> not 0xFF -> materialHasAlpha = true.
|
var queue = new RetailAlphaQueue();
|
||||||
const uint colorArgbWithAlpha = 0x80FFFFFFu;
|
var source = new RecordingSource();
|
||||||
|
int reserveCount = 0;
|
||||||
|
int immediateCount = 0;
|
||||||
|
queue.BeginFrame();
|
||||||
|
|
||||||
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
const uint colorArgbWithAlpha = 0x80FFFFFFu;
|
||||||
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, colorArgbWithAlpha);
|
RetailAlphaMeshDecision decision = ParticleRenderer.DeferToRetailAlphaQueue(
|
||||||
|
ParticleSubmissionKind.Mesh,
|
||||||
|
TranslucencyKind.Opaque,
|
||||||
|
colorArgbWithAlpha,
|
||||||
|
queue,
|
||||||
|
source,
|
||||||
|
reserveDeferred: () => reserveCount++,
|
||||||
|
drawImmediate: (_, _, _, _) => immediateCount++,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
drawIndex: 7);
|
||||||
|
|
||||||
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
||||||
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
||||||
|
Assert.Equal(1, reserveCount);
|
||||||
|
Assert.Equal(1, queue.AlphaCount);
|
||||||
|
Assert.Equal(0, source.DrawCount);
|
||||||
|
Assert.Equal(0, immediateCount);
|
||||||
|
|
||||||
|
queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(1, source.DrawCount);
|
||||||
|
Assert.Equal(0, immediateCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>M1(d) pin 2: the SAME Opaque-classified mesh-particle batch
|
/// <summary>M1(d) pin 2: the SAME Opaque-classified mesh-particle batch
|
||||||
|
|
@ -47,14 +75,100 @@ public sealed class ParticleRendererRouteTests
|
||||||
/// as a failure (an unhandled exception) rather than an assertion
|
/// as a failure (an unhandled exception) rather than an assertion
|
||||||
/// mismatch.</summary>
|
/// mismatch.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_RoutesImmediate()
|
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState()
|
||||||
{
|
{
|
||||||
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
|
var queue = new RetailAlphaQueue();
|
||||||
|
var source = new RecordingSource();
|
||||||
|
int reserveCount = 0;
|
||||||
|
var immediate = new List<(ParticleSubmissionKind Kind, int Index, bool Opaque)>();
|
||||||
|
queue.BeginFrame();
|
||||||
|
|
||||||
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
|
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
|
||||||
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, fullyOpaqueColorArgb);
|
RetailAlphaMeshDecision decision = ParticleRenderer.DeferToRetailAlphaQueue(
|
||||||
|
ParticleSubmissionKind.Mesh,
|
||||||
|
TranslucencyKind.Opaque,
|
||||||
|
fullyOpaqueColorArgb,
|
||||||
|
queue,
|
||||||
|
source,
|
||||||
|
reserveDeferred: () => reserveCount++,
|
||||||
|
drawImmediate: (_, kind, index, opaque) => immediate.Add((kind, index, opaque)),
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
drawIndex: 11);
|
||||||
|
|
||||||
Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action);
|
Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action);
|
||||||
|
Assert.Equal(0, reserveCount);
|
||||||
|
Assert.Equal(0, queue.PendingCount);
|
||||||
|
Assert.Equal(
|
||||||
|
[(ParticleSubmissionKind.Mesh, 11, true)],
|
||||||
|
immediate);
|
||||||
|
|
||||||
|
ParticleRenderer.MeshParticlePipelineState state =
|
||||||
|
ParticleRenderer.ResolveMeshParticlePipelineState(
|
||||||
|
TranslucencyKind.Opaque,
|
||||||
|
opaqueDepthState: immediate[0].Opaque);
|
||||||
|
Assert.Equal(GpuBlendMode.None, state.Blend);
|
||||||
|
Assert.True(state.Depth.Test);
|
||||||
|
Assert.True(state.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, state.Depth.Compare);
|
||||||
|
|
||||||
|
queue.EndFrame();
|
||||||
|
Assert.Equal(0, source.PrepareCount);
|
||||||
|
Assert.Equal(0, source.DrawCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// M1 production fixed-state/binding pin. Executes the real private
|
||||||
|
/// pipeline factory against the recording GPU device and the real
|
||||||
|
/// <c>PipelineForMeshBlend</c> selector (not a restated expected-state
|
||||||
|
/// helper). Mutation checks: making the opaque factory depth-write false
|
||||||
|
/// fails the descriptor assertions; mapping the immediate opaque case to
|
||||||
|
/// <c>_meshAlphaPipeline</c> fails the identity assertion.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription()
|
||||||
|
{
|
||||||
|
using var device = new RecordingGpuDevice();
|
||||||
|
using var manager = new ObjectMeshManager(
|
||||||
|
new VulkanMeshPipelineDevice(device.Retirement),
|
||||||
|
device,
|
||||||
|
new NullPreparedAssetSource(),
|
||||||
|
NullLogger<ObjectMeshManager>.Instance);
|
||||||
|
var adapter = (WbMeshAdapter)RuntimeHelpers.GetUninitializedObject(
|
||||||
|
typeof(WbMeshAdapter));
|
||||||
|
typeof(WbMeshAdapter).GetField(
|
||||||
|
"_meshManager",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.SetValue(adapter, manager);
|
||||||
|
var frames = new GpuDeviceFrameLifetime(device);
|
||||||
|
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||||
|
using var renderer = new ParticleRenderer(
|
||||||
|
device,
|
||||||
|
frames,
|
||||||
|
scope,
|
||||||
|
new ParticleSystem(new EmitterDescRegistry(), new Random(42)),
|
||||||
|
meshAdapter: adapter);
|
||||||
|
MethodInfo select = typeof(ParticleRenderer).GetMethod(
|
||||||
|
"PipelineForMeshBlend",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
|
||||||
|
var selected = (IGpuPipeline)select.Invoke(
|
||||||
|
renderer,
|
||||||
|
[TranslucencyKind.Opaque, true])!;
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
device.CreatedPipelines.Single(
|
||||||
|
static pipeline => pipeline.Description.Name == "particle-mesh-opaque"),
|
||||||
|
selected);
|
||||||
|
GpuPipelineDescription description = selected.Description;
|
||||||
|
Assert.Equal("particle_mesh", description.Shaders.Name);
|
||||||
|
Assert.Equal(GpuBlendMode.None, description.Blend);
|
||||||
|
Assert.True(description.Depth.Test);
|
||||||
|
Assert.True(description.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, description.Depth.Compare);
|
||||||
|
Assert.Equal(GpuCullMode.None, description.Cull);
|
||||||
|
Assert.Equal(GpuFrontFace.Clockwise, description.FrontFace);
|
||||||
|
Assert.False(description.AlphaToCoverage);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A billboard submission always carries the alpha-family mask
|
/// <summary>A billboard submission always carries the alpha-family mask
|
||||||
|
|
@ -73,4 +187,114 @@ public sealed class ParticleRendererRouteTests
|
||||||
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
|
||||||
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionDispatch_WarmedAppendAndImmediatePathsDoNotAllocate()
|
||||||
|
{
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
var sink = new NoAllocParticleSink();
|
||||||
|
ParticleRenderer.ReserveDeferredParticleDraw reserve = sink.Reserve;
|
||||||
|
ParticleRenderer.DrawImmediateParticle immediate = sink.DrawImmediate;
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
|
RunPair(queue, sink, reserve, immediate);
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 1_000; i++)
|
||||||
|
RunPair(queue, sink, reserve, immediate);
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunPair(
|
||||||
|
RetailAlphaQueue queue,
|
||||||
|
NoAllocParticleSink sink,
|
||||||
|
ParticleRenderer.ReserveDeferredParticleDraw reserve,
|
||||||
|
ParticleRenderer.DrawImmediateParticle immediate)
|
||||||
|
{
|
||||||
|
queue.BeginFrame();
|
||||||
|
ParticleRenderer.DeferToRetailAlphaQueue(
|
||||||
|
ParticleSubmissionKind.Mesh,
|
||||||
|
TranslucencyKind.Opaque,
|
||||||
|
0x80FFFFFFu,
|
||||||
|
queue,
|
||||||
|
sink,
|
||||||
|
reserve,
|
||||||
|
immediate,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
drawIndex: 0);
|
||||||
|
ParticleRenderer.DeferToRetailAlphaQueue(
|
||||||
|
ParticleSubmissionKind.Mesh,
|
||||||
|
TranslucencyKind.Opaque,
|
||||||
|
0xFFFFFFFFu,
|
||||||
|
queue,
|
||||||
|
sink,
|
||||||
|
reserve,
|
||||||
|
immediate,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
drawIndex: 1);
|
||||||
|
queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingSource : IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
public int PrepareCount { get; private set; }
|
||||||
|
public int DrawCount { get; private set; }
|
||||||
|
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens) => PrepareCount++;
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) =>
|
||||||
|
DrawCount += drawCount;
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoAllocParticleSink : IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
private int _nextToken;
|
||||||
|
|
||||||
|
public int Reserve() => _nextToken++;
|
||||||
|
|
||||||
|
public void DrawImmediate(
|
||||||
|
Matrix4x4 viewProjection,
|
||||||
|
ParticleSubmissionKind kind,
|
||||||
|
int drawIndex,
|
||||||
|
bool opaqueDepthState)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions() => _nextToken = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
|
{
|
||||||
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
|
||||||
|
public CacheStats DecodedTextureCacheStats => default;
|
||||||
|
|
||||||
|
public PreparedAssetPresence Probe(
|
||||||
|
AcDream.Content.Pak.PakAssetType type,
|
||||||
|
uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Missing;
|
||||||
|
|
||||||
|
public PreparedAssetReadResult Read(
|
||||||
|
in PreparedAssetRequest request,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedAssetReadResult.Missing;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -289,11 +289,77 @@ public sealed class RetailAlphaQueueTests
|
||||||
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.PendingCount);
|
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.PendingCount);
|
||||||
|
|
||||||
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
|
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
|
||||||
// The dropped 3001st token (3000) never reaches the source at all —
|
// The dropped 3001st token (3000) is never prepared or drawn — only
|
||||||
// only the 3000 accepted entries drew.
|
// the 3000 accepted entries draw. Its source still participates in
|
||||||
|
// cleanup; the two-source pin below discriminates that case.
|
||||||
Assert.Equal(RetailAlphaQueue.ListCapacity, source.LastDrawCount);
|
Assert.Equal(RetailAlphaQueue.ListCapacity, source.LastDrawCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 final fix R2-3: production callers reserve source-owned payload
|
||||||
|
/// before <c>TryAppend</c>. If source A already fills ALPHA, source B's
|
||||||
|
/// FIRST append is rejected but B must still be registered for cleanup.
|
||||||
|
/// It is never prepared/drawn, is reset exactly once by every terminal
|
||||||
|
/// path, and starts the next frame empty. Mutation check: moving
|
||||||
|
/// <c>RegisterSource(source)</c> below the capacity return makes the first
|
||||||
|
/// <c>Assert.Equal(1, rejected.ResetCount)</c> fail with actual 0.</summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("flush")]
|
||||||
|
[InlineData("end")]
|
||||||
|
[InlineData("abort")]
|
||||||
|
public void RejectedFirstUseSource_IsCleanedWithoutPrepareOrDraw(string terminal)
|
||||||
|
{
|
||||||
|
var filling = new CountingSource();
|
||||||
|
var rejected = new RetainedPayloadSource();
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
|
||||||
|
queue.BeginFrame();
|
||||||
|
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
|
||||||
|
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, filling, i, false));
|
||||||
|
|
||||||
|
int rejectedToken = rejected.Reserve(91);
|
||||||
|
Assert.False(queue.TryAppend(
|
||||||
|
RetailAlphaList.Alpha,
|
||||||
|
rejected,
|
||||||
|
rejectedToken,
|
||||||
|
overrideClipmap: false));
|
||||||
|
|
||||||
|
switch (terminal)
|
||||||
|
{
|
||||||
|
case "flush":
|
||||||
|
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||||
|
queue.AbortFrame();
|
||||||
|
break;
|
||||||
|
case "end":
|
||||||
|
queue.EndFrame();
|
||||||
|
break;
|
||||||
|
case "abort":
|
||||||
|
queue.AbortFrame();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(terminal));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(0, rejected.PrepareCount);
|
||||||
|
Assert.Equal(0, rejected.DrawCount);
|
||||||
|
Assert.Equal(1, rejected.ResetCount);
|
||||||
|
Assert.Equal(0, rejected.PendingCount);
|
||||||
|
|
||||||
|
queue.BeginFrame();
|
||||||
|
int acceptedToken = rejected.Reserve(92);
|
||||||
|
Assert.True(queue.TryAppend(
|
||||||
|
RetailAlphaList.Alpha,
|
||||||
|
rejected,
|
||||||
|
acceptedToken,
|
||||||
|
overrideClipmap: false));
|
||||||
|
queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(1, rejected.PrepareCount);
|
||||||
|
Assert.Equal(1, rejected.DrawCount);
|
||||||
|
Assert.Equal(2, rejected.ResetCount);
|
||||||
|
Assert.Equal(0, rejected.PendingCount);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The ALPHA list has its own independent capacity — filling
|
/// <summary>The ALPHA list has its own independent capacity — filling
|
||||||
/// CLIP to capacity must not affect ALPHA appends.</summary>
|
/// CLIP to capacity must not affect ALPHA appends.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -513,6 +579,45 @@ public sealed class RetailAlphaQueueTests
|
||||||
public void ResetAlphaSubmissions() => ResetCount++;
|
public void ResetAlphaSubmissions() => ResetCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RetainedPayloadSource : IRetailAlphaDrawSource
|
||||||
|
{
|
||||||
|
private readonly List<int> _pending = new();
|
||||||
|
private int[] _prepared = [];
|
||||||
|
|
||||||
|
public int PrepareCount { get; private set; }
|
||||||
|
public int DrawCount { get; private set; }
|
||||||
|
public int ResetCount { get; private set; }
|
||||||
|
public int PendingCount => _pending.Count;
|
||||||
|
|
||||||
|
public int Reserve(int value)
|
||||||
|
{
|
||||||
|
int token = _pending.Count;
|
||||||
|
_pending.Add(value);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
|
{
|
||||||
|
PrepareCount++;
|
||||||
|
_prepared = new int[tokens.Length];
|
||||||
|
for (int i = 0; i < tokens.Length; i++)
|
||||||
|
_prepared[i] = _pending[tokens[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
{
|
||||||
|
Assert.InRange(firstPreparedDraw, 0, _prepared.Length - drawCount);
|
||||||
|
DrawCount += drawCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAlphaSubmissions()
|
||||||
|
{
|
||||||
|
ResetCount++;
|
||||||
|
_pending.Clear();
|
||||||
|
_prepared = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FailureSource(
|
private sealed class FailureSource(
|
||||||
string? drawFailure,
|
string? drawFailure,
|
||||||
string? resetFailure) : IRetailAlphaDrawSource
|
string? resetFailure) : IRetailAlphaDrawSource
|
||||||
|
|
|
||||||
|
|
@ -160,23 +160,17 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 fix round 1 (M6, contract C4): a cell's transparent shell's
|
/// S4-c2 fix round 2 (M6): supplemental structure pin linking the real
|
||||||
/// immediate-vs-append decision must come from the SAME
|
/// cell submitter to the retained-batch scan and the functional production
|
||||||
/// <see cref="RetailAlphaMeshRouter"/> table every other alpha submitter
|
/// dispatcher proved by <c>EnvCellAlphaDrawSourceTests</c>. The per-subset
|
||||||
/// uses — never a hand-coded <c>if (detailEnabled)</c> branch that
|
/// <see cref="RetailAlphaMeshRouter"/> calls now correctly live inside
|
||||||
/// bypasses it. Pins the STRUCTURE: <see cref="EnvCellRenderer.TransparentDetailEnabled"/>
|
/// <see cref="EnvCellRenderer.GetTransparentRoutes"/> (one call per real
|
||||||
/// is read to feed <see cref="RetailAlphaMeshRouter.Route"/>'s own
|
/// retained batch), not here once per whole cell. Mutation check: bypassing
|
||||||
/// <c>detailSurfaceActive</c> argument BEFORE the route call, and the
|
/// that scan for a hand-coded cell-wide mask removes the required call and
|
||||||
/// route call precedes BOTH possible outcomes
|
/// fails before the dispatch-order assertion.
|
||||||
/// (<see cref="EnvCellRenderer.RenderTransparentOrdered"/> for
|
|
||||||
/// Immediate, <see cref="RetailAlphaQueue.TryAppend"/> for Append).
|
|
||||||
/// Mutation check: deleting the <c>Route</c> call and branching directly
|
|
||||||
/// on <c>TransparentDetailEnabled</c> instead removes the
|
|
||||||
/// <see cref="RetailAlphaMeshRouter"/> call entirely — <c>RequiredCallIndex</c>
|
|
||||||
/// fails its <c>index >= 0</c> assertion for the route call.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SubmitOrDrawTransparentCellShell_RoutesThroughTheSharedTableBeforeEitherOutcome()
|
public void SubmitOrDrawTransparentCellShell_ScansRetainedBatchesBeforeProductionDispatch()
|
||||||
{
|
{
|
||||||
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
||||||
"SubmitOrDrawTransparentCellShell",
|
"SubmitOrDrawTransparentCellShell",
|
||||||
|
|
@ -185,16 +179,13 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
|
|
||||||
int detailProbe = RequiredCallIndex(
|
int detailProbe = RequiredCallIndex(
|
||||||
calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled");
|
calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled");
|
||||||
int route = RequiredCallIndex(
|
int scan = RequiredCallIndex(
|
||||||
calls, typeof(RetailAlphaMeshRouter), nameof(RetailAlphaMeshRouter.Route));
|
calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.GetTransparentRoutes));
|
||||||
int renderImmediate = RequiredCallIndex(
|
int dispatch = RequiredCallIndex(
|
||||||
calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.RenderTransparentOrdered));
|
calls, typeof(RetailPViewPassExecutor), nameof(RetailPViewPassExecutor.DispatchTransparentCellShell));
|
||||||
int append = RequiredCallIndex(
|
|
||||||
calls, typeof(RetailAlphaQueue), nameof(RetailAlphaQueue.TryAppend));
|
|
||||||
|
|
||||||
Assert.True(detailProbe < route);
|
Assert.True(detailProbe < scan);
|
||||||
Assert.True(route < renderImmediate);
|
Assert.True(scan < dispatch);
|
||||||
Assert.True(route < append);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -89,15 +89,15 @@ public static class WalkAlphaDepthTrace
|
||||||
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
|
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
|
||||||
// RetailAlphaList's own doc comment. "new" is retail's per-entry
|
// RetailAlphaList's own doc comment. "new" is retail's per-entry
|
||||||
// first-for-list flag (param_4); "clip" (param_5, unused here)
|
// first-for-list flag (param_4); "clip" (param_5, unused here)
|
||||||
// is overrideClipmap. S4-c2 fix round 1 (M4/A6): IsNew is parsed
|
// is overrideClipmap. S4-c2 fix round 2: DrawMesh sets one flag
|
||||||
// and RETAINED purely as a captured/parsed FACT (fidelity to the
|
// per list at entry and clears that list's flag after its first
|
||||||
// capture's own format) but is never compared against acdream's
|
// successful append; later subsets in the same call can therefore
|
||||||
// own routing — M4 established that flag is trivially true for
|
// carry new=0 (the capture has 9,685 true and 871 false). IsNew is
|
||||||
// EVERY subset retail appends (one DrawMesh invocation owns one
|
// retained as a capture fact but is not compared to acdream: its
|
||||||
// subset per list), so a per-subset comparison would be
|
// retained Wb/particle/EnvCell submission units are not one-to-one
|
||||||
// meaningless at acdream's coarser per-INSTANCE granularity (the
|
// DrawMesh invocations, and Vulkan has no consumer for retail's
|
||||||
// same content-volume mismatch M5's KnownFailure count gate
|
// material-capture optimization bit. The KnownFailure count gate
|
||||||
// documents) — no dead-parse-as-evidence claim is made here.
|
// below remains about zero replay content, not this parsed field.
|
||||||
Match am = AmPattern.Match(line);
|
Match am = AmPattern.Match(line);
|
||||||
if (am.Success)
|
if (am.Success)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -594,6 +594,151 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
fx.AlphaQueue.AbortFrame();
|
fx.AlphaQueue.AbortFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 final M3 production pin for <c>SubmitWalkAlphaInstance</c>:
|
||||||
|
/// building shell + detail ON executes the immediate base+detail draws at
|
||||||
|
/// the submit mark and queues nothing; detail OFF queues, then drains the
|
||||||
|
/// base draw without a detail pass; an ordinary batch remains deferred
|
||||||
|
/// even while the global detail option is on. Mutation check: hardcoding
|
||||||
|
/// <c>detailSurfaceActive=false</c> at the production submit makes the
|
||||||
|
/// first case's pending-count assertion fail with 1.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void SubmitWalkAlphaInstance_DetailOnOffAndOrdinaryUseProductionOutcomes()
|
||||||
|
{
|
||||||
|
using (var on = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: true))
|
||||||
|
using (DrawScope draw = on.BeginDraw(beginAlpha: true))
|
||||||
|
{
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
|
||||||
|
on.Dispatcher.SubmitWalkAlphaInstance(
|
||||||
|
in building,
|
||||||
|
Vector3.Zero,
|
||||||
|
Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Equal(0, on.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Contains(
|
||||||
|
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName.Contains("alpha", StringComparison.Ordinal)
|
||||||
|
&& !call.PipelineName.Contains("detail", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(
|
||||||
|
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var off = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: false))
|
||||||
|
using (DrawScope draw = off.BeginDraw(beginAlpha: true))
|
||||||
|
{
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
|
||||||
|
off.Dispatcher.SubmitWalkAlphaInstance(
|
||||||
|
in building,
|
||||||
|
Vector3.Zero,
|
||||||
|
Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Equal(1, off.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Empty(off.Device.Calls.OfType<GpuRecordedPipelineBind>());
|
||||||
|
off.AlphaQueue.EndFrame();
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
off.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName.Contains("retail-detail", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var ordinary = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: true))
|
||||||
|
using (DrawScope draw = ordinary.BeginDraw(beginAlpha: true))
|
||||||
|
{
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch batch = AlphaWalkBatch(detailCategory: 0u);
|
||||||
|
ordinary.Dispatcher.SubmitWalkAlphaInstance(
|
||||||
|
in batch,
|
||||||
|
Vector3.Zero,
|
||||||
|
Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Equal(1, ordinary.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Empty(ordinary.Device.Calls.OfType<GpuRecordedPipelineBind>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The sibling grouped-production site executes the same detail-on row 1,
|
||||||
|
/// not merely the walk submit site. This invokes the real private
|
||||||
|
/// <c>DeferTransparentGroups</c> after seeding its real retained group
|
||||||
|
/// scratch. The same hardcoded-false mutation leaves one queued token and
|
||||||
|
/// makes the pending-count assertion fail.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void DeferTransparentGroups_BuildingDetailOnDrawsImmediateWithDetail()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: true);
|
||||||
|
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||||
|
WbDrawDispatcher.InstanceGroup group = AlphaInstanceGroup(detailCategory: 1u);
|
||||||
|
FieldInfo field = typeof(WbDrawDispatcher).GetField(
|
||||||
|
"_translucentDraws",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
var groups = (List<WbDrawDispatcher.InstanceGroup>)field.GetValue(fx.Dispatcher)!;
|
||||||
|
groups.Add(group);
|
||||||
|
MethodInfo defer = typeof(WbDrawDispatcher).GetMethod(
|
||||||
|
"DeferTransparentGroups",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
|
||||||
|
defer.Invoke(fx.Dispatcher, [Vector3.Zero, Matrix4x4.Identity]);
|
||||||
|
|
||||||
|
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Contains(
|
||||||
|
fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(uint detailCategory) =>
|
||||||
|
new(
|
||||||
|
new GroupKey(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
new GpuTextureSlot(1),
|
||||||
|
0,
|
||||||
|
TranslucencyKind.AlphaBlend,
|
||||||
|
FoliageFlags: 0),
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
ClipSlot: 0,
|
||||||
|
WbDrawDispatcher.InstanceLightSet.Disabled,
|
||||||
|
IndoorFlag: 0,
|
||||||
|
Alpha: 1f,
|
||||||
|
SelectionLighting: Vector2.Zero,
|
||||||
|
DetailCategory: detailCategory,
|
||||||
|
IsOpaque: false,
|
||||||
|
LocalSortCenter: Vector3.Zero);
|
||||||
|
|
||||||
|
private static WbDrawDispatcher.InstanceGroup AlphaInstanceGroup(uint detailCategory)
|
||||||
|
{
|
||||||
|
var group = new WbDrawDispatcher.InstanceGroup
|
||||||
|
{
|
||||||
|
FirstIndex = 0,
|
||||||
|
BaseVertex = 0,
|
||||||
|
IndexCount = 3,
|
||||||
|
TextureSlot = new GpuTextureSlot(1),
|
||||||
|
TextureLayer = 0,
|
||||||
|
Translucency = TranslucencyKind.AlphaBlend,
|
||||||
|
};
|
||||||
|
group.Matrices.Add(Matrix4x4.Identity);
|
||||||
|
group.LocalSortCenters.Add(Vector3.Zero);
|
||||||
|
group.SubmissionOrders.Add(0);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
||||||
{
|
{
|
||||||
|
|
@ -685,7 +830,9 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
|
|
||||||
public DispatcherFixture(
|
public DispatcherFixture(
|
||||||
bool withAlphaQueue = false,
|
bool withAlphaQueue = false,
|
||||||
IRetailSelectionRenderSink? selectionSink = null)
|
IRetailSelectionRenderSink? selectionSink = null,
|
||||||
|
bool detailAvailable = false,
|
||||||
|
bool detailEnabled = false)
|
||||||
{
|
{
|
||||||
Device = new RecordingGpuDevice();
|
Device = new RecordingGpuDevice();
|
||||||
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
||||||
|
|
@ -712,7 +859,17 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
new EntityClassificationCache(),
|
new EntityClassificationCache(),
|
||||||
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
||||||
selectionSink: selectionSink,
|
selectionSink: selectionSink,
|
||||||
alphaQueue: AlphaQueue);
|
alphaQueue: AlphaQueue,
|
||||||
|
buildingDetail: detailAvailable
|
||||||
|
? new TerrainAtlas.RetailDetailTextureBinding(
|
||||||
|
new GpuTextureSlot(99),
|
||||||
|
Tiling: 2f,
|
||||||
|
SurfaceTextureId: 1,
|
||||||
|
RenderSurfaceId: 2,
|
||||||
|
Width: 4,
|
||||||
|
Height: 4)
|
||||||
|
: default,
|
||||||
|
buildingDetailEnabled: detailEnabled ? DetailOn : DetailOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecordingGpuDevice Device { get; }
|
public RecordingGpuDevice Device { get; }
|
||||||
|
|
@ -727,8 +884,13 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
|
|
||||||
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
||||||
|
|
||||||
public DrawScope BeginDraw()
|
public DrawScope BeginDraw(bool beginAlpha = false)
|
||||||
{
|
{
|
||||||
|
if (beginAlpha)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginFrame(frameSlot: 0);
|
||||||
|
AlphaQueue!.BeginFrame();
|
||||||
|
}
|
||||||
FrameLifetime.BeginFrame();
|
FrameLifetime.BeginFrame();
|
||||||
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
||||||
IGpuPassEncoder pass = frame.BeginPass(
|
IGpuPassEncoder pass = frame.BeginPass(
|
||||||
|
|
@ -739,6 +901,10 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
return new DrawScope(frame, pass, publication);
|
return new DrawScope(frame, pass, publication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool DetailOn() => true;
|
||||||
|
|
||||||
|
private static bool DetailOff() => false;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispatcher.Dispose();
|
Dispatcher.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
using AcDream.App.Rendering.Packs;
|
using AcDream.App.Rendering.Packs;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.World;
|
using AcDream.Runtime.World;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Lib.IO;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
|
@ -513,6 +527,38 @@ public sealed class WorldSceneRendererTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c2 fix round 2 (A1): this is the positive, concrete owner-path
|
||||||
|
/// proof. It opens the real recording GPU world scope, executes
|
||||||
|
/// <see cref="RetailPViewRenderer.DrawInside(RetailPViewFrameInput,
|
||||||
|
/// RetailPViewPassExecutor)"/> for an outdoor root containing one real
|
||||||
|
/// walk building, then lets <see cref="WorldSceneRenderer"/> close the
|
||||||
|
/// SAME <see cref="RetailAlphaQueue"/> through <c>EndFrame</c>. Only
|
||||||
|
/// drains that pass retail's threshold gate are observed. The production
|
||||||
|
/// order is therefore the building's own zero-threshold barrier followed
|
||||||
|
/// by the final RenderNormalMode drain; an outdoor LandscapeFlush is
|
||||||
|
/// forbidden. Restoring the removed outdoor
|
||||||
|
/// <c>passes.FlushLandscapeAlpha()</c> call inserts LandscapeFlush between
|
||||||
|
/// those two entries. Moving/deleting <c>_alpha.EndFrame()</c> removes or
|
||||||
|
/// reorders RenderNormalMode. The IL pin in
|
||||||
|
/// <c>RetailPViewRendererOutdoorFlushTests</c> remains secondary only.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void OutdoorProductionPView_DrainsBuildingThenRenderNormalModeWithoutLandscapeFlush()
|
||||||
|
{
|
||||||
|
var drains = new List<RetailAlphaFlushSite>();
|
||||||
|
using var fixture = new OutdoorAlphaOwnerFixture(drains.Add);
|
||||||
|
|
||||||
|
WorldRenderFrameOutcome outcome = fixture.Renderer.Render(default);
|
||||||
|
|
||||||
|
Assert.True(outcome.NormalWorldDrawn);
|
||||||
|
Assert.Equal(1, fixture.PView.DrawCount);
|
||||||
|
Assert.Equal(
|
||||||
|
[RetailAlphaFlushSite.DrawBuilding, RetailAlphaFlushSite.RenderNormalMode],
|
||||||
|
drains);
|
||||||
|
Assert.DoesNotContain(RetailAlphaFlushSite.LandscapeFlush, drains);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class Rig
|
private sealed class Rig
|
||||||
{
|
{
|
||||||
public Rig(
|
public Rig(
|
||||||
|
|
@ -603,6 +649,241 @@ public sealed class WorldSceneRendererTests
|
||||||
public WorldSceneRenderer Renderer { get; }
|
public WorldSceneRenderer Renderer { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class OutdoorAlphaOwnerFixture : IDisposable
|
||||||
|
{
|
||||||
|
private readonly RecordingGpuDevice _device;
|
||||||
|
private readonly TextureCache _textures;
|
||||||
|
private readonly WbMeshAdapter _meshAdapter;
|
||||||
|
private readonly WbDrawDispatcher _dispatcher;
|
||||||
|
private readonly EnvCellRenderer _envCells;
|
||||||
|
private readonly ClipFrame _clipFrame;
|
||||||
|
private readonly RenderSceneShadowRuntime _renderScene;
|
||||||
|
private readonly IGpuPassEncoder _pass;
|
||||||
|
private readonly IDisposable _scopePublication;
|
||||||
|
|
||||||
|
public OutdoorAlphaOwnerFixture(Action<RetailAlphaFlushSite> observeDrain)
|
||||||
|
{
|
||||||
|
_device = new RecordingGpuDevice();
|
||||||
|
var frames = new GpuDeviceFrameLifetime(_device);
|
||||||
|
var scope = new FixedWorldPassScope();
|
||||||
|
var dat = new NoopDatReaderWriter();
|
||||||
|
var prepared = new NullPreparedAssetSource();
|
||||||
|
_textures = new TextureCache(_device, dat);
|
||||||
|
_meshAdapter = new WbMeshAdapter(
|
||||||
|
_device,
|
||||||
|
dat,
|
||||||
|
prepared,
|
||||||
|
NullLogger<WbMeshAdapter>.Instance,
|
||||||
|
_device.Retirement);
|
||||||
|
var spawns = new EntitySpawnAdapter(
|
||||||
|
_textures,
|
||||||
|
_ => throw new NotSupportedException("The empty owner-path fixture never spawns entities."));
|
||||||
|
var alpha = new RetailAlphaQueue(drainObserver: observeDrain);
|
||||||
|
_dispatcher = new WbDrawDispatcher(
|
||||||
|
_device,
|
||||||
|
frames,
|
||||||
|
scope,
|
||||||
|
_textures,
|
||||||
|
_meshAdapter,
|
||||||
|
spawns,
|
||||||
|
new EntityClassificationCache(),
|
||||||
|
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
||||||
|
alphaQueue: alpha);
|
||||||
|
_envCells = new EnvCellRenderer(
|
||||||
|
_device,
|
||||||
|
frames,
|
||||||
|
scope,
|
||||||
|
_meshAdapter.MeshManager!,
|
||||||
|
new WbFrustum());
|
||||||
|
_clipFrame = ClipFrame.NoClip();
|
||||||
|
|
||||||
|
var glDiagnostics = new WorldRenderDiagnostics(
|
||||||
|
new DefaultGlStateReader(),
|
||||||
|
new NullDiagnosticLog());
|
||||||
|
var terrainDiagnostics = new TerrainDrawDiagnosticsController(
|
||||||
|
enabled: false,
|
||||||
|
glDiagnostics,
|
||||||
|
new EmptyFrameFacts(),
|
||||||
|
new NullDiagnosticLog());
|
||||||
|
var pviewPasses = new RetailPViewPassExecutor(
|
||||||
|
new NullWorldPassSurface(),
|
||||||
|
NullRenderFrameGlState.Instance,
|
||||||
|
_clipFrame,
|
||||||
|
terrain: null,
|
||||||
|
_envCells,
|
||||||
|
_dispatcher,
|
||||||
|
sky: null,
|
||||||
|
particles: null,
|
||||||
|
particleRenderer: null,
|
||||||
|
portalDepthMask: null,
|
||||||
|
alpha,
|
||||||
|
glDiagnostics,
|
||||||
|
terrainDiagnostics);
|
||||||
|
|
||||||
|
const uint landblockId = 0xF4180000u;
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = landblockId | 1u,
|
||||||
|
DrawingBsp = new WalkBspNode { InPortals = [] },
|
||||||
|
};
|
||||||
|
var entry = new WalkBuildingFactory.Entry(
|
||||||
|
building,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
Matrix4x4.Identity);
|
||||||
|
var buildings = new WalkBuildingRegistry();
|
||||||
|
buildings.Publish(landblockId, [entry]);
|
||||||
|
var landscape = new WalkLandscapeAssembler();
|
||||||
|
landscape.PublishLandblock(landblockId, maxZ: 10f, minZ: -1f, [entry]);
|
||||||
|
var cells = new CellVisibility();
|
||||||
|
_renderScene = new RenderSceneShadowRuntime(RenderSceneGeneration.FromRaw(1));
|
||||||
|
var retailPView = new RetailPViewRenderer(
|
||||||
|
_renderScene,
|
||||||
|
buildings,
|
||||||
|
landscape,
|
||||||
|
cells,
|
||||||
|
new ShadowObjectRegistry());
|
||||||
|
PView = new CountingPView(new WorldScenePViewRenderer(retailPView, pviewPasses));
|
||||||
|
|
||||||
|
frames.BeginFrame();
|
||||||
|
IGpuFrame frame = frames.CurrentFrame!;
|
||||||
|
_pass = frame.BeginPass(
|
||||||
|
GpuPassDescription.BackbufferClear(
|
||||||
|
"s4-c2-outdoor-owner-path",
|
||||||
|
Vector4.Zero,
|
||||||
|
sampleCount: 1));
|
||||||
|
_scopePublication = scope.Publish(_pass);
|
||||||
|
_dispatcher.BeginFrame(frameSlot: 0);
|
||||||
|
_envCells.BeginFrame(frameSlot: 0);
|
||||||
|
|
||||||
|
var calls = new List<string>();
|
||||||
|
var root = new LoadedCell
|
||||||
|
{
|
||||||
|
CellId = landblockId | 1u,
|
||||||
|
IsOutdoorNode = true,
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
};
|
||||||
|
var day = new DayGroupData { Name = "s4-c2-owner" };
|
||||||
|
var foundation = new RenderFrameFoundation(
|
||||||
|
PortalViewportVisible: false,
|
||||||
|
Sky: default,
|
||||||
|
Atmosphere: default);
|
||||||
|
Renderer = new WorldSceneRenderer(
|
||||||
|
new FoundationSource(foundation),
|
||||||
|
new LoginSource(false),
|
||||||
|
new SkySource(day, dayFraction: 0f),
|
||||||
|
new FrameBuilder(calls, CreateFrame(root, playerSeenOutside: true)),
|
||||||
|
new EntitySource(),
|
||||||
|
selection: null,
|
||||||
|
alpha,
|
||||||
|
new ParticleVisibility(calls),
|
||||||
|
PView,
|
||||||
|
new PViewCells(),
|
||||||
|
new PassExecutor(calls),
|
||||||
|
new WorldRenderRangeState(nearRadius: 4, farRadius: 12),
|
||||||
|
new Diagnostics(calls));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountingPView PView { get; }
|
||||||
|
|
||||||
|
public WorldSceneRenderer Renderer { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_scopePublication.Dispose();
|
||||||
|
_pass.Dispose();
|
||||||
|
_envCells.Dispose();
|
||||||
|
_dispatcher.Dispose();
|
||||||
|
_meshAdapter.Dispose();
|
||||||
|
_textures.Dispose();
|
||||||
|
_renderScene.Dispose();
|
||||||
|
_clipFrame.Dispose();
|
||||||
|
_device.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CountingPView(IWorldScenePViewRenderer inner) : IWorldScenePViewRenderer
|
||||||
|
{
|
||||||
|
public int DrawCount { get; private set; }
|
||||||
|
|
||||||
|
public RetailPViewFrameResult DrawInside(RetailPViewFrameInput input)
|
||||||
|
{
|
||||||
|
DrawCount++;
|
||||||
|
return inner.DrawInside(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AbortFrame() => inner.AbortFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullWorldPassSurface : IWorldPassSurface
|
||||||
|
{
|
||||||
|
public void PrepareClipFrame() { }
|
||||||
|
|
||||||
|
public void EnableClipDistances() { }
|
||||||
|
|
||||||
|
public void DisableClipDistances() { }
|
||||||
|
|
||||||
|
public void ClearInteriorDepth() =>
|
||||||
|
throw new InvalidOperationException("An outdoor root cannot clear interior depth.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FixedWorldPassScope : IWorldPassScope
|
||||||
|
{
|
||||||
|
private IGpuPassEncoder? _encoder;
|
||||||
|
|
||||||
|
public int SampleCount => 1;
|
||||||
|
|
||||||
|
public IGpuPassEncoder? CurrentEncoder => _encoder;
|
||||||
|
|
||||||
|
public int AttachmentWidth => 1024;
|
||||||
|
|
||||||
|
public int AttachmentHeight => 720;
|
||||||
|
|
||||||
|
public WorldFrameSections Sections { get; } = new();
|
||||||
|
|
||||||
|
public IGpuPassEncoder RequireEncoder() =>
|
||||||
|
_encoder ?? throw new InvalidOperationException("No recording world pass is open.");
|
||||||
|
|
||||||
|
public void ClearInteriorDepth() =>
|
||||||
|
throw new InvalidOperationException("An outdoor root cannot clear interior depth.");
|
||||||
|
|
||||||
|
public IDisposable Publish(IGpuPassEncoder encoder)
|
||||||
|
{
|
||||||
|
Assert.Null(_encoder);
|
||||||
|
_encoder = encoder;
|
||||||
|
Sections.Reset();
|
||||||
|
return new Publication(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Publication(FixedWorldPassScope owner) : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
owner._encoder = null;
|
||||||
|
owner.Sections.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DefaultGlStateReader : IRenderGlStateReader
|
||||||
|
{
|
||||||
|
public RenderGlStateSnapshot CaptureState() => default;
|
||||||
|
|
||||||
|
public RenderGlScissorSnapshot CaptureScissor() => default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullDiagnosticLog : IRenderFrameDiagnosticLog
|
||||||
|
{
|
||||||
|
public void WriteLine(string message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EmptyFrameFacts : IFramePipelineDiagnosticFactsSource
|
||||||
|
{
|
||||||
|
public TerrainRenderDiagnosticFacts CaptureTerrain() => default;
|
||||||
|
|
||||||
|
public FramePipelineDiagnosticFacts CaptureFrame() => default;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FoundationSource(RenderFrameFoundation foundation) :
|
private sealed class FoundationSource(RenderFrameFoundation foundation) :
|
||||||
IRenderFrameFoundationSource
|
IRenderFrameFoundationSource
|
||||||
{
|
{
|
||||||
|
|
@ -909,6 +1190,137 @@ public sealed class WorldSceneRendererTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
|
{
|
||||||
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
|
||||||
|
public CacheStats DecodedTextureCacheStats => default;
|
||||||
|
|
||||||
|
public PreparedAssetPresence Probe(
|
||||||
|
AcDream.Content.Pak.PakAssetType type,
|
||||||
|
uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Missing;
|
||||||
|
|
||||||
|
public PreparedAssetReadResult Read(
|
||||||
|
in PreparedAssetRequest request,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedAssetReadResult.Missing;
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
||||||
|
{
|
||||||
|
private readonly StubDatabase _portal = new();
|
||||||
|
private readonly StubDatabase _highRes = new();
|
||||||
|
private readonly StubDatabase _language = new();
|
||||||
|
private readonly StubDatabase _cell = new();
|
||||||
|
|
||||||
|
public string SourceDirectory => string.Empty;
|
||||||
|
|
||||||
|
public IDatDatabase Portal => _portal;
|
||||||
|
|
||||||
|
public IDatDatabase Cell => _cell;
|
||||||
|
|
||||||
|
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
||||||
|
new(new Dictionary<uint, IDatDatabase>());
|
||||||
|
|
||||||
|
public IDatDatabase HighRes => _highRes;
|
||||||
|
|
||||||
|
public IDatDatabase Language => _language;
|
||||||
|
|
||||||
|
public IDatDatabase Local => _language;
|
||||||
|
|
||||||
|
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
|
||||||
|
new(new Dictionary<uint, uint>());
|
||||||
|
|
||||||
|
public int PortalIteration => 0;
|
||||||
|
|
||||||
|
public int CellIteration => 0;
|
||||||
|
|
||||||
|
public int HighResIteration => 0;
|
||||||
|
|
||||||
|
public int LanguageIteration => 0;
|
||||||
|
|
||||||
|
public bool TryGetFileBytes(
|
||||||
|
uint regionId,
|
||||||
|
uint fileId,
|
||||||
|
ref byte[] bytes,
|
||||||
|
out int bytesRead)
|
||||||
|
{
|
||||||
|
bytesRead = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||||
|
Array.Empty<uint>();
|
||||||
|
|
||||||
|
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
||||||
|
Array.Empty<IDatReaderWriter.IdResolution>();
|
||||||
|
|
||||||
|
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public bool TrySave<T>(
|
||||||
|
uint regionId,
|
||||||
|
T obj,
|
||||||
|
int iteration = 0) where T : IDBObj =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
[return: MaybeNull]
|
||||||
|
public T Get<T>(uint fileId) where T : IDBObj => default;
|
||||||
|
|
||||||
|
public bool TryGet<T>(
|
||||||
|
uint fileId,
|
||||||
|
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
|
||||||
|
private sealed class StubDatabase : IDatDatabase
|
||||||
|
{
|
||||||
|
public DatDatabase Db => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public int Iteration => 0;
|
||||||
|
|
||||||
|
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||||
|
Array.Empty<uint>();
|
||||||
|
|
||||||
|
public bool TryGet<T>(
|
||||||
|
uint fileId,
|
||||||
|
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetFileBytes(
|
||||||
|
uint fileId,
|
||||||
|
[MaybeNullWhen(false)] out byte[] value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetFileBytes(
|
||||||
|
uint fileId,
|
||||||
|
ref byte[] bytes,
|
||||||
|
out int bytesRead)
|
||||||
|
{
|
||||||
|
bytesRead = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static WorldRenderFrame CreateFrame(
|
private static WorldRenderFrame CreateFrame(
|
||||||
LoadedCell? clipRoot,
|
LoadedCell? clipRoot,
|
||||||
bool playerSeenOutside)
|
bool playerSeenOutside)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue