fix(rendering): repair EnvCell retail CLIP state
Exclude deferred EnvCell subsets from the opaque turn, preserve exact CLIP/ALPHA fixed-state groups through leaf replay, and use retail's row-3 override, blend, depth, and texture-class alpha references. Keep the existing building-detail sentinel distinct from the two CLIP references.
Bound rejected source payload, replace no-op allocation proofs with actual EnvCell and particle RHI paths, rebuild checked-in SPIR-V, and correct AP-238/AP-240 plus the S4-c2 evidence record.
Gates: Release 0W/0E; hermetic 16735/0/0; InstalledDat 255 pass/10 known fail/1 skip; shaders 32/32; focused 239/239; allocation 2/2 at 0 B. The evidence/comment repair re-ran Release 0W/0E, shaders 32/32, affected 37/37, and allocation 2/2; mesh_detail.vert.spv remained byte-identical at SHA-256 5346247ab7d606046943e19b28888c814e08dc6cb27cd9750096ac055457eb57.
Mutation proof, with each mutation restored after its named first failure:
1. Restoring the opaque predicate to !IsAdditive fails WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain first at draw count: expected 1, actual 3.
2. Selecting _alphaPipeline for CLIP fails WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference first at the bind sequence: expected [envcell-clip], actual [envcell-alpha].
3. Disabling CLIP depth write fails that production Theory first at Assert.True(clipPipeline.Depth.Write): expected true, actual false.
4. Swapping palette/DDS references fails the DDS row first: expected 0.784313738, actual 0.392156869; the palette row reports the inverse.
5. Mutating mesh_modern.frag from < to <= fails ClipShaders_UseGreaterEqualForThePerRangeReference first at Assert.Contains("if (color.a < alphaCutoff) discard;"): the required source spelling is absent.
6. Restoring row-3 OverrideClipmap=true fails the real-interface clip Theory first at Assert.False: expected false, actual true.
7. Deleting failed-append rollback fails the flush/end/abort rejection-storm rows first at the bounded pending-count assertion: expected 0, actual 9000.
8. Selecting _transparentDetailPipeline for a ClipMap detail contribution fails the leaf detail pin first at the bind sequence: expected second bind envcell-retail-detail-clip, actual envcell-retail-detail-alpha.
9. Resetting detail ParamB to zero fails the same detail pin first at the second pushed reference: expected 0.784313738, actual 0.
10. Classifying CLIP with exact mask equality excludes legal 0x09 and fails WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference first at pipeline: expected envcell-clip, actual envcell-alpha.
11. Mapping the new blend to SRC_ALPHA/INVSRCALPHA fails AllRetailBlendModesAreRepresentable first at the tuple: expected (One, OneMinusSrcAlpha), actual (SrcAlpha, OneMinusSrcAlpha).
12. Restoring mesh_detail.vert's uParamB > 0.5 category predicate fails ClipShaders_UseGreaterEqualForThePerRangeReference first because vDetailCategory = uParamB == 1.0 is absent.
13. Treating every positive detail uParamB as a cutoff fails that source pin first because isRetailClipReference(uParamB) ? uParamB : 0.05 is absent.
14. Adding arbitrary 0.5 as an accepted reference to either mesh_modern.frag or mesh_detail.frag fails that source pin first at Assert.DoesNotContain("value - 0.5"); both mutations were run and reversed independently.
Retail: D3DPolyRender::SetSurface @ 0x0059c4d0; paired binary @ 0x0059c72a, 0x0059c747/0x0059c74f, 0x0059c821, 0x0059c838, 0x0059c866.
This commit is contained in:
parent
0aa166aa09
commit
252886e84f
21 changed files with 812 additions and 277 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 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-238 | **S4-c2 (2026-09-04); NARROWED FURTHER by fix rounds 1/2 and the owner-authorized repair — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main`; ids are never reused).** `RetailAlphaQueue` is retail's independent CLIP/ALPHA FIFO pair. The repaired production leaf excludes every non-additive transparent EnvCell subset from its preceding opaque draw, retains each real batch's `RetailSurfaceMask`, and routes canonical `0x08` plus legal positive-stipple `0x09` to CLIP with row-3 `overrideClipmap=false`; `0x02` routes to ALPHA. EnvCell CLIP now uses `SetSurface`'s exact `ONE/INVSRCALPHA`, depth-test/write-on state and `GREATER_EQUAL` reference (100/255 paletted, 200/255 DDS); detail-active row 1 keeps that CLIP base/reference before its detail contribution. A mixed cell can contribute to both lists without cross-feeding. The only remaining residual is 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` (`BatchBelongsToPass`, retained-mask state grouping, 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/state 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; `D3DPolyRender::SetSurface` 0x0059C4D0 (paired binary: blend ids 2/6 @0x0059C72A, refs `0x64`/`0xC8` at globals 0x00820D8C/0x00820D90, func 7, depth-write byte 1 @0x0059C866); 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 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-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); 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-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2); narrowed at fix round 2 and the owner-authorized repair.** `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. CLIP has two genuine production feeder families outside this residual: clip-mapped mesh particles and exact-mask EnvCell subsets (canonical `F4180104` / surface `08000BFF`), with EnvCell now proven through the real leaf and exact `SetSurface` CLIP state. The residual is only ordinary GfxObj/scenery/building content; no ordinary-Wb A2C behavior changed in this repair. | `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 |
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,14 @@ internal enum GpuBlendMode
|
||||||
/// <summary>Straight alpha: <c>SrcAlpha, OneMinusSrcAlpha</c>.</summary>
|
/// <summary>Straight alpha: <c>SrcAlpha, OneMinusSrcAlpha</c>.</summary>
|
||||||
StraightAlpha,
|
StraightAlpha,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Premultiplied/source-over: <c>One, OneMinusSrcAlpha</c>. Retail pure
|
||||||
|
/// Base1ClipMap surfaces select exactly this pair in
|
||||||
|
/// <c>D3DPolyRender::SetSurface</c> @0x0059c72a when the per-entry
|
||||||
|
/// override-clipmap byte is clear.
|
||||||
|
/// </summary>
|
||||||
|
PremultipliedAlpha,
|
||||||
|
|
||||||
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
||||||
Additive,
|
Additive,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ internal static class VulkanViewportMapping
|
||||||
internal static (BlendFactor Source, BlendFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
|
internal static (BlendFactor Source, BlendFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
|
||||||
{
|
{
|
||||||
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||||
|
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
||||||
/// <c>BindlessSupport</c>: the five pipelines compile <c>particle</c> and
|
/// <c>BindlessSupport</c>: the six pipelines compile <c>particle</c> and
|
||||||
/// <c>particle_mesh</c> from the committed SPIR-V, and both texture sources
|
/// <c>particle_mesh</c> from the committed SPIR-V, and both texture sources
|
||||||
/// already hand out the device's own <c>GpuTextureSlot</c> (V4t).
|
/// already hand out the device's own <c>GpuTextureSlot</c> (V4t).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -267,9 +267,11 @@ public sealed unsafe partial class ParticleRenderer
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One mesh-particle pipeline. Same depth bracket as the billboards (see
|
/// One mesh-particle pipeline. Every variant uses retail's world
|
||||||
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the world
|
/// <c>GL_LESS</c> compare; delayed alpha variants match the billboard
|
||||||
/// <c>GL_LESS</c> citation); the winding is CW because
|
/// no-write bracket, while row 5's opaque variant depth-writes. See
|
||||||
|
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the compare
|
||||||
|
/// citation. The winding is CW because
|
||||||
/// <c>PrepareMeshPipeline</c> sets <c>glFrontFace(GL_CW)</c>, and the cull
|
/// <c>PrepareMeshPipeline</c> sets <c>glFrontFace(GL_CW)</c>, and the cull
|
||||||
/// mode stays DYNAMIC because it is resolved per sub-batch from the DAT's
|
/// mode stays DYNAMIC because it is resolved per sub-batch from the DAT's
|
||||||
/// own <c>CullMode</c>.
|
/// own <c>CullMode</c>.
|
||||||
|
|
|
||||||
|
|
@ -263,13 +263,31 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||||
{
|
{
|
||||||
int token = clipSource.AddPendingCellId(cellId);
|
int token = clipSource.AddPendingCellId(cellId);
|
||||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
// This fixed source is ordinary DrawMesh row 3: pure ClipMap,
|
||||||
|
// multipass false, therefore CLIP with overrideClipmap=false.
|
||||||
|
// If the queue drops at 3,000, retain no source-owned payload;
|
||||||
|
// TryAppend has already registered the source for exact reset.
|
||||||
|
if (!queue.TryAppend(
|
||||||
|
RetailAlphaList.Clip,
|
||||||
|
clipSource,
|
||||||
|
token,
|
||||||
|
overrideClipmap: false))
|
||||||
|
{
|
||||||
|
clipSource.RollbackPendingCellId(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||||
{
|
{
|
||||||
int token = alphaSource.AddPendingCellId(cellId);
|
int token = alphaSource.AddPendingCellId(cellId);
|
||||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
if (!queue.TryAppend(
|
||||||
|
RetailAlphaList.Alpha,
|
||||||
|
alphaSource,
|
||||||
|
token,
|
||||||
|
overrideClipmap: false))
|
||||||
|
{
|
||||||
|
alphaSource.RollbackPendingCellId(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,13 +315,27 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||||
{
|
{
|
||||||
int token = clipSource.AddPendingCellId(cellId);
|
int token = clipSource.AddPendingCellId(cellId);
|
||||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
if (!queue.TryAppend(
|
||||||
|
RetailAlphaList.Clip,
|
||||||
|
clipSource,
|
||||||
|
token,
|
||||||
|
overrideClipmap: false))
|
||||||
|
{
|
||||||
|
clipSource.RollbackPendingCellId(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||||
{
|
{
|
||||||
int token = alphaSource.AddPendingCellId(cellId);
|
int token = alphaSource.AddPendingCellId(cellId);
|
||||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
if (!queue.TryAppend(
|
||||||
|
RetailAlphaList.Alpha,
|
||||||
|
alphaSource,
|
||||||
|
token,
|
||||||
|
overrideClipmap: false))
|
||||||
|
{
|
||||||
|
alphaSource.RollbackPendingCellId(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,15 +359,26 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
EnvCellTransparentRoute route,
|
EnvCellTransparentRoute route,
|
||||||
bool detailSurfaceActive);
|
bool detailSurfaceActive);
|
||||||
|
|
||||||
internal sealed class EnvCellAlphaDrawSource(
|
internal sealed class EnvCellAlphaDrawSource : IRetailAlphaDrawSource
|
||||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
|
||||||
EnvCellTransparentRoute route)
|
|
||||||
: IRetailAlphaDrawSource
|
|
||||||
{
|
{
|
||||||
|
private readonly RenderEnvCellsByRoute _renderTransparentOrdered;
|
||||||
|
private readonly EnvCellTransparentRoute _route;
|
||||||
|
private readonly Action? _resetObserver;
|
||||||
private readonly List<uint> _pendingCellIds = new();
|
private readonly List<uint> _pendingCellIds = new();
|
||||||
private readonly List<uint> _preparedCellIds = new();
|
private readonly List<uint> _preparedCellIds = new();
|
||||||
private readonly List<uint> _drawScratch = new();
|
private readonly List<uint> _drawScratch = new();
|
||||||
|
|
||||||
|
internal EnvCellAlphaDrawSource(
|
||||||
|
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||||
|
EnvCellTransparentRoute route,
|
||||||
|
Action? resetObserver = null)
|
||||||
|
{
|
||||||
|
_renderTransparentOrdered = renderTransparentOrdered
|
||||||
|
?? throw new ArgumentNullException(nameof(renderTransparentOrdered));
|
||||||
|
_route = route;
|
||||||
|
_resetObserver = resetObserver;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns this cell's token — its index into
|
/// <summary>Returns this cell's token — its index into
|
||||||
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
|
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
|
||||||
internal int AddPendingCellId(uint cellId)
|
internal int AddPendingCellId(uint cellId)
|
||||||
|
|
@ -345,6 +388,22 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rolls back only the just-reserved tail token after queue rejection.
|
||||||
|
/// Accepted indices therefore never move, while a rejection storm can
|
||||||
|
/// neither grow the pending count nor force a second geometric growth
|
||||||
|
/// beyond the 3,000-entry list's warmed capacity.
|
||||||
|
/// </summary>
|
||||||
|
internal void RollbackPendingCellId(int token)
|
||||||
|
{
|
||||||
|
if (token != _pendingCellIds.Count - 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only the just-reserved EnvCell alpha token can be rolled back.");
|
||||||
|
}
|
||||||
|
_pendingCellIds.RemoveAt(token);
|
||||||
|
}
|
||||||
|
|
||||||
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||||
{
|
{
|
||||||
_preparedCellIds.Clear();
|
_preparedCellIds.Clear();
|
||||||
|
|
@ -359,7 +418,10 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
_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, route, detailSurfaceActive: false);
|
_renderTransparentOrdered(
|
||||||
|
_drawScratch,
|
||||||
|
_route,
|
||||||
|
detailSurfaceActive: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetAlphaSubmissions()
|
public void ResetAlphaSubmissions()
|
||||||
|
|
@ -367,9 +429,13 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
||||||
_pendingCellIds.Clear();
|
_pendingCellIds.Clear();
|
||||||
_preparedCellIds.Clear();
|
_preparedCellIds.Clear();
|
||||||
_drawScratch.Clear();
|
_drawScratch.Clear();
|
||||||
|
_resetObserver?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int PendingCount => _pendingCellIds.Count;
|
internal int PendingCount => _pendingCellIds.Count;
|
||||||
|
internal int PendingCapacity => _pendingCellIds.Capacity;
|
||||||
|
internal int PreparedCapacity => _preparedCellIds.Capacity;
|
||||||
|
internal int DrawCapacity => _drawScratch.Capacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ in flat uint vBatchFlags;
|
||||||
in flat uint vDetailCategory;
|
in flat uint vDetailCategory;
|
||||||
|
|
||||||
uniform uint uTextureIndexA; // category detail texture, layer 0
|
uniform uint uTextureIndexA; // category detail texture, layer 0
|
||||||
|
// S4-c2 F3: 100/255 or 200/255 for a pure ClipMap base/detail pair. Exact 1
|
||||||
|
// remains mesh_detail.vert's existing building-category sentinel and must use
|
||||||
|
// the ordinary empty-fragment cutoff. Zero is the ordinary EnvCell category.
|
||||||
|
uniform float uParamB;
|
||||||
|
|
||||||
// SceneLighting UBO — IDENTICAL layout to mesh_modern.frag binding=1 (same
|
// SceneLighting UBO — IDENTICAL layout to mesh_modern.frag binding=1 (same
|
||||||
// std140 block, same struct, same binding). Declared here ONLY for fog
|
// std140 block, same struct, same binding). Declared here ONLY for fog
|
||||||
|
|
@ -50,6 +54,11 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
bool isRetailClipReference(float value) {
|
||||||
|
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||||
|
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||||
|
}
|
||||||
|
|
||||||
// VM2 (2026-08-22, live cdb read on the PDB-paired retail client, GUID
|
// VM2 (2026-08-22, live cdb read on the PDB-paired retail client, GUID
|
||||||
// 9e847e2f-777c-4bd9-886c-22256bb87f32): retail's RenderDevice reports
|
// 9e847e2f-777c-4bd9-886c-22256bb87f32): retail's RenderDevice reports
|
||||||
// m_caps.bCanDoSinglePassDetailing = 1 and the file-static trysinglepass = 1,
|
// m_caps.bCanDoSinglePassDetailing = 1 and the file-static trysinglepass = 1,
|
||||||
|
|
@ -100,7 +109,8 @@ void main() {
|
||||||
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
||||||
vBaseTextureIndex,
|
vBaseTextureIndex,
|
||||||
vec3(vBaseUv, float(vBaseTextureLayer)));
|
vec3(vBaseUv, float(vBaseTextureLayer)));
|
||||||
if (base.a < 0.05)
|
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||||
|
if (base.a < alphaCutoff)
|
||||||
discard;
|
discard;
|
||||||
|
|
||||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||||||
uint instanceClipSlot[];
|
uint instanceClipSlot[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Object renderer only: 1 for a retail building shell, 0 for ordinary
|
// Exact uParamB=1 selects this per-instance building/object category. EnvCell
|
||||||
// scenery/creatures/players. EnvCellRenderer sets uParamB=0 and ignores the
|
// zero and its pure-ClipMap 100/255 or 200/255 references retain the bound
|
||||||
// value, but still binds one valid word because Vulkan sees this static use.
|
// environment category; the fragment shader consumes those CLIP references.
|
||||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||||
uint instanceDetailCategory[];
|
uint instanceDetailCategory[];
|
||||||
};
|
};
|
||||||
|
|
@ -61,7 +61,10 @@ uniform mat4 uViewProjection;
|
||||||
uniform int uDrawIDOffset;
|
uniform int uDrawIDOffset;
|
||||||
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
||||||
uniform float uParamA; // detail UV tiling
|
uniform float uParamA; // detail UV tiling
|
||||||
uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
|
// Exact 1 remains the existing "require building instance" sentinel. S4-c2
|
||||||
|
// also passes 100/255 or 200/255 for an EnvCell ClipMap alpha reference; both
|
||||||
|
// must retain the EnvCell category rather than taking the building branch.
|
||||||
|
uniform float uParamB;
|
||||||
|
|
||||||
out vec2 vBaseUv;
|
out vec2 vBaseUv;
|
||||||
out vec2 vDetailUv;
|
out vec2 vDetailUv;
|
||||||
|
|
@ -95,7 +98,7 @@ void main() {
|
||||||
vBaseTextureIndex = batch.textureIndex;
|
vBaseTextureIndex = batch.textureIndex;
|
||||||
vBaseTextureLayer = batch.textureLayer;
|
vBaseTextureLayer = batch.textureLayer;
|
||||||
vBatchFlags = batch.flags;
|
vBatchFlags = batch.flags;
|
||||||
vDetailCategory = uParamB > 0.5
|
vDetailCategory = uParamB == 1.0
|
||||||
? instanceDetailCategory[instanceIndex]
|
? instanceDetailCategory[instanceIndex]
|
||||||
: 1u;
|
: 1u;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||||
// transparent overdraw cost otherwise)
|
// transparent overdraw cost otherwise)
|
||||||
uniform int uRenderPass;
|
uniform int uRenderPass;
|
||||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||||
|
// S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are
|
||||||
|
// recognized, supplied per MDI range from TextureKey.PaletteId (100/255
|
||||||
|
// paletted, 200/255 DDS/non-paletted). Every other value keeps 0.05.
|
||||||
|
uniform float uParamB;
|
||||||
|
|
||||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||||
struct Light {
|
struct Light {
|
||||||
|
|
@ -57,15 +61,23 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
bool isRetailClipReference(float value) {
|
||||||
|
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||||
|
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||||
|
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||||
|
|
||||||
// Two-pass alpha-test (N.5 Decision 2).
|
// Two-pass alpha-test (N.5 Decision 2).
|
||||||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||||
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
||||||
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
||||||
if (uRenderPass == 0) {
|
if (uRenderPass == 0) {
|
||||||
if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Retail loads
|
||||||
|
// 100 or 200 and func 7 at SetSurface 0x0059c73c..0x0059c838.
|
||||||
|
if (color.a < alphaCutoff) discard;
|
||||||
} else {
|
} else {
|
||||||
// Transparent pass.
|
// Transparent pass.
|
||||||
//
|
//
|
||||||
|
|
@ -84,7 +96,7 @@ void main() {
|
||||||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||||||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||||||
// sprites with large transparent margins).
|
// sprites with large transparent margins).
|
||||||
if (color.a < 0.05) discard;
|
if (color.a < alphaCutoff) discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -231,12 +231,12 @@
|
||||||
"stages": [
|
"stages": [
|
||||||
{
|
{
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "0c1eddb0fc892cb76ef4da47f727cec2516052d63247fa2682918bfd60b84fa8",
|
"sourceSha256": "2d8b5b42f053a2d42e1ff44db3e331d5bea2d3f7dd36e20d813b7ae22c033181",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "e037fd28cf71453792c17522c97c4c7797193823ed78ca629a4331b632f177b1",
|
"sourceSha256": "a5285c51e8d6a1fa91f403f8489819595fa5a20abaaaccc969f220ea2c4eb335",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "12f2ceec9a8420bd273650e95251ca56c3bf3c3691fce1a539e4c5aea7abaabf",
|
"sourceSha256": "d6ae09de279e814b159e7c481d6b66f3cc37c741b3d8a249fbe2bc8156e52d4e",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
private readonly IWorldPassScope? _scope;
|
private readonly IWorldPassScope? _scope;
|
||||||
private IGpuPipeline? _opaquePipeline;
|
private IGpuPipeline? _opaquePipeline;
|
||||||
private IGpuPipeline? _alphaPipeline;
|
private IGpuPipeline? _alphaPipeline;
|
||||||
|
private IGpuPipeline? _clipPipeline;
|
||||||
private IGpuPipeline? _additivePipeline;
|
private IGpuPipeline? _additivePipeline;
|
||||||
private IGpuPipeline? _detailPipeline;
|
private IGpuPipeline? _detailPipeline;
|
||||||
private IGpuPipeline? _transparentDetailPipeline;
|
private IGpuPipeline? _transparentDetailPipeline;
|
||||||
|
private IGpuPipeline? _clipDetailPipeline;
|
||||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||||
private readonly Func<bool> _buildingDetailEnabled;
|
private readonly Func<bool> _buildingDetailEnabled;
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||||
/// five pipelines ARE this renderer's program, so there is no second step
|
/// seven pipelines ARE this renderer's program, so there is no second step
|
||||||
/// and no <c>Shader</c> to hand in.
|
/// and no <c>Shader</c> to hand in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal EnvCellRenderer(
|
internal EnvCellRenderer(
|
||||||
|
|
@ -72,6 +74,12 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
|
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
|
||||||
_alphaPipeline = CreateShellPipeline(
|
_alphaPipeline = CreateShellPipeline(
|
||||||
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
|
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
|
||||||
|
_clipPipeline = CreateShellPipeline(
|
||||||
|
device,
|
||||||
|
"envcell-clip",
|
||||||
|
GpuBlendMode.PremultipliedAlpha,
|
||||||
|
depthWrite: true,
|
||||||
|
scope.SampleCount);
|
||||||
_additivePipeline = CreateShellPipeline(
|
_additivePipeline = CreateShellPipeline(
|
||||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||||
_detailPipeline = CreateShellPipeline(
|
_detailPipeline = CreateShellPipeline(
|
||||||
|
|
@ -92,6 +100,15 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
shaderName: "mesh_detail",
|
shaderName: "mesh_detail",
|
||||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||||
transparent: true));
|
transparent: true));
|
||||||
|
_clipDetailPipeline = CreateShellPipeline(
|
||||||
|
device,
|
||||||
|
"envcell-retail-detail-clip",
|
||||||
|
GpuBlendMode.RetailDetail,
|
||||||
|
depthWrite: true,
|
||||||
|
scope.SampleCount,
|
||||||
|
shaderName: "mesh_detail",
|
||||||
|
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||||
|
transparent: false));
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,12 +291,18 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||||
int groupIndex = drawRange.GroupIndex;
|
int groupIndex = drawRange.GroupIndex;
|
||||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||||
(CullMode)(groupIndex % 4));
|
(CullMode)(groupIndex % CullGroupCount));
|
||||||
|
|
||||||
bool isAdditive = groupIndex >= 4;
|
bool isAdditive = renderPass == WbRenderPass.Transparent
|
||||||
IGpuPipeline rangeBasePipeline = isAdditive
|
&& groupIndex >= AdditiveGroupBase
|
||||||
? _additivePipeline!
|
&& groupIndex < ClipDdsGroupBase;
|
||||||
: _alphaPipeline!;
|
bool isClip = renderPass == WbRenderPass.Transparent
|
||||||
|
&& groupIndex >= ClipDdsGroupBase;
|
||||||
|
IGpuPipeline rangeBasePipeline = isClip
|
||||||
|
? _clipPipeline!
|
||||||
|
: isAdditive
|
||||||
|
? _additivePipeline!
|
||||||
|
: _alphaPipeline!;
|
||||||
if (renderPass == WbRenderPass.Transparent)
|
if (renderPass == WbRenderPass.Transparent)
|
||||||
{
|
{
|
||||||
// Blend state is the pipeline's; switching variants mid-pass has
|
// Blend state is the pipeline's; switching variants mid-pass has
|
||||||
|
|
@ -298,6 +321,9 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
? (int)renderPass | 0x100
|
? (int)renderPass | 0x100
|
||||||
: (int)renderPass;
|
: (int)renderPass;
|
||||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||||
|
pushConstants.ParamB = isClip
|
||||||
|
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||||
|
: 0f;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
|
|
||||||
// Retail DrawMesh's two-pass fallback redraws each transparent
|
// Retail DrawMesh's two-pass fallback redraws each transparent
|
||||||
|
|
@ -317,7 +343,9 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
pushConstants.DrawIdOffset = command;
|
pushConstants.DrawIdOffset = command;
|
||||||
pushConstants.TextureIndexA = 0;
|
pushConstants.TextureIndexA = 0;
|
||||||
pushConstants.ParamA = 0f;
|
pushConstants.ParamA = 0f;
|
||||||
pushConstants.ParamB = 0f;
|
pushConstants.ParamB = isClip
|
||||||
|
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||||
|
: 0f;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
encoder.MultiDrawIndexedIndirect(
|
encoder.MultiDrawIndexedIndirect(
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
|
|
@ -325,12 +353,17 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
1,
|
1,
|
||||||
(uint)sizeof(DrawElementsIndirectCommand));
|
(uint)sizeof(DrawElementsIndirectCommand));
|
||||||
|
|
||||||
BindPipelineWithMesh(encoder, _transparentDetailPipeline!, mesh);
|
BindPipelineWithMesh(
|
||||||
|
encoder,
|
||||||
|
isClip ? _clipDetailPipeline! : _transparentDetailPipeline!,
|
||||||
|
mesh);
|
||||||
SetCullMode(encoder, cullMode);
|
SetCullMode(encoder, cullMode);
|
||||||
pushConstants.DrawIdOffset = command;
|
pushConstants.DrawIdOffset = command;
|
||||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||||
pushConstants.ParamB = 0f;
|
pushConstants.ParamB = isClip
|
||||||
|
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||||
|
: 0f;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
encoder.MultiDrawIndexedIndirect(
|
encoder.MultiDrawIndexedIndirect(
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
|
|
@ -348,9 +381,11 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
(uint)sizeof(DrawElementsIndirectCommand));
|
(uint)sizeof(DrawElementsIndirectCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
|
// Retail DrawEnvCell category (2). Replay only the already-filtered
|
||||||
// shell commands, including ClipMap built-mesh subsets. No distance
|
// opaque shell commands. Transparent ClipMap is excluded here: it
|
||||||
// fade (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
|
// takes the immediate/delayed CLIP path and, when detail is active,
|
||||||
|
// its own _clipDetailPipeline contribution. No distance fade
|
||||||
|
// (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
|
||||||
// immediate-polygon path only, which built meshes never reach;
|
// immediate-polygon path only, which built meshes never reach;
|
||||||
// attenuation is the sampler's linear mip chain converging to the
|
// attenuation is the sampler's linear mip chain converging to the
|
||||||
// texture mean. The existing "Building Detail Textures" option gates
|
// texture mean. The existing "Building Detail Textures" option gates
|
||||||
|
|
@ -368,7 +403,7 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
{
|
{
|
||||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||||
(CullMode)(drawRange.GroupIndex % 4));
|
(CullMode)(drawRange.GroupIndex % CullGroupCount));
|
||||||
SetCullMode(encoder, cullMode);
|
SetCullMode(encoder, cullMode);
|
||||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
|
|
@ -468,9 +503,10 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
||||||
/// binding 9. EnvCell draws select their renderer-wide category via
|
/// binding 9. The vertex shader treats only exact <c>uParamB=1</c> as the
|
||||||
/// <c>uParamB=0</c>, so the value is semantically unused, but Vulkan still
|
/// object/building-category sentinel; ordinary EnvCell zero and the CLIP
|
||||||
/// requires the declared descriptor to be valid.
|
/// references 100/255 and 200/255 therefore keep the environment category.
|
||||||
|
/// Vulkan still requires the declared descriptor to be valid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static void BindEnvironmentDetailCategory(
|
internal static void BindEnvironmentDetailCategory(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
|
|
@ -491,11 +527,15 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
_opaquePipeline = null;
|
_opaquePipeline = null;
|
||||||
_alphaPipeline?.Dispose();
|
_alphaPipeline?.Dispose();
|
||||||
_alphaPipeline = null;
|
_alphaPipeline = null;
|
||||||
|
_clipPipeline?.Dispose();
|
||||||
|
_clipPipeline = null;
|
||||||
_additivePipeline?.Dispose();
|
_additivePipeline?.Dispose();
|
||||||
_additivePipeline = null;
|
_additivePipeline = null;
|
||||||
_detailPipeline?.Dispose();
|
_detailPipeline?.Dispose();
|
||||||
_detailPipeline = null;
|
_detailPipeline = null;
|
||||||
_transparentDetailPipeline?.Dispose();
|
_transparentDetailPipeline?.Dispose();
|
||||||
_transparentDetailPipeline = null;
|
_transparentDetailPipeline = null;
|
||||||
|
_clipDetailPipeline?.Dispose();
|
||||||
|
_clipDetailPipeline = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,17 @@ public sealed partial class EnvCellRenderer :
|
||||||
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = new();
|
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = new();
|
||||||
private readonly Dictionary<ulong, List<InstanceData>> _filteredGroups = new();
|
private readonly Dictionary<ulong, List<InstanceData>> _filteredGroups = new();
|
||||||
private readonly HashSet<List<InstanceData>> _filteredOwnedLists = new();
|
private readonly HashSet<List<InstanceData>> _filteredOwnedLists = new();
|
||||||
|
// S4-c2 F3: four cull modes for each fixed-state family. The original
|
||||||
|
// eight slots were non-additive/additive only; transparent replay also
|
||||||
|
// needs distinct pure-ClipMap DDS/paletted ranges because their alpha-test
|
||||||
|
// references differ and neither state may share an MDI call with ALPHA.
|
||||||
|
private const int CullGroupCount = 4;
|
||||||
|
private const int AdditiveGroupBase = 4;
|
||||||
|
private const int ClipDdsGroupBase = 8;
|
||||||
|
private const int ClipPalettedGroupBase = 12;
|
||||||
|
private const int BatchGroupCount = 16;
|
||||||
private readonly List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>[] _batchesByCullGroup =
|
private readonly List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>[] _batchesByCullGroup =
|
||||||
Enumerable.Range(0, 8)
|
Enumerable.Range(0, BatchGroupCount)
|
||||||
.Select(_ => new List<(ObjectRenderBatch, int, int)>())
|
.Select(_ => new List<(ObjectRenderBatch, int, int)>())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
private readonly List<int> _activeCullGroups = new(8);
|
private readonly List<int> _activeCullGroups = new(8);
|
||||||
|
|
@ -1214,17 +1223,8 @@ public sealed partial class EnvCellRenderer :
|
||||||
foreach (var batch in call.renderData.Batches)
|
foreach (var batch in call.renderData.Batches)
|
||||||
{
|
{
|
||||||
// WB BaseObjectRenderManager.cs:723-731: pass-filter.
|
// WB BaseObjectRenderManager.cs:723-731: pass-filter.
|
||||||
if (renderPass != WbRenderPass.SinglePass)
|
if (!BatchBelongsToPass(batch, renderPass))
|
||||||
{
|
continue;
|
||||||
if (batch.IsAdditive)
|
|
||||||
{
|
|
||||||
if (renderPass == WbRenderPass.Opaque) continue;
|
|
||||||
}
|
|
||||||
else if (!batch.IsTransparent)
|
|
||||||
{
|
|
||||||
if (renderPass == WbRenderPass.Transparent) continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderPass == WbRenderPass.Transparent
|
if (renderPass == WbRenderPass.Transparent
|
||||||
&& transparentRoute != EnvCellTransparentRoute.All
|
&& transparentRoute != EnvCellTransparentRoute.All
|
||||||
|
|
@ -1277,17 +1277,8 @@ public sealed partial class EnvCellRenderer :
|
||||||
var call = drawCalls[callIndex];
|
var call = drawCalls[callIndex];
|
||||||
foreach (var batch in call.renderData.Batches)
|
foreach (var batch in call.renderData.Batches)
|
||||||
{
|
{
|
||||||
if (renderPass != WbRenderPass.SinglePass)
|
if (!BatchBelongsToPass(batch, renderPass))
|
||||||
{
|
continue;
|
||||||
if (batch.IsAdditive)
|
|
||||||
{
|
|
||||||
if (renderPass == WbRenderPass.Opaque) continue;
|
|
||||||
}
|
|
||||||
else if (!batch.IsTransparent)
|
|
||||||
{
|
|
||||||
if (renderPass == WbRenderPass.Transparent) continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderPass == WbRenderPass.Transparent
|
if (renderPass == WbRenderPass.Transparent
|
||||||
&& transparentRoute != EnvCellTransparentRoute.All
|
&& transparentRoute != EnvCellTransparentRoute.All
|
||||||
|
|
@ -1296,7 +1287,9 @@ public sealed partial class EnvCellRenderer :
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
int groupIndex = ResolveBatchGroupIndex(
|
||||||
|
batch,
|
||||||
|
renderPass);
|
||||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||||
_batchesByCullGroup[groupIndex];
|
_batchesByCullGroup[groupIndex];
|
||||||
if (group.Count == 0)
|
if (group.Count == 0)
|
||||||
|
|
@ -1357,6 +1350,50 @@ public sealed partial class EnvCellRenderer :
|
||||||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact WB pass membership, with <see cref="WbRenderPass.SinglePass"/>
|
||||||
|
/// intentionally unchanged. A non-additive transparent batch is delayed
|
||||||
|
/// content and must never draw during the preceding opaque cell turn.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool BatchBelongsToPass(
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
WbRenderPass renderPass) => renderPass switch
|
||||||
|
{
|
||||||
|
WbRenderPass.Opaque => !batch.IsAdditive && !batch.IsTransparent,
|
||||||
|
WbRenderPass.Transparent => batch.IsAdditive || batch.IsTransparent,
|
||||||
|
WbRenderPass.SinglePass => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int ResolveBatchGroupIndex(
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
WbRenderPass renderPass)
|
||||||
|
{
|
||||||
|
int cull = (int)batch.CullMode;
|
||||||
|
if ((uint)cull >= CullGroupCount)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(batch), batch.CullMode, "Unknown cell-shell cull mode.");
|
||||||
|
|
||||||
|
// Preserve the old SinglePass grouping exactly: non-additive 0..3,
|
||||||
|
// additive 4..7. Its submission path still keeps the opaque pipeline.
|
||||||
|
if (renderPass != WbRenderPass.Transparent)
|
||||||
|
return cull + (batch.IsAdditive ? AdditiveGroupBase : 0);
|
||||||
|
|
||||||
|
if (batch.IsAdditive)
|
||||||
|
return cull + AdditiveGroupBase;
|
||||||
|
|
||||||
|
// Fixed state follows ConstructMesh's retained per-subset class, not
|
||||||
|
// the DrawMesh row action. Detail-active row 1 is Immediate, but a
|
||||||
|
// mask-0x08 subset (including legal positive-stipple 0x09) still needs
|
||||||
|
// the CLIP base pipeline/reference. ConstructMesh's priority already
|
||||||
|
// excludes additive/alpha-family rows before retaining this class.
|
||||||
|
if ((batch.RetailSurfaceMask & RetailAlphaMeshRouter.MaskClipMap) == 0)
|
||||||
|
return cull;
|
||||||
|
|
||||||
|
return cull + (batch.Key.PaletteId != 0
|
||||||
|
? ClipPalettedGroupBase
|
||||||
|
: ClipDdsGroupBase);
|
||||||
|
}
|
||||||
|
|
||||||
internal static void AppendMdiDrawRange(
|
internal static void AppendMdiDrawRange(
|
||||||
List<MdiDrawRange> ranges,
|
List<MdiDrawRange> ranges,
|
||||||
int groupIndex,
|
int groupIndex,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Numerics;
|
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;
|
||||||
using AcDream.App.Rendering.Gpu.Vk;
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
|
|
@ -38,44 +40,38 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A mixed cell creates one token in each list and neither set draws before
|
/// The real walk leaf executes the preceding opaque pass, dispatch, and
|
||||||
/// the real queue drain. CLIP drains first and receives only the CLIP
|
/// queue drain. The opaque subset draws at the turn; CLIP and ALPHA do not.
|
||||||
/// replay filter; ALPHA receives only ALPHA. Mutation checks: inverting the
|
/// Each delayed list then replays exactly its own subset.</summary>
|
||||||
/// detail-off immediate predicate makes <c>Assert.Empty(log)</c> fail;
|
|
||||||
/// removing the replay filter makes either route assertion fail.</summary>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MixedCell_DefersOneTokenPerList_AndReplayIsListFiltered()
|
public void WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain()
|
||||||
{
|
{
|
||||||
using var fixture = new ProductionEnvCellFixture(
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
detailSurfaceActive: false,
|
detailSurfaceActive: false,
|
||||||
0x08,
|
BatchSpec.Opaque,
|
||||||
0x02);
|
BatchSpec.ClipDds,
|
||||||
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
BatchSpec.Alpha);
|
||||||
ProductionEnvCellFixture.CellId,
|
|
||||||
detailSurfaceActive: false);
|
|
||||||
|
|
||||||
fixture.Queue.BeginFrame();
|
fixture.Queue.BeginFrame();
|
||||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
ProductionEnvCellFixture.CellId,
|
|
||||||
routes,
|
|
||||||
detailSurfaceActive: false,
|
|
||||||
fixture.Queue,
|
|
||||||
fixture.ClipSource,
|
|
||||||
fixture.AlphaSource,
|
|
||||||
fixture.ImmediateSink);
|
|
||||||
|
|
||||||
Assert.Equal(EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Alpha, routes);
|
GpuRecordedMultiDrawIndirect turnDraw = Assert.Single(
|
||||||
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.Equal(1u, turnDraw.DrawCount);
|
||||||
|
Assert.Equal(["envcell-opaque"], DrawPipelineNames(fixture.Device));
|
||||||
Assert.Equal(1, fixture.Queue.ClipCount);
|
Assert.Equal(1, fixture.Queue.ClipCount);
|
||||||
Assert.Equal(1, fixture.Queue.AlphaCount);
|
Assert.Equal(1, fixture.Queue.AlphaCount);
|
||||||
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||||
|
|
||||||
fixture.Queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
fixture.Queue.EndFrame();
|
||||||
|
|
||||||
GpuRecordedMultiDrawIndirect[] draws =
|
GpuRecordedMultiDrawIndirect[] draws =
|
||||||
[.. fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
[.. fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||||
Assert.Equal(2, draws.Length);
|
Assert.Equal(3, draws.Length);
|
||||||
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
|
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
|
||||||
fixture.Queue.EndFrame();
|
Assert.Equal(
|
||||||
|
["envcell-opaque", "envcell-clip", "envcell-alpha"],
|
||||||
|
DrawPipelineNames(fixture.Device));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -83,28 +79,29 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
{
|
{
|
||||||
using var fixture = new ProductionEnvCellFixture(
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
detailSurfaceActive: true,
|
detailSurfaceActive: true,
|
||||||
0x08);
|
BatchSpec.ClipDds);
|
||||||
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
|
||||||
ProductionEnvCellFixture.CellId,
|
|
||||||
detailSurfaceActive: true);
|
|
||||||
fixture.Queue.BeginFrame();
|
fixture.Queue.BeginFrame();
|
||||||
|
|
||||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
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(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||||
|
Assert.Equal(
|
||||||
|
["envcell-clip", "envcell-retail-detail-clip"],
|
||||||
|
DrawPipelineNames(fixture.Device));
|
||||||
|
Assert.All(
|
||||||
|
DrawPushConstants(fixture.Device),
|
||||||
|
constants => Assert.Equal(200f / 255f, constants.ParamB));
|
||||||
|
GpuPipelineDescription detailPipeline = fixture.Device.CreatedPipelines
|
||||||
|
.Single(static pipeline =>
|
||||||
|
pipeline.Description.Name == "envcell-retail-detail-clip")
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(GpuBlendMode.RetailDetail, detailPipeline.Blend);
|
||||||
|
Assert.True(detailPipeline.Depth.Test);
|
||||||
|
Assert.True(detailPipeline.Depth.Write);
|
||||||
|
Assert.Equal(GpuCompareOp.Equal, detailPipeline.Depth.Compare);
|
||||||
Assert.Equal(0, fixture.Queue.PendingCount);
|
Assert.Equal(0, fixture.Queue.PendingCount);
|
||||||
fixture.Queue.EndFrame();
|
fixture.Queue.EndFrame();
|
||||||
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
|
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||||
|
|
@ -141,47 +138,202 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionDispatch_WarmedImmediateAndFilteredSourcesDoNotAllocate()
|
public void ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate()
|
||||||
{
|
{
|
||||||
var sink = new NoAllocRouteSink();
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
var clipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
detailSurfaceActive: false,
|
||||||
sink.Render,
|
BatchSpec.Opaque,
|
||||||
EnvCellTransparentRoute.Clip);
|
BatchSpec.ClipDds,
|
||||||
var alphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
BatchSpec.Alpha,
|
||||||
sink.Render,
|
ringCapacityBytes: 64 * 1024 * 1024);
|
||||||
EnvCellTransparentRoute.Alpha);
|
|
||||||
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate = sink.DrawImmediate;
|
|
||||||
var queue = new RetailAlphaQueue();
|
|
||||||
|
|
||||||
for (int i = 0; i < 64; i++)
|
fixture.Device.Clear();
|
||||||
RunMixed(queue, clipSource, alphaSource, immediate);
|
fixture.Device.RecordingEnabled = false;
|
||||||
|
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
fixture.RunWholeLeafFrame,
|
||||||
for (int i = 0; i < 1_000; i++)
|
batchSize: 256,
|
||||||
RunMixed(queue, clipSource, alphaSource, immediate);
|
warmupBatches: 2,
|
||||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
samples: 4);
|
||||||
|
|
||||||
Assert.Equal(0, allocated);
|
Assert.Equal(0, allocated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunMixed(
|
[Theory]
|
||||||
RetailAlphaQueue queue,
|
[InlineData(false, 200f / 255f)]
|
||||||
RetailPViewPassExecutor.EnvCellAlphaDrawSource clipSource,
|
[InlineData(true, 100f / 255f)]
|
||||||
RetailPViewPassExecutor.EnvCellAlphaDrawSource alphaSource,
|
public void WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference(
|
||||||
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate)
|
bool paletted,
|
||||||
|
float expectedReference)
|
||||||
{
|
{
|
||||||
queue.BeginFrame();
|
BatchSpec clip = paletted ? BatchSpec.ClipPaletted : BatchSpec.ClipDds;
|
||||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
0xF4180104u,
|
|
||||||
EnvCellTransparentRoute.Immediate
|
|
||||||
| EnvCellTransparentRoute.Clip
|
|
||||||
| EnvCellTransparentRoute.Alpha,
|
|
||||||
detailSurfaceActive: false,
|
detailSurfaceActive: false,
|
||||||
queue,
|
clip);
|
||||||
clipSource,
|
|
||||||
alphaSource,
|
fixture.Queue.BeginFrame();
|
||||||
immediate);
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
queue.EndFrame();
|
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||||
|
fixture.Queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
||||||
|
Assert.Equal(
|
||||||
|
expectedReference,
|
||||||
|
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
||||||
|
|
||||||
|
GpuPipelineDescription clipPipeline = fixture.Device.CreatedPipelines
|
||||||
|
.Single(static pipeline => pipeline.Description.Name == "envcell-clip")
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(GpuBlendMode.PremultipliedAlpha, clipPipeline.Blend);
|
||||||
|
Assert.True(clipPipeline.Depth.Test);
|
||||||
|
Assert.True(clipPipeline.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, clipPipeline.Depth.Compare);
|
||||||
|
Assert.False(clipPipeline.AlphaToCoverage);
|
||||||
|
|
||||||
|
GpuPipelineDescription alphaPipeline = fixture.Device.CreatedPipelines
|
||||||
|
.Single(static pipeline => pipeline.Description.Name == "envcell-alpha")
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(GpuBlendMode.StraightAlpha, alphaPipeline.Blend);
|
||||||
|
Assert.True(alphaPipeline.Depth.Test);
|
||||||
|
Assert.False(alphaPipeline.Depth.Write);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference()
|
||||||
|
{
|
||||||
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
BatchSpec.ClipPositiveStippleDds);
|
||||||
|
|
||||||
|
fixture.Queue.BeginFrame();
|
||||||
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
|
|
||||||
|
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.Equal(1, fixture.Queue.ClipCount);
|
||||||
|
Assert.Equal(0, fixture.Queue.AlphaCount);
|
||||||
|
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||||
|
|
||||||
|
fixture.Queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
||||||
|
Assert.Equal(
|
||||||
|
200f / 255f,
|
||||||
|
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vulkan has no fixed-function alpha test. The shader's discard-on-less
|
||||||
|
/// spelling is exactly GREATER_EQUAL: equality survives. Changing either
|
||||||
|
/// comparison to <= (strict GREATER) fails this named source+manifest pin.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ClipShaders_UseGreaterEqualForThePerRangeReference()
|
||||||
|
{
|
||||||
|
string root = RepositoryRoot();
|
||||||
|
string modern = File.ReadAllText(Path.Combine(
|
||||||
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
|
||||||
|
string detail = File.ReadAllText(Path.Combine(
|
||||||
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.frag"));
|
||||||
|
string detailVertex = File.ReadAllText(Path.Combine(
|
||||||
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.vert"));
|
||||||
|
|
||||||
|
Assert.Contains("if (color.a < alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("if (color.a <= alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", modern, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", detail, StringComparison.Ordinal);
|
||||||
|
foreach (string shader in new[] { modern, detail })
|
||||||
|
{
|
||||||
|
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
Assert.Contains("if (base.a < alphaCutoff)", detail, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("if (base.a <= alphaCutoff)", detail, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"vDetailCategory = uParamB == 1.0",
|
||||||
|
detailVertex,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)] // Flush
|
||||||
|
[InlineData(1)] // EndFrame
|
||||||
|
[InlineData(2)] // AbortFrame
|
||||||
|
public void RejectedEnvCellStorm_RollsBackPayloadAndStillResetsFirstUseSource(int completion)
|
||||||
|
{
|
||||||
|
const int retainedGeometricBound = 4096;
|
||||||
|
int rejectedResetCount = 0;
|
||||||
|
int rejectedDrawCount = 0;
|
||||||
|
var accepted = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
static (_, _, _) => { },
|
||||||
|
EnvCellTransparentRoute.Clip);
|
||||||
|
var rejected = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
(_, _, _) => rejectedDrawCount++,
|
||||||
|
EnvCellTransparentRoute.Clip,
|
||||||
|
() => rejectedResetCount++);
|
||||||
|
var unusedAlpha = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
static (_, _, _) => { },
|
||||||
|
EnvCellTransparentRoute.Alpha);
|
||||||
|
var queue = new RetailAlphaQueue();
|
||||||
|
RetailPViewPassExecutor.RenderImmediateEnvCellRoute neverImmediate =
|
||||||
|
static (_, _, _) => throw new InvalidOperationException();
|
||||||
|
|
||||||
|
queue.BeginFrame();
|
||||||
|
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
|
||||||
|
{
|
||||||
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
|
(uint)i,
|
||||||
|
EnvCellTransparentRoute.Clip,
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
queue,
|
||||||
|
accepted,
|
||||||
|
unusedAlpha,
|
||||||
|
neverImmediate);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < RetailAlphaQueue.ListCapacity * 3; i++)
|
||||||
|
{
|
||||||
|
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||||
|
0xF4180104u,
|
||||||
|
EnvCellTransparentRoute.Clip,
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
queue,
|
||||||
|
rejected,
|
||||||
|
unusedAlpha,
|
||||||
|
neverImmediate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.ClipCount);
|
||||||
|
Assert.Equal(RetailAlphaQueue.ListCapacity, accepted.PendingCount);
|
||||||
|
Assert.Equal(0, rejected.PendingCount);
|
||||||
|
Assert.InRange(accepted.PendingCapacity, RetailAlphaQueue.ListCapacity, retainedGeometricBound);
|
||||||
|
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
||||||
|
|
||||||
|
switch (completion)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||||
|
queue.AbortFrame();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
queue.EndFrame();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
queue.AbortFrame();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(completion));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(1, rejectedResetCount);
|
||||||
|
Assert.Equal(0, rejectedDrawCount);
|
||||||
|
Assert.Equal(0, rejected.PendingCount);
|
||||||
|
Assert.InRange(accepted.PendingCapacity, 0, retainedGeometricBound);
|
||||||
|
Assert.InRange(accepted.PreparedCapacity, 0, retainedGeometricBound);
|
||||||
|
Assert.InRange(accepted.DrawCapacity, 0, retainedGeometricBound);
|
||||||
|
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
||||||
|
Assert.InRange(rejected.PreparedCapacity, 0, retainedGeometricBound);
|
||||||
|
Assert.InRange(rejected.DrawCapacity, 0, retainedGeometricBound);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
|
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
|
||||||
|
|
@ -197,6 +349,97 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
},
|
},
|
||||||
expectedRoute);
|
expectedRoute);
|
||||||
|
|
||||||
|
private static List<RetailAlphaEntry> ClipEntries(RetailAlphaQueue queue) =>
|
||||||
|
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue)
|
||||||
|
.GetField("_clip", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||||
|
.GetValue(queue)!;
|
||||||
|
|
||||||
|
private static string[] DrawPipelineNames(RecordingGpuDevice device)
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||||
|
for (int i = 0; i < calls.Count; i++)
|
||||||
|
{
|
||||||
|
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
||||||
|
continue;
|
||||||
|
for (int prior = i - 1; prior >= 0; prior--)
|
||||||
|
{
|
||||||
|
if (calls[prior] is GpuRecordedPipelineBind bind)
|
||||||
|
{
|
||||||
|
names.Add(bind.PipelineName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [.. names];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GpuPushConstants[] DrawPushConstants(RecordingGpuDevice device)
|
||||||
|
{
|
||||||
|
var constants = new List<GpuPushConstants>();
|
||||||
|
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||||
|
for (int i = 0; i < calls.Count; i++)
|
||||||
|
{
|
||||||
|
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
||||||
|
continue;
|
||||||
|
for (int prior = i - 1; prior >= 0; prior--)
|
||||||
|
{
|
||||||
|
if (calls[prior] is GpuRecordedPushConstants push)
|
||||||
|
{
|
||||||
|
constants.Add(push.Constants);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [.. constants];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RepositoryRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
|
||||||
|
while (cursor is not null && !File.Exists(Path.Combine(cursor.FullName, "AcDream.slnx")))
|
||||||
|
cursor = cursor.Parent;
|
||||||
|
return cursor?.FullName
|
||||||
|
?? throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct BatchSpec(
|
||||||
|
byte Mask,
|
||||||
|
bool IsTransparent,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind Translucency,
|
||||||
|
uint PaletteId)
|
||||||
|
{
|
||||||
|
internal static BatchSpec Opaque { get; } = new(
|
||||||
|
0x00,
|
||||||
|
IsTransparent: false,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.Opaque,
|
||||||
|
PaletteId: 0);
|
||||||
|
|
||||||
|
internal static BatchSpec ClipDds { get; } = new(
|
||||||
|
RetailAlphaMeshRouter.MaskClipMap,
|
||||||
|
IsTransparent: true,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||||
|
PaletteId: 0);
|
||||||
|
|
||||||
|
internal static BatchSpec ClipPaletted { get; } = new(
|
||||||
|
RetailAlphaMeshRouter.MaskClipMap,
|
||||||
|
IsTransparent: true,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||||
|
PaletteId: 0x04000001u);
|
||||||
|
|
||||||
|
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
|
||||||
|
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple,
|
||||||
|
IsTransparent: true,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||||
|
PaletteId: 0);
|
||||||
|
|
||||||
|
internal static BatchSpec Alpha { get; } = new(
|
||||||
|
RetailAlphaMeshRouter.MaskAlphaFamily,
|
||||||
|
IsTransparent: true,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
||||||
|
PaletteId: 0);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recording-GPU fixture for the production EnvCell scan, dispatch and
|
/// Recording-GPU fixture for the production EnvCell scan, dispatch and
|
||||||
/// filtered replay path. The two retained masks enter through the real
|
/// filtered replay path. The two retained masks enter through the real
|
||||||
|
|
@ -215,9 +458,12 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
|
|
||||||
public ProductionEnvCellFixture(
|
public ProductionEnvCellFixture(
|
||||||
bool detailSurfaceActive,
|
bool detailSurfaceActive,
|
||||||
params byte[] retainedMasks)
|
BatchSpec firstBatch,
|
||||||
|
BatchSpec secondBatch = default,
|
||||||
|
BatchSpec thirdBatch = default,
|
||||||
|
int ringCapacityBytes = 8 * 1024 * 1024)
|
||||||
{
|
{
|
||||||
Device = new RecordingGpuDevice();
|
Device = new RecordingGpuDevice(ringCapacityBytes);
|
||||||
_frames = new GpuDeviceFrameLifetime(Device);
|
_frames = new GpuDeviceFrameLifetime(Device);
|
||||||
_scope = new VulkanWorldPassScope(sampleCount: 1);
|
_scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||||
_meshManager = new ObjectMeshManager(
|
_meshManager = new ObjectMeshManager(
|
||||||
|
|
@ -236,17 +482,27 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
new VertexPositionNormalTexture { Position = new Vector3(0, 1, 0) },
|
new VertexPositionNormalTexture { Position = new Vector3(0, 1, 0) },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
var batches = new List<TextureBatchData>(retainedMasks.Length);
|
BatchSpec[] specs = secondBatch == default
|
||||||
for (int i = 0; i < retainedMasks.Length; i++)
|
? [firstBatch]
|
||||||
|
: thirdBatch == default
|
||||||
|
? [firstBatch, secondBatch]
|
||||||
|
: [firstBatch, secondBatch, thirdBatch];
|
||||||
|
var batches = new List<TextureBatchData>(specs.Length);
|
||||||
|
for (int i = 0; i < specs.Length; i++)
|
||||||
{
|
{
|
||||||
|
BatchSpec spec = specs[i];
|
||||||
batches.Add(new TextureBatchData
|
batches.Add(new TextureBatchData
|
||||||
{
|
{
|
||||||
Key = new TextureKey { SurfaceId = 0x08000BFFu + (uint)i },
|
Key = new TextureKey
|
||||||
|
{
|
||||||
|
SurfaceId = 0x08000BFFu + (uint)i,
|
||||||
|
PaletteId = spec.PaletteId,
|
||||||
|
},
|
||||||
TextureData = new byte[8 * 8 * 4],
|
TextureData = new byte[8 * 8 * 4],
|
||||||
Indices = [0, 1, 2],
|
Indices = [0, 1, 2],
|
||||||
IsTransparent = true,
|
IsTransparent = spec.IsTransparent,
|
||||||
Translucency = AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
Translucency = spec.Translucency,
|
||||||
RetailSurfaceMask = retainedMasks[i],
|
RetailSurfaceMask = spec.Mask,
|
||||||
CullMode = CullMode.Clockwise,
|
CullMode = CullMode.Clockwise,
|
||||||
IsCellShell = true,
|
IsCellShell = true,
|
||||||
SourceSurfaceIndex = i,
|
SourceSurfaceIndex = i,
|
||||||
|
|
@ -291,6 +547,10 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
((HashSet<uint>)typeof(EnvCellRenderer)
|
||||||
|
.GetField("_transparentCellIds", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||||
|
.GetValue(Renderer)!)
|
||||||
|
.Add(CellId);
|
||||||
|
|
||||||
Queue = new RetailAlphaQueue();
|
Queue = new RetailAlphaQueue();
|
||||||
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
|
|
@ -299,7 +559,28 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||||
Renderer.RenderTransparentOrdered,
|
Renderer.RenderTransparentOrdered,
|
||||||
EnvCellTransparentRoute.Alpha);
|
EnvCellTransparentRoute.Alpha);
|
||||||
ImmediateSink = new ProductionImmediateSink(Renderer);
|
var passes = new RetailPViewPassExecutor(
|
||||||
|
new NullWorldPassSurface(),
|
||||||
|
NullRenderFrameGlState.Instance,
|
||||||
|
ClipFrame.NoClip(),
|
||||||
|
terrain: null,
|
||||||
|
Renderer,
|
||||||
|
(WbDrawDispatcher)RuntimeHelpers.GetUninitializedObject(typeof(WbDrawDispatcher)),
|
||||||
|
sky: null,
|
||||||
|
particles: null,
|
||||||
|
particleRenderer: null,
|
||||||
|
portalDepthMask: null,
|
||||||
|
Queue,
|
||||||
|
(WorldRenderDiagnostics)RuntimeHelpers.GetUninitializedObject(typeof(WorldRenderDiagnostics)),
|
||||||
|
(TerrainDrawDiagnosticsController)RuntimeHelpers.GetUninitializedObject(
|
||||||
|
typeof(TerrainDrawDiagnosticsController)));
|
||||||
|
Leaf = new WalkProductionLeafRenderer(
|
||||||
|
passes,
|
||||||
|
new RetailPViewFrameInput(),
|
||||||
|
new ClipFrameAssembly(),
|
||||||
|
static () => { },
|
||||||
|
static () => { },
|
||||||
|
static () => 0);
|
||||||
|
|
||||||
_frames.BeginFrame();
|
_frames.BeginFrame();
|
||||||
IGpuFrame frame = _frames.CurrentFrame!;
|
IGpuFrame frame = _frames.CurrentFrame!;
|
||||||
|
|
@ -323,7 +604,14 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
|
|
||||||
public RetailPViewPassExecutor.EnvCellAlphaDrawSource AlphaSource { get; }
|
public RetailPViewPassExecutor.EnvCellAlphaDrawSource AlphaSource { get; }
|
||||||
|
|
||||||
public ProductionImmediateSink ImmediateSink { get; }
|
public WalkProductionLeafRenderer Leaf { get; }
|
||||||
|
|
||||||
|
public void RunWholeLeafFrame()
|
||||||
|
{
|
||||||
|
Queue.BeginFrame();
|
||||||
|
Leaf.DrawCellShell(CellId);
|
||||||
|
Queue.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
private static bool DetailOn() => true;
|
private static bool DetailOn() => true;
|
||||||
|
|
||||||
|
|
@ -339,22 +627,15 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ProductionImmediateSink(EnvCellRenderer renderer) :
|
private sealed class NullWorldPassSurface : IWorldPassSurface
|
||||||
IEnvCellImmediateDrawSink
|
|
||||||
{
|
{
|
||||||
public int DrawCount { get; private set; }
|
public void PrepareClipFrame() { }
|
||||||
|
|
||||||
public bool LastDetailSurfaceActive { get; private set; }
|
public void EnableClipDistances() { }
|
||||||
|
|
||||||
public void DrawImmediate(
|
public void DisableClipDistances() { }
|
||||||
uint cellId,
|
|
||||||
EnvCellTransparentRoute route,
|
public void ClearInteriorDepth() => throw new InvalidOperationException();
|
||||||
bool detailSurfaceActive)
|
|
||||||
{
|
|
||||||
DrawCount++;
|
|
||||||
LastDetailSurfaceActive = detailSurfaceActive;
|
|
||||||
renderer.RenderTransparentOrdered([cellId], route, detailSurfaceActive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
|
|
@ -395,20 +676,4 @@ 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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,16 +74,17 @@ public sealed class GpuContractTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BlendModesCoverEveryRetailTranslucencyKind()
|
public void BlendModesCoverEveryRetailRendererState()
|
||||||
{
|
{
|
||||||
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
||||||
// surface's TranslucencyKind. Retail has three, and the V0 contract shipped
|
// surface's TranslucencyKind. Retail also selects ONE/INVSRCALPHA for a
|
||||||
// with only two — slice V4c found the gap. Mapping InvAlpha onto
|
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
||||||
// StraightAlpha would silently change how every inverse-alpha surface
|
// the detail combine is a separate renderer state. Collapsing any of
|
||||||
// composites, so the contract has to carry all three.
|
// these modes changes the corresponding subset's composition.
|
||||||
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length);
|
Assert.Equal(6, Enum.GetValues<GpuBlendMode>().Length);
|
||||||
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
|
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.RetailDetail, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.RetailDetail, Enum.GetValues<GpuBlendMode>());
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ internal sealed class RecordingGpuDevice : IGpuDevice, IGpuPipelineFormatVariant
|
||||||
/// <summary>Every recorded call, in submission order.</summary>
|
/// <summary>Every recorded call, in submission order.</summary>
|
||||||
public IReadOnlyList<GpuRecordedCall> Calls => _calls;
|
public IReadOnlyList<GpuRecordedCall> Calls => _calls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocation measurements may drive the real renderer/RHI submission
|
||||||
|
/// path without allocating one diagnostic record per call. Resource,
|
||||||
|
/// frame, ring, and encoder behavior stays identical; only call-record
|
||||||
|
/// object construction is suppressed when this is false.
|
||||||
|
/// </summary>
|
||||||
|
public bool RecordingEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>Backing store for ring allocations, so tests can read what a renderer wrote.</summary>
|
/// <summary>Backing store for ring allocations, so tests can read what a renderer wrote.</summary>
|
||||||
public ReadOnlySpan<byte> RingBytes => _ring;
|
public ReadOnlySpan<byte> RingBytes => _ring;
|
||||||
|
|
||||||
|
|
@ -410,7 +418,8 @@ internal sealed class RecordingGpuDevice : IGpuDevice, IGpuPipelineFormatVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
_ringCursor = aligned + (uint)byteCount;
|
_ringCursor = aligned + (uint)byteCount;
|
||||||
_calls.Add(new GpuRecordedRingAllocation(usage, byteCount, aligned));
|
if (RecordingEnabled)
|
||||||
|
_calls.Add(new GpuRecordedRingAllocation(usage, byteCount, aligned));
|
||||||
return new GpuRingAllocation(RingBuffer, aligned, _ring.AsSpan((int)aligned, byteCount));
|
return new GpuRingAllocation(RingBuffer, aligned, _ring.AsSpan((int)aligned, byteCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,7 +469,8 @@ internal sealed class RecordingGpuFrame(RecordingGpuDevice device, long serial,
|
||||||
"Published host writes require a host-writable storage buffer.",
|
"Published host writes require a host-writable storage buffer.",
|
||||||
nameof(buffer));
|
nameof(buffer));
|
||||||
}
|
}
|
||||||
device.Record(new GpuRecordedHostStorageVisibility(buffer.Name));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedHostStorageVisibility(buffer.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
||||||
|
|
@ -549,60 +559,97 @@ internal sealed class RecordingGpuPassEncoder(RecordingGpuDevice device, GpuPass
|
||||||
throw new InvalidOperationException("Pipeline and pass colour-attachment intents must match.");
|
throw new InvalidOperationException("Pipeline and pass colour-attachment intents must match.");
|
||||||
if (pipeline.Description.ViewMask != Pass.ViewMask)
|
if (pipeline.Description.ViewMask != Pass.ViewMask)
|
||||||
throw new InvalidOperationException("Pipeline and pass view masks must match.");
|
throw new InvalidOperationException("Pipeline and pass view masks must match.");
|
||||||
device.Record(new GpuRecordedPipelineBind(pipeline.Description.Name));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedPipelineBind(pipeline.Description.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(buffer);
|
ArgumentNullException.ThrowIfNull(buffer);
|
||||||
device.Record(new GpuRecordedStorageBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedStorageBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(buffer);
|
ArgumentNullException.ThrowIfNull(buffer);
|
||||||
device.Record(new GpuRecordedUniformBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedUniformBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes)
|
public void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(buffer);
|
ArgumentNullException.ThrowIfNull(buffer);
|
||||||
device.Record(new GpuRecordedVertexBind(binding, buffer.Name, offsetBytes));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedVertexBind(binding, buffer.Name, offsetBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType)
|
public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(buffer);
|
ArgumentNullException.ThrowIfNull(buffer);
|
||||||
device.Record(new GpuRecordedIndexBind(buffer.Name, offsetBytes, indexType));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedIndexBind(buffer.Name, offsetBytes, indexType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPushConstants(in GpuPushConstants constants) =>
|
public void SetPushConstants(in GpuPushConstants constants)
|
||||||
device.Record(new GpuRecordedPushConstants(constants));
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedPushConstants(constants));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetViewport(int x, int y, int width, int height) =>
|
public void SetViewport(int x, int y, int width, int height)
|
||||||
device.Record(new GpuRecordedViewport(x, y, width, height));
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedViewport(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetScissor(int x, int y, int width, int height) =>
|
public void SetScissor(int x, int y, int width, int height)
|
||||||
device.Record(new GpuRecordedScissor(x, y, width, height));
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedScissor(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetCullMode(GpuCullMode cullMode) => device.Record(new GpuRecordedCullMode(cullMode));
|
public void SetCullMode(GpuCullMode cullMode)
|
||||||
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedCullMode(cullMode));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetFrontFace(GpuFrontFace frontFace) => device.Record(new GpuRecordedFrontFace(frontFace));
|
public void SetFrontFace(GpuFrontFace frontFace)
|
||||||
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedFrontFace(frontFace));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetStencil(in GpuStencilState stencil) => device.Record(new GpuRecordedStencil(stencil));
|
public void SetStencil(in GpuStencilState stencil)
|
||||||
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedStencil(stencil));
|
||||||
|
}
|
||||||
|
|
||||||
public void SetDepthWrite(bool enabled) => device.Record(new GpuRecordedDepthWrite(enabled));
|
public void SetDepthWrite(bool enabled)
|
||||||
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedDepthWrite(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
public void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance) =>
|
public void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance)
|
||||||
device.Record(new GpuRecordedDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance));
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance));
|
||||||
|
}
|
||||||
|
|
||||||
public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance) =>
|
public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance)
|
||||||
device.Record(new GpuRecordedDraw(vertexCount, instanceCount, firstVertex, firstInstance));
|
{
|
||||||
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedDraw(vertexCount, instanceCount, firstVertex, firstInstance));
|
||||||
|
}
|
||||||
|
|
||||||
public void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes)
|
public void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(commands);
|
ArgumentNullException.ThrowIfNull(commands);
|
||||||
device.Record(new GpuRecordedMultiDrawIndirect(commands.Name, offsetBytes, drawCount, strideBytes));
|
if (device.RecordingEnabled)
|
||||||
|
device.Record(new GpuRecordedMultiDrawIndirect(commands.Name, offsetBytes, drawCount, strideBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable BeginTimerScope(string scopeName)
|
public IDisposable BeginTimerScope(string scopeName)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,13 @@ public sealed class VulkanShaderManifestTests
|
||||||
{
|
{
|
||||||
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
|
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
|
||||||
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
|
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
|
||||||
["mesh_modern.frag.spv"] = "b702b644862aca31ce1fb0677adc5872b39c4ea87f595a89363b44d10f2cc50e",
|
// Re-pinned 2026-09-04 (Campaign OVERHAUL S4-c2 owner-authorized
|
||||||
|
// repair): Vulkan expresses SetSurface's pure-ClipMap alpha test
|
||||||
|
// through uParamB, recognizing only the paletted 100/255 and DDS
|
||||||
|
// 200/255 references and using discard-on-less so equality passes.
|
||||||
|
// The paired binary selects those globals and
|
||||||
|
// D3DCMP_GREATEREQUAL at 0x0059c73c..0x0059c838.
|
||||||
|
["mesh_modern.frag.spv"] = "1b3a59d0cc7c5bb6acf793d57d9673d860a7eb50bdd51770781f59feac4604a1",
|
||||||
// mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
|
// mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
|
||||||
// F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
|
// F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
|
||||||
// gl_ClipDistance loop, and the gl_PerVertex redeclaration are
|
// gl_ClipDistance loop, and the gl_PerVertex redeclaration are
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,14 @@ public sealed class VulkanViewportMappingTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AllThreeRetailBlendModesAreRepresentable()
|
public void AllRetailBlendModesAreRepresentable()
|
||||||
{
|
{
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha));
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha));
|
||||||
|
Assert.Equal(
|
||||||
|
(BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||||
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.PremultipliedAlpha));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
(BlendFactor.SrcAlpha, BlendFactor.One),
|
(BlendFactor.SrcAlpha, BlendFactor.One),
|
||||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
@ -9,6 +10,7 @@ using AcDream.App.Tests.Rendering.Gpu;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
|
using Chorizite.Core.Render.Enums;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
@ -189,54 +191,110 @@ public sealed class ParticleRendererRouteTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionDispatch_WarmedAppendAndImmediatePathsDoNotAllocate()
|
public void ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate()
|
||||||
{
|
{
|
||||||
var queue = new RetailAlphaQueue();
|
using var device = new RecordingGpuDevice(64 * 1024 * 1024);
|
||||||
var sink = new NoAllocParticleSink();
|
using var manager = new ObjectMeshManager(
|
||||||
ParticleRenderer.ReserveDeferredParticleDraw reserve = sink.Reserve;
|
new VulkanMeshPipelineDevice(device.Retirement),
|
||||||
ParticleRenderer.DrawImmediateParticle immediate = sink.DrawImmediate;
|
device,
|
||||||
|
new NullPreparedAssetSource(),
|
||||||
|
NullLogger<ObjectMeshManager>.Instance);
|
||||||
|
ObjectRenderData renderData = Assert.IsType<ObjectRenderData>(manager.UploadMeshData(
|
||||||
|
new ObjectMeshData
|
||||||
|
{
|
||||||
|
ObjectId = 0x010001ECu,
|
||||||
|
Vertices =
|
||||||
|
[
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.Zero },
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.UnitX },
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.UnitY },
|
||||||
|
],
|
||||||
|
TextureBatches =
|
||||||
|
{
|
||||||
|
[(8, 8, TextureFormat.RGBA8)] =
|
||||||
|
[
|
||||||
|
new TextureBatchData
|
||||||
|
{
|
||||||
|
Key = new TextureKey { SurfaceId = 0x08000015u },
|
||||||
|
TextureData = new byte[8 * 8 * 4],
|
||||||
|
Indices = [0, 1, 2],
|
||||||
|
Translucency = TranslucencyKind.Opaque,
|
||||||
|
CullMode = DatReaderWriter.Enums.CullMode.Clockwise,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
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);
|
||||||
|
|
||||||
for (int i = 0; i < 64; i++)
|
Type keyType = typeof(ParticleRenderer).GetNestedType(
|
||||||
RunPair(queue, sink, reserve, immediate);
|
"MeshBatchKey", BindingFlags.NonPublic)!;
|
||||||
|
Type instanceType = typeof(ParticleRenderer).GetNestedType(
|
||||||
|
"MeshParticleInstance", BindingFlags.NonPublic)!;
|
||||||
|
Type drawType = typeof(ParticleRenderer).GetNestedType(
|
||||||
|
"MeshParticleDraw", BindingFlags.NonPublic)!;
|
||||||
|
object key = Activator.CreateInstance(
|
||||||
|
keyType,
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
|
binder: null,
|
||||||
|
args: [0x010001ECu, 0],
|
||||||
|
culture: null)!;
|
||||||
|
object instance = Activator.CreateInstance(
|
||||||
|
instanceType,
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
|
binder: null,
|
||||||
|
args: [Matrix4x4.Identity, 0xFFFFFFFFu, 0f, 0u],
|
||||||
|
culture: null)!;
|
||||||
|
object draw = Activator.CreateInstance(
|
||||||
|
drawType,
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
|
binder: null,
|
||||||
|
args: [key, renderData.Batches[0], instance],
|
||||||
|
culture: null)!;
|
||||||
|
var drawList = (IList)typeof(ParticleRenderer).GetField(
|
||||||
|
"_meshDrawListScratch",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.GetValue(renderer)!;
|
||||||
|
drawList.Add(draw);
|
||||||
|
var immediate = (ParticleRenderer.DrawImmediateParticle)typeof(ParticleRenderer)
|
||||||
|
.GetField("_drawImmediateParticle", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.GetValue(renderer)!;
|
||||||
|
|
||||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
frames.BeginFrame();
|
||||||
for (int i = 0; i < 1_000; i++)
|
renderer.BeginFrame(frameSlot: 0);
|
||||||
RunPair(queue, sink, reserve, immediate);
|
IGpuFrame frame = frames.CurrentFrame!;
|
||||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
using IGpuPassEncoder pass = frame.BeginPass(GpuPassDescription.BackbufferClear(
|
||||||
|
"s4-c2-particle-allocation",
|
||||||
|
Vector4.Zero,
|
||||||
|
sampleCount: 1));
|
||||||
|
using IDisposable publication = scope.Publish(pass);
|
||||||
|
|
||||||
|
device.Clear();
|
||||||
|
device.RecordingEnabled = false;
|
||||||
|
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||||
|
() => immediate(
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
ParticleSubmissionKind.Mesh,
|
||||||
|
drawIndex: 0,
|
||||||
|
opaqueDepthState: true),
|
||||||
|
batchSize: 256,
|
||||||
|
warmupBatches: 2,
|
||||||
|
samples: 4);
|
||||||
|
|
||||||
Assert.Equal(0, allocated);
|
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
|
private sealed class RecordingSource : IRetailAlphaDrawSource
|
||||||
{
|
{
|
||||||
public int PrepareCount { get; private set; }
|
public int PrepareCount { get; private set; }
|
||||||
|
|
@ -252,31 +310,6 @@ public sealed class ParticleRendererRouteTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
{
|
{
|
||||||
public PreparedAssetSourceStats Stats => default;
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
|
|
||||||
|
|
@ -240,9 +240,10 @@ public sealed class RetailAlphaMeshRouterTests
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s
|
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s
|
||||||
/// reachable mapping (opaque/ClipMap are pre-filtered upstream and never
|
/// complete mapping. Ordinary Wb callers pre-filter opaque/ClipMap and
|
||||||
/// actually call this from a live site, but the function itself must
|
/// retain AP-239's earlier subset-mask reconstruction distinction. Mesh
|
||||||
/// still map them defensively).
|
/// particles, however, genuinely call this method without that ordinary-
|
||||||
|
/// Wb pre-filter, including for opaque-classified batches.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(TranslucencyKind.AlphaBlend, RetailAlphaMeshRouter.MaskAlphaFamily)]
|
[InlineData(TranslucencyKind.AlphaBlend, RetailAlphaMeshRouter.MaskAlphaFamily)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue