feat(overhaul): select pack shadows from retail visibility
Borrow the exact prior-completed landscape visibility transaction and S2 CELLARRAY owner for the opt-in IA-24 directional-shadow pack. Select terrain by authored 1..64 cells, ordinary casters by CELLARRAY, and buildings by outdoor EffectCellId with no fallback. Keep retained caster/material/terrain topology stable across visibility-only frames. Publish exact arbitrary active instance runs and bounded terrain commands through separate selection sequences; preserve transform-journal, fade/retry, deferral, shader/RHI, ordinary world, and pack-off behavior. Amend IA-24 and the S5 ledger. Pre-commit gates: Release solution build 0 warnings/0 errors; focused visibility/frame/caster/prepared/GPU/terrain/pack lane 137/137; warmed caster/prepared/terrain selectors 0 B; git diff --check clean. Official hermetic and InstalledDat evidence intentionally run post-commit from this exact clean tree. Mutation evidence (each restored exactly): (1) CELLARRAY->Parent first failed PriorLandscapeSelection expected [201,202,203,205], actual [204,205]. (2) all resident terrain first failed Assert.Single with 3 commands. (3) building EffectCell->anchor first failed expected trailing 205, actual 206. (4) admit missing membership first failed with extra 204. (5) completed->building scratch first failed completed-view Assert.True, expected true/actual false. (6) selection advanced BuildSequence first failed expected 1/actual 2. (7) alternating->prefix first failed active command count expected 3/actual 1. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c106bcdff
commit
a4de2efc4e
19 changed files with 1198 additions and 100 deletions
|
|
@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` |
|
| IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` |
|
||||||
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
||||||
| IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` |
|
| IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` |
|
||||||
| IA-24 | **Filed 2026-08-22, Campaign AR.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. **While shadows render, the pack's receiver vertex shaders (`mesh_atmospheric.vert`, `terrain_atmospheric.vert`) also take the outdoor directional LIGHTING direction from that same celestial source instead of retail's authored `uLights[0]` direction, so the lit term agrees with the shadow direction; whenever the shadow gate is closed (night, user strength 0, portal cover, indoor) the flag bit is clear and both shaders fall back to the plain pipeline's authored-light expression (Campaign VM VM6 round 5, `754d59d9`).** Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` | This is the user-requested headline graphics enhancement and is strictly opt-in. The retail path remains the default and authoritative fallback; pack-off does not build/select shadow work or change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail. A wrong celestial identity/transform or stale source transition would visibly misalign shadows from the sky; pack-off output changing would violate the campaign's primary safety contract. | `SkyDesc::GetLighting @0x00500A80`; `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research note |
|
| IA-24 | **Filed 2026-08-22, Campaign AR; amended 2026-09-04, Campaign OVERHAUL S5-c2.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. Its active terrain/object projection borrows retail's **prior successfully completed** landscape visibility by exact set reference, then selects ordinary objects by S2's retained retail CELLARRAY intersection and building shells by their outdoor placement `EffectCellId`; missing membership and no completed view fail closed, with no resident/frustum fallback. **While shadows render, the pack's receiver vertex shaders (`mesh_atmospheric.vert`, `terrain_atmospheric.vert`) also take the outdoor directional LIGHTING direction from that same celestial source instead of retail's authored `uLights[0]` direction, so the lit term agrees with the shadow direction; whenever the shadow gate is closed (night, user strength 0, portal cover, indoor) the flag bit is clear and both shaders fall back to the plain pipeline's authored-light expression (Campaign VM VM6 round 5, `754d59d9`).** Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; `src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` and OVERHAUL packet §14 | This is the user-requested headline graphics enhancement and is strictly opt-in. Borrowing the prior completed retail landscape product and exact S2 membership narrows pack-on work without inventing another visibility owner. The retail path remains the default and authoritative fallback; pack-off does not build, select, upload, or draw shadow work and does not change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail and deliberately trails camera visibility by one successfully completed world view. A wrong CELLARRAY/EffectCell identity, stale prior-view transaction, or celestial transform would visibly add, omit, or misalign shadows; pack-off output changing would violate the campaign's primary safety contract. | Landscape input: `LScape::draw_check_blocks @0x00505F80`, `LScape::landcell_check @0x005050A0`, `CLandCell::IsInView @0x00532CB0`; celestial: `SkyDesc::GetLighting @0x00500A80`, `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research notes |
|
||||||
| IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per DAT-classified `AcDream.Core.World.WeatherKind` (Clear/Overcast/Rain/Snow/Storm — the same classification `WeatherState.cs` already derives from the active day group's authored name, not the day group's raw index, which carries no weather meaning by itself) and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByWeather`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`, `mesh_detail`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (top-nibble-0x8 entity-id namespace, existing acdream mechanism, not retail) |
|
| IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per DAT-classified `AcDream.Core.World.WeatherKind` (Clear/Overcast/Rain/Snow/Storm — the same classification `WeatherState.cs` already derives from the active day group's authored name, not the day group's raw index, which carries no weather meaning by itself) and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByWeather`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`, `mesh_detail`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (top-nibble-0x8 entity-id namespace, existing acdream mechanism, not retail) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -681,8 +681,8 @@ Update immediately when a slice changes state. Chat is not the ledger.
|
||||||
| S4-c3a | **LANDED 2026-09-04 after the owner-authorized §21 evidence exception.** Reviewed scratch stack `44e2bc227b` → `b6bf6c131` → `eea5793d2` → `14397b14c` → `359061b82` → `255194e90` → `9cfddf301`; campaign cherry-picks `a86ec73ec` → `8e0c6fb14` → `01674bcc7` → `8bd75ba31` → `b6b015604` → `316193043` → `67c76026e`. The earlier stop remains in history at `06b986622`. | G3/G4 UNPASSED | Final retail lens PASS; production behavior/lifecycle/allocation/scope clean. The narrow independent gate-honesty re-review PASS reproduced the exact first failure (AP-table-boundary `Assert.True`, `rowIndex=-1`, before `Assert.Single`), restored AP-241 byte-identically, and passed the pin 1/1. Fresh campaign: Release 0W/0E; focused class lane 96/96; no-surface 1/1; AP pin 1/1; real allocation 2/2 at 0 B; shader 32/32; `git diff --check` clean. No client launched. NEXT: write c3b's deletion-only contract from the landed code; G3 stays locked until c3b lands. |
|
| S4-c3a | **LANDED 2026-09-04 after the owner-authorized §21 evidence exception.** Reviewed scratch stack `44e2bc227b` → `b6bf6c131` → `eea5793d2` → `14397b14c` → `359061b82` → `255194e90` → `9cfddf301`; campaign cherry-picks `a86ec73ec` → `8e0c6fb14` → `01674bcc7` → `8bd75ba31` → `b6b015604` → `316193043` → `67c76026e`. The earlier stop remains in history at `06b986622`. | G3/G4 UNPASSED | Final retail lens PASS; production behavior/lifecycle/allocation/scope clean. The narrow independent gate-honesty re-review PASS reproduced the exact first failure (AP-table-boundary `Assert.True`, `rowIndex=-1`, before `Assert.Single`), restored AP-241 byte-identically, and passed the pin 1/1. Fresh campaign: Release 0W/0E; focused class lane 96/96; no-surface 1/1; AP pin 1/1; real allocation 2/2 at 0 B; shader 32/32; `git diff --check` clean. No client launched. NEXT: write c3b's deletion-only contract from the landed code; G3 stays locked until c3b lands. |
|
||||||
| S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22–§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. |
|
| S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22–§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. |
|
||||||
| S5-c1 | **LANDED 2026-09-04 after the owner-authorized documentation/evidence exception.** Mandatory stop `efb075619` remains in history; final campaign tip `1718832e2`, reviewed scratch tip `b2d56f9e3`. | G4 UNPASSED | Narrow exception re-review PASS. Clean evidence: hermetic 16,760/16,760; inclusive InstalledDat 385 pass / the same documented 10 fail / 1 skip; both exact `d6592d3ac`, `WorktreeDirty=false`, manifests fully verified. Fresh campaign Release 0W/0E, Core 111/111, visibility 38/38, allocation/AP-116 4/4. Lead graphical portal-haze gate PROVISIONAL PASS, exit 0/graceful, 10 PNGs under `logs/selfgate-20260904-171137-s5c1-particle-visibility`; owner inspection pending. |
|
| S5-c1 | **LANDED 2026-09-04 after the owner-authorized documentation/evidence exception.** Mandatory stop `efb075619` remains in history; final campaign tip `1718832e2`, reviewed scratch tip `b2d56f9e3`. | G4 UNPASSED | Narrow exception re-review PASS. Clean evidence: hermetic 16,760/16,760; inclusive InstalledDat 385 pass / the same documented 10 fail / 1 skip; both exact `d6592d3ac`, `WorktreeDirty=false`, manifests fully verified. Fresh campaign Release 0W/0E, Core 111/111, visibility 38/38, allocation/AP-116 4/4. Lead graphical portal-haze gate PROVISIONAL PASS, exit 0/graceful, 10 PNGs under `logs/selfgate-20260904-171137-s5c1-particle-visibility`; owner inspection pending. |
|
||||||
| S5-c2 | **CONTRACTED 2026-09-04; Sonnet implementation next.** Exact prior-completed landscape visibility selects opt-in IA-24 terrain/world shadow casters through S2 CELLARRAY/building placement facts; pack-off remains retail and untouched. | G4 UNPASSED | Packet §14 binding: one borrowed set, zero fallback, arbitrary-instance exactness, no retained topology rebuild on camera-only change, 0-B warmed path, dual sequential review. |
|
| S5-c2 | **IMPLEMENTED 2026-09-04; sequential reviews pending.** The opt-in IA-24 path borrows the exact prior-completed landscape set/flag, selects world casters through S2 CELLARRAY or building `EffectCellId`, selects authored terrain slots, and projects arbitrary active instance runs without rebuilding retained topology. Pack-off remains unchanged. | G4 UNPASSED | Packet §14 implementation result: Release 0W/0E; focused production lane 137/137; warmed caster/prepared/terrain selection pins 0 B; all seven required sabotages failed at the named first assertions and were restored. Clean exact-commit official evidence is the remaining implementer return before review. |
|
||||||
| S5 | IN FLIGHT — c1 landed/provisionally self-gated; c2 contracted; c3–c5 and closeout remain | G4 | Packet §§7 and 12–14 are binding; never merge main before G4. |
|
| S5 | IN FLIGHT — c1 landed/provisionally self-gated; c2 implementation candidate awaiting clean evidence + dual review; c3–c5 and closeout remain | G4 | Packet §§7 and 12–14 are binding; never merge main before G4. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -732,3 +732,76 @@ reconstruction, arbitrary-instance correctness, no topology rebuild on camera
|
||||||
motion, transform/GPU flight safety, 0-B steady state, lifecycle/deferral,
|
motion, transform/GPU flight safety, 0-B steady state, lifecycle/deferral,
|
||||||
allowed scope, counts, and at least three mutation claims. A failed lens gets
|
allowed scope, counts, and at least three mutation claims. A failed lens gets
|
||||||
one bounded fix contract; a third fix round stops and is written up.
|
one bounded fix contract; a third fix round stops and is written up.
|
||||||
|
|
||||||
|
### 14.7 S5-c2 implementation result — clean evidence pending
|
||||||
|
|
||||||
|
The bounded implementation is complete for review. The selected atmospheric
|
||||||
|
pack now borrows the existing visibility controller's exact prior-completed
|
||||||
|
set and completed-view flag by reference. The frame captures that fact before
|
||||||
|
the current transaction begins and borrows S2's retained CELLARRAY owner
|
||||||
|
through a typed read-only adapter. No second visibility set, cell index, or
|
||||||
|
fallback was added.
|
||||||
|
|
||||||
|
The retained caster and terrain products now have separate active-selection
|
||||||
|
projections. Ordinary casters intersect their non-empty retail CELLARRAY with
|
||||||
|
the completed landscape set; building shells use outdoor `EffectCellId`;
|
||||||
|
missing membership and no/empty completed views fail closed. The established
|
||||||
|
outdoor family remains `low != 0 && low < 0x0100` (including `0x41`), while a
|
||||||
|
terrain slot scans only its authored 1..64 cells. Arbitrary selected instances
|
||||||
|
inside one material command are emitted as exact contiguous
|
||||||
|
`BaseInstance`/`InstanceCount` runs. Active indirect publication advances its
|
||||||
|
own sequence; caster, prepared-draw, and terrain topology sequences remain
|
||||||
|
unchanged on visibility-only frames. Terrain snapshot publication also marks
|
||||||
|
an aborted build invalid so the next frame retries rather than accepting a
|
||||||
|
partially written snapshot.
|
||||||
|
|
||||||
|
Built-in and declared directional-shadow graphs call the same typed selector
|
||||||
|
and pass the same prior product into terrain selection. The existing
|
||||||
|
production pack-off integration remains unchanged and proves no active pack
|
||||||
|
runtime, shadow resource, or shadow work. IA-24 now states the prior-view/S2
|
||||||
|
selection adaptation and keeps the shadow map explicitly non-retail.
|
||||||
|
|
||||||
|
Pre-commit automated evidence:
|
||||||
|
|
||||||
|
- Release solution build: PASS, 0 warnings / 0 errors.
|
||||||
|
- focused visibility/frame/caster/prepared/GPU/terrain/built-in/declared/
|
||||||
|
pack-off/long-cycle lane: 137 passed, 0 failed, 0 skipped;
|
||||||
|
- warmed production selection: caster selector, arbitrary-run projection, and
|
||||||
|
terrain projection each measured 0 managed bytes while retained scratch and
|
||||||
|
topology build sequences stayed stable;
|
||||||
|
- `git diff --check`: PASS.
|
||||||
|
|
||||||
|
The required mutations were each built, run against the named production pin,
|
||||||
|
and restored exactly. Their actual first failures were:
|
||||||
|
|
||||||
|
1. CELLARRAY → `ParentCellId`:
|
||||||
|
`PriorLandscapeSelection_UsesExactCellArrayAndBuildingEffectCell`, first
|
||||||
|
`Assert.Equal` expected `[201, 202, 203, 205]`, actual `[204, 205]`.
|
||||||
|
2. Select every resident terrain range:
|
||||||
|
`PriorLandscapeSelection_ScansExactAuthoredEightByEightCells`, first
|
||||||
|
`Assert.Single` found 3 commands.
|
||||||
|
3. Building anchor → placement effect cell:
|
||||||
|
`PriorLandscapeSelection_UsesExactCellArrayAndBuildingEffectCell`, first
|
||||||
|
`Assert.Equal` expected `[201, 202, 203, 205]`, actual
|
||||||
|
`[201, 202, 203, 206]`.
|
||||||
|
4. Admit missing membership:
|
||||||
|
the same caster pin's first `Assert.Equal` expected
|
||||||
|
`[201, 202, 203, 205]`, actual `[201, 202, 203, 204, 205]`.
|
||||||
|
5. Publish current building scratch instead of the completed product:
|
||||||
|
`BorrowedLandscapeFrame_TracksOnlyCompletedTransactionsByReference`, first
|
||||||
|
`Assert.True` expected `HasCompletedWorldView=true`, actual `false` after
|
||||||
|
the completed empty frame.
|
||||||
|
6. Advance retained caster topology during selection:
|
||||||
|
`PriorLandscapeSelection_UsesExactCellArrayAndBuildingEffectCell`, first
|
||||||
|
topology `Assert.Equal` expected `BuildSequence=1`, actual `2`.
|
||||||
|
7. Collapse alternating selection into one prefix instance count:
|
||||||
|
`AlternatingCasterSelection_EmitsExactContiguousInstanceRuns`, first
|
||||||
|
active-command-count `Assert.Equal` expected 3, actual 1.
|
||||||
|
|
||||||
|
The one bounded implementation commit is made before the official lanes.
|
||||||
|
Official hermetic and inclusive InstalledDat evidence will run only from that
|
||||||
|
exact clean commit, with `WorktreeDirty=false`, under
|
||||||
|
`artifacts/s5-c2-hermetic-20260904` and
|
||||||
|
`artifacts/s5-c2-installed-20260904`. Those post-commit artifacts supersede no
|
||||||
|
earlier run and will be reported directly; this result does not invent their
|
||||||
|
counts.
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,9 @@ internal sealed class FrameRootCompositionPhase
|
||||||
live.EquippedChildren),
|
live.EquippedChildren),
|
||||||
new RuntimeWorldFrameBuildingSource(
|
new RuntimeWorldFrameBuildingSource(
|
||||||
live.LandblockPipeline,
|
live.LandblockPipeline,
|
||||||
d.CellVisibility));
|
d.CellVisibility),
|
||||||
|
new RuntimeDirectionalShadowCellMembership(
|
||||||
|
d.PhysicsEngine.ShadowObjects));
|
||||||
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
|
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
|
||||||
d.FrameDiagnosticsEnabled,
|
d.FrameDiagnosticsEnabled,
|
||||||
worldRenderDiagnostics,
|
worldRenderDiagnostics,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Packs;
|
using AcDream.App.Rendering.Packs;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -39,6 +40,7 @@ internal readonly record struct DirectionalSunShadowRenderInput(
|
||||||
bool MeasureGpuTimers = true,
|
bool MeasureGpuTimers = true,
|
||||||
bool MeasureCpuStages = false,
|
bool MeasureCpuStages = false,
|
||||||
AtmosphericFrameBufferBinding AtmosphericFrame = default,
|
AtmosphericFrameBufferBinding AtmosphericFrame = default,
|
||||||
|
RetailLandscapeVisibilityFrame PriorLandscapeVisibility = default,
|
||||||
// #429 owner-approved pipelining: false keeps the retained caster/draw
|
// #429 owner-approved pipelining: false keeps the retained caster/draw
|
||||||
// topology this frame (transform refresh only) so the rebuild lands on a
|
// topology this frame (transform refresh only) so the rebuild lands on a
|
||||||
// quieter frame. The prepare seams below re-validate and rebuild anyway
|
// quieter frame. The prepare seams below re-validate and rebuild anyway
|
||||||
|
|
@ -73,7 +75,8 @@ internal readonly record struct DirectionalShadowTransformChurnDiagnostics(
|
||||||
bool FlightFullDynamicFallback,
|
bool FlightFullDynamicFallback,
|
||||||
bool DenseDirectUpload,
|
bool DenseDirectUpload,
|
||||||
bool DenseFlightReplay,
|
bool DenseFlightReplay,
|
||||||
DirectionalShadowCasterClassDiagnostics CasterClasses = default);
|
DirectionalShadowCasterClassDiagnostics CasterClasses = default,
|
||||||
|
int ActiveSelectedCasters = 0);
|
||||||
|
|
||||||
internal readonly record struct DirectionalSunShadowDiagnostics(
|
internal readonly record struct DirectionalSunShadowDiagnostics(
|
||||||
DirectionalShadowGateReason GateReason,
|
DirectionalShadowGateReason GateReason,
|
||||||
|
|
@ -96,7 +99,15 @@ internal readonly record struct DirectionalSunShadowDiagnostics(
|
||||||
int SourceObjectIndex = -1,
|
int SourceObjectIndex = -1,
|
||||||
uint SourceGfxObjId = 0u,
|
uint SourceGfxObjId = 0u,
|
||||||
Vector3 SurfaceToLightDirection = default,
|
Vector3 SurfaceToLightDirection = default,
|
||||||
float LightElevationSin = 0f);
|
float LightElevationSin = 0f,
|
||||||
|
int ResidentWorldCasters = 0,
|
||||||
|
int ActiveWorldCasters = 0,
|
||||||
|
int ResidentWorldInstances = 0,
|
||||||
|
int ActiveWorldInstances = 0,
|
||||||
|
int ResidentWorldCommands = 0,
|
||||||
|
int ActiveWorldCommands = 0,
|
||||||
|
int ResidentTerrainCommands = 0,
|
||||||
|
int ActiveTerrainCommands = 0);
|
||||||
|
|
||||||
internal static class DirectionalShadowBatchFlags
|
internal static class DirectionalShadowBatchFlags
|
||||||
{
|
{
|
||||||
|
|
@ -292,9 +303,9 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
internal DirectionalShadowFrameBinding CurrentFrameBinding => _currentFrameBinding;
|
internal DirectionalShadowFrameBinding CurrentFrameBinding => _currentFrameBinding;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Topology-only command metadata lives in pack-owned device-local buffers.
|
/// The bounded active indirect projection lives in pack-owned device-local
|
||||||
/// It is rebuilt transactionally when the retained CPU product changes and
|
/// buffers. Selection republishes these buffers without rebuilding the
|
||||||
/// is never copied through a per-frame ring on a stable scene.
|
/// retained CPU topology and never copies them through a per-frame ring.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal long RetainedCommandBufferBytes => checked(
|
internal long RetainedCommandBufferBytes => checked(
|
||||||
(_worldBatchBuffer?.SizeBytes ?? 0L)
|
(_worldBatchBuffer?.SizeBytes ?? 0L)
|
||||||
|
|
@ -402,10 +413,15 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
world.PrepareDirectionalShadowDraws(
|
world.PrepareDirectionalShadowDraws(
|
||||||
input.Casters,
|
input.Casters,
|
||||||
input.AllowTopologyRebuild);
|
input.AllowTopologyRebuild);
|
||||||
|
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
|
||||||
|
input.PriorLandscapeVisibility;
|
||||||
DirectionalShadowTerrainPreparedDraws terrainDraws =
|
DirectionalShadowTerrainPreparedDraws terrainDraws =
|
||||||
terrain.PrepareDirectionalShadowDraws();
|
terrain.PrepareDirectionalShadowDraws(
|
||||||
|
in priorLandscapeVisibility);
|
||||||
DirectionalShadowMeshGeometry? worldGeometry =
|
DirectionalShadowMeshGeometry? worldGeometry =
|
||||||
worldDraws.Commands.IsEmpty ? null : world.GetDirectionalShadowGeometry();
|
worldDraws.ActiveCommands.IsEmpty
|
||||||
|
? null
|
||||||
|
: world.GetDirectionalShadowGeometry();
|
||||||
DirectionalShadowTerrainGeometry? terrainGeometry =
|
DirectionalShadowTerrainGeometry? terrainGeometry =
|
||||||
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
|
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
|
||||||
uint transformBindingSizeBytes =
|
uint transformBindingSizeBytes =
|
||||||
|
|
@ -433,7 +449,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
DirectionalShadowCasterClassDiagnostics casterClasses =
|
DirectionalShadowCasterClassDiagnostics casterClasses =
|
||||||
CompleteCasterClassDiagnostics(
|
CompleteCasterClassDiagnostics(
|
||||||
in casterStats,
|
in casterStats,
|
||||||
terrainDraws.Commands.Length);
|
terrainDraws.ResidentRanges.Length);
|
||||||
DirectionalShadowTransformPublishStats publishStats =
|
DirectionalShadowTransformPublishStats publishStats =
|
||||||
_transformBuffers.LastStats;
|
_transformBuffers.LastStats;
|
||||||
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
|
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
|
||||||
|
|
@ -457,7 +473,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
publishStats.UsedFullDynamicFallback,
|
publishStats.UsedFullDynamicFallback,
|
||||||
publishStats.DenseDirectUpload,
|
publishStats.DenseDirectUpload,
|
||||||
publishStats.DenseFlightReplay,
|
publishStats.DenseFlightReplay,
|
||||||
casterClasses);
|
casterClasses,
|
||||||
|
casterStats.ActiveSelected);
|
||||||
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
|
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
|
||||||
? Stopwatch.GetTimestamp() - cpuStageStarted
|
? Stopwatch.GetTimestamp() - cpuStageStarted
|
||||||
: 0L;
|
: 0L;
|
||||||
|
|
@ -661,7 +678,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
PublishDisabledReceiverBinding(frame, atmosphericFrame);
|
PublishDisabledReceiverBinding(frame, atmosphericFrame);
|
||||||
return Disabled(in environment, cpuStages);
|
return Disabled(in environment, cpuStages);
|
||||||
}
|
}
|
||||||
if (!worldDraws.Commands.IsEmpty && worldGeometry is null)
|
if (!worldDraws.ActiveCommands.IsEmpty && worldGeometry is null)
|
||||||
throw new ArgumentNullException(nameof(worldGeometry));
|
throw new ArgumentNullException(nameof(worldGeometry));
|
||||||
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
|
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
|
||||||
throw new ArgumentNullException(nameof(terrainGeometry));
|
throw new ArgumentNullException(nameof(terrainGeometry));
|
||||||
|
|
@ -812,9 +829,10 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
|
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
|
||||||
drawsPerCascade = checked(
|
drawsPerCascade = checked(
|
||||||
drawsPerCascade
|
drawsPerCascade
|
||||||
+ (worldDraws.Commands.IsEmpty
|
+ (worldDraws.ActiveCommands.IsEmpty
|
||||||
? 0
|
? 0
|
||||||
: worldDraws.OpaqueRuns.Length + worldDraws.AlphaCutoutRuns.Length));
|
: worldDraws.ActiveOpaqueRuns.Length
|
||||||
|
+ worldDraws.ActiveAlphaCutoutRuns.Length));
|
||||||
long finished = Stopwatch.GetTimestamp();
|
long finished = Stopwatch.GetTimestamp();
|
||||||
cpuStages = cpuStages with
|
cpuStages = cpuStages with
|
||||||
{
|
{
|
||||||
|
|
@ -833,8 +851,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
environment.Strength,
|
environment.Strength,
|
||||||
cascadeCount,
|
cascadeCount,
|
||||||
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
|
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
|
||||||
worldDraws.OpaqueCommandCount,
|
worldDraws.ActiveOpaqueCommandCount,
|
||||||
worldDraws.AlphaCutoutCommandCount,
|
worldDraws.ActiveAlphaCutoutCommandCount,
|
||||||
terrainDraws.Commands.Length,
|
terrainDraws.Commands.Length,
|
||||||
worldDraws.BuildSequence,
|
worldDraws.BuildSequence,
|
||||||
terrainDraws.BuildSequence,
|
terrainDraws.BuildSequence,
|
||||||
|
|
@ -848,7 +866,30 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
environment.SourceObjectIndex,
|
environment.SourceObjectIndex,
|
||||||
environment.SourceGfxObjId,
|
environment.SourceGfxObjId,
|
||||||
environment.SurfaceToLightDirection,
|
environment.SurfaceToLightDirection,
|
||||||
environment.LightElevationSin);
|
environment.LightElevationSin,
|
||||||
|
ResidentWorldCasters: inputCasterClassesCount(transformChurn),
|
||||||
|
ActiveWorldCasters: transformChurn.ActiveSelectedCasters,
|
||||||
|
ResidentWorldInstances: worldDraws.Stats.PreparedInstances,
|
||||||
|
ActiveWorldInstances: worldDraws.Stats.ActiveInstances,
|
||||||
|
ResidentWorldCommands: worldDraws.Commands.Length,
|
||||||
|
ActiveWorldCommands: worldDraws.ActiveCommands.Length,
|
||||||
|
ResidentTerrainCommands: terrainDraws.ResidentRanges.Length,
|
||||||
|
ActiveTerrainCommands: terrainDraws.Commands.Length);
|
||||||
|
|
||||||
|
static int inputCasterClassesCount(
|
||||||
|
DirectionalShadowTransformChurnDiagnostics churn)
|
||||||
|
{
|
||||||
|
DirectionalShadowCasterClassDiagnostics classes = churn.CasterClasses;
|
||||||
|
return checked(
|
||||||
|
classes.OutdoorStatics
|
||||||
|
+ classes.Buildings
|
||||||
|
+ classes.AnimatedStatics
|
||||||
|
+ classes.LocalPlayers
|
||||||
|
+ classes.RemotePlayers
|
||||||
|
+ classes.NonPlayerCreatures
|
||||||
|
+ classes.OtherLiveDynamics
|
||||||
|
+ classes.EquippedChildren);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PreparedGpuUploads PrepareGpuData(
|
private PreparedGpuUploads PrepareGpuData(
|
||||||
|
|
@ -856,9 +897,9 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
DirectionalShadowPreparedDraws world,
|
DirectionalShadowPreparedDraws world,
|
||||||
DirectionalShadowTerrainPreparedDraws terrain)
|
DirectionalShadowTerrainPreparedDraws terrain)
|
||||||
{
|
{
|
||||||
if (_worldGpuBuildSequence != world.BuildSequence)
|
if (_worldGpuBuildSequence != world.ActiveSelectionSequence)
|
||||||
RebuildWorldGpuData(world);
|
RebuildWorldGpuData(world);
|
||||||
if (_terrainGpuBuildSequence != terrain.BuildSequence)
|
if (_terrainGpuBuildSequence != terrain.ActiveSelectionSequence)
|
||||||
RebuildTerrainGpuData(terrain);
|
RebuildTerrainGpuData(terrain);
|
||||||
|
|
||||||
return new PreparedGpuUploads(
|
return new PreparedGpuUploads(
|
||||||
|
|
@ -874,12 +915,12 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
IGpuBuffer? commands = null;
|
IGpuBuffer? commands = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!world.Commands.IsEmpty)
|
if (!world.ActiveCommands.IsEmpty)
|
||||||
{
|
{
|
||||||
EnsureBatchCapacity(world.Batches.Length);
|
EnsureBatchCapacity(world.ActiveBatches.Length);
|
||||||
for (int i = 0; i < world.Batches.Length; i++)
|
for (int i = 0; i < world.ActiveBatches.Length; i++)
|
||||||
{
|
{
|
||||||
DirectionalShadowPreparedBatch batch = world.Batches[i];
|
DirectionalShadowPreparedBatch batch = world.ActiveBatches[i];
|
||||||
_batchScratch[i] = new DirectionalShadowBatchGpuData(
|
_batchScratch[i] = new DirectionalShadowBatchGpuData(
|
||||||
batch.TextureSlot.Index,
|
batch.TextureSlot.Index,
|
||||||
0u,
|
0u,
|
||||||
|
|
@ -892,15 +933,15 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
|
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
|
||||||
_batchScratch.AsSpan(0, world.Batches.Length));
|
_batchScratch.AsSpan(0, world.ActiveBatches.Length));
|
||||||
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
|
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
|
||||||
world.Commands);
|
world.ActiveCommands);
|
||||||
batches = CreateRetainedBuffer(
|
batches = CreateRetainedBuffer(
|
||||||
$"directional-shadow-world-batches-{world.BuildSequence}",
|
$"directional-shadow-world-batches-{world.ActiveSelectionSequence}",
|
||||||
batchBytes,
|
batchBytes,
|
||||||
GpuBufferUsage.Storage);
|
GpuBufferUsage.Storage);
|
||||||
commands = CreateRetainedBuffer(
|
commands = CreateRetainedBuffer(
|
||||||
$"directional-shadow-world-commands-{world.BuildSequence}",
|
$"directional-shadow-world-commands-{world.ActiveSelectionSequence}",
|
||||||
commandBytes,
|
commandBytes,
|
||||||
GpuBufferUsage.Indirect);
|
GpuBufferUsage.Indirect);
|
||||||
}
|
}
|
||||||
|
|
@ -916,7 +957,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
IGpuBuffer? previousCommands = _worldCommandBuffer;
|
IGpuBuffer? previousCommands = _worldCommandBuffer;
|
||||||
_worldBatchBuffer = batches;
|
_worldBatchBuffer = batches;
|
||||||
_worldCommandBuffer = commands;
|
_worldCommandBuffer = commands;
|
||||||
_worldGpuBuildSequence = world.BuildSequence;
|
_worldGpuBuildSequence = world.ActiveSelectionSequence;
|
||||||
previousCommands?.Dispose();
|
previousCommands?.Dispose();
|
||||||
previousBatches?.Dispose();
|
previousBatches?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -927,14 +968,14 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
if (!terrain.Commands.IsEmpty)
|
if (!terrain.Commands.IsEmpty)
|
||||||
{
|
{
|
||||||
commands = CreateRetainedBuffer(
|
commands = CreateRetainedBuffer(
|
||||||
$"directional-shadow-terrain-commands-{terrain.BuildSequence}",
|
$"directional-shadow-terrain-commands-{terrain.ActiveSelectionSequence}",
|
||||||
MemoryMarshal.AsBytes(terrain.Commands),
|
MemoryMarshal.AsBytes(terrain.Commands),
|
||||||
GpuBufferUsage.Indirect);
|
GpuBufferUsage.Indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
IGpuBuffer? previous = _terrainCommandBuffer;
|
IGpuBuffer? previous = _terrainCommandBuffer;
|
||||||
_terrainCommandBuffer = commands;
|
_terrainCommandBuffer = commands;
|
||||||
_terrainGpuBuildSequence = terrain.BuildSequence;
|
_terrainGpuBuildSequence = terrain.ActiveSelectionSequence;
|
||||||
previous?.Dispose();
|
previous?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -997,7 +1038,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
IGpuPipeline? opaquePipeline = null,
|
IGpuPipeline? opaquePipeline = null,
|
||||||
IGpuPipeline? cutoutPipeline = null)
|
IGpuPipeline? cutoutPipeline = null)
|
||||||
{
|
{
|
||||||
if (draws.Commands.IsEmpty)
|
if (draws.ActiveCommands.IsEmpty)
|
||||||
return;
|
return;
|
||||||
DirectionalShadowMeshGeometry actual = geometry!.Value;
|
DirectionalShadowMeshGeometry actual = geometry!.Value;
|
||||||
encoder.BindStorageBuffer(
|
encoder.BindStorageBuffer(
|
||||||
|
|
@ -1013,14 +1054,14 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
||||||
DrawWorldRange(
|
DrawWorldRange(
|
||||||
encoder,
|
encoder,
|
||||||
uploads.WorldCommands,
|
uploads.WorldCommands,
|
||||||
draws.OpaqueRuns,
|
draws.ActiveOpaqueRuns,
|
||||||
cascadeIndex,
|
cascadeIndex,
|
||||||
opaquePipeline ?? _worldOpaquePipeline,
|
opaquePipeline ?? _worldOpaquePipeline,
|
||||||
actual);
|
actual);
|
||||||
DrawWorldRange(
|
DrawWorldRange(
|
||||||
encoder,
|
encoder,
|
||||||
uploads.WorldCommands,
|
uploads.WorldCommands,
|
||||||
draws.AlphaCutoutRuns,
|
draws.ActiveAlphaCutoutRuns,
|
||||||
cascadeIndex,
|
cascadeIndex,
|
||||||
cutoutPipeline ?? _worldCutoutPipeline,
|
cutoutPipeline ?? _worldCutoutPipeline,
|
||||||
actual);
|
actual);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Plugin.Abstractions.Rendering;
|
using AcDream.Plugin.Abstractions.Rendering;
|
||||||
|
|
@ -421,6 +422,12 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
!shadowInputsChanged || _shadowRebuildDeferrals >= 2;
|
!shadowInputsChanged || _shadowRebuildDeferrals >= 2;
|
||||||
ulong casterSequenceBefore = _shadowCasters.BuildSequence;
|
ulong casterSequenceBefore = _shadowCasters.BuildSequence;
|
||||||
_shadowCasters.Build(in scene, allowTopologyRebuild);
|
_shadowCasters.Build(in scene, allowTopologyRebuild);
|
||||||
|
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
|
||||||
|
world.PriorLandscapeVisibility;
|
||||||
|
_shadowCasters.Select(
|
||||||
|
in priorLandscapeVisibility,
|
||||||
|
world.DirectionalShadowCellMembership
|
||||||
|
?? EmptyDirectionalShadowCellMembership.Instance);
|
||||||
if (_shadowCasters.BuildSequence != casterSequenceBefore)
|
if (_shadowCasters.BuildSequence != casterSequenceBefore)
|
||||||
allowTopologyRebuild = true;
|
allowTopologyRebuild = true;
|
||||||
_shadowRebuildDeferrals = allowTopologyRebuild
|
_shadowRebuildDeferrals = allowTopologyRebuild
|
||||||
|
|
@ -463,9 +470,10 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
frame.Serial),
|
frame.Serial),
|
||||||
MeasureCpuStages: measureCpuStages,
|
MeasureCpuStages: measureCpuStages,
|
||||||
AtmosphericFrame: shadowAtmosphericFrame,
|
AtmosphericFrame: shadowAtmosphericFrame,
|
||||||
|
PriorLandscapeVisibility: world.PriorLandscapeVisibility,
|
||||||
AllowTopologyRebuild: allowTopologyRebuild);
|
AllowTopologyRebuild: allowTopologyRebuild);
|
||||||
long environmentFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
long environmentFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||||
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
|
_lastShadowCasterCount = _shadowCasters.Stats.ActiveSelected;
|
||||||
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
||||||
_lastShadowDiagnostics = _directionalShadows.Render(
|
_lastShadowDiagnostics = _directionalShadows.Render(
|
||||||
frame,
|
frame,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Plugin.Abstractions.Rendering;
|
using AcDream.Plugin.Abstractions.Rendering;
|
||||||
|
|
@ -201,6 +202,12 @@ internal class DeclaredFullscreenRenderPackGraph :
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"Pack '{Descriptor.Id}' has no declared directional-shadow executor.");
|
$"Pack '{Descriptor.Id}' has no declared directional-shadow executor.");
|
||||||
_shadowCasters.Build(in scene);
|
_shadowCasters.Build(in scene);
|
||||||
|
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
|
||||||
|
world.PriorLandscapeVisibility;
|
||||||
|
_shadowCasters.Select(
|
||||||
|
in priorLandscapeVisibility,
|
||||||
|
world.DirectionalShadowCellMembership
|
||||||
|
?? EmptyDirectionalShadowCellMembership.Instance);
|
||||||
AuthoredCelestialShadowSource source = world.CelestialShadowSource;
|
AuthoredCelestialShadowSource source = world.CelestialShadowSource;
|
||||||
float elevationStrength = RenderPackAtmospherePolicyEvaluation
|
float elevationStrength = RenderPackAtmospherePolicyEvaluation
|
||||||
.DirectionalShadowFromSin(
|
.DirectionalShadowFromSin(
|
||||||
|
|
@ -225,8 +232,9 @@ internal class DeclaredFullscreenRenderPackGraph :
|
||||||
world.Camera.Projection,
|
world.Camera.Projection,
|
||||||
_shadowCasters,
|
_shadowCasters,
|
||||||
ResidentMaximumReachMeters:
|
ResidentMaximumReachMeters:
|
||||||
world.ResidentStreamingWindow.MaximumReachMeters);
|
world.ResidentStreamingWindow.MaximumReachMeters,
|
||||||
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
|
PriorLandscapeVisibility: world.PriorLandscapeVisibility);
|
||||||
|
_lastShadowCasterCount = _shadowCasters.Stats.ActiveSelected;
|
||||||
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
||||||
_lastShadowDiagnostics = renderer.Render(
|
_lastShadowDiagnostics = renderer.Render(
|
||||||
frame,
|
frame,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -80,7 +82,8 @@ internal readonly record struct DirectionalShadowCasterBuildStats(
|
||||||
int LiveDynamicRootChanges = 0,
|
int LiveDynamicRootChanges = 0,
|
||||||
int EquippedChildChanges = 0,
|
int EquippedChildChanges = 0,
|
||||||
bool DensityBulkRefresh = false,
|
bool DensityBulkRefresh = false,
|
||||||
int BatchedProjectionCopyCalls = 0)
|
int BatchedProjectionCopyCalls = 0,
|
||||||
|
int ActiveSelected = 0)
|
||||||
{
|
{
|
||||||
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
|
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +99,7 @@ internal sealed class DirectionalShadowCasterFrame
|
||||||
private RenderProjectionRecord[] _outdoorStaticScratch = [];
|
private RenderProjectionRecord[] _outdoorStaticScratch = [];
|
||||||
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
|
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
|
||||||
private DirectionalShadowCaster[] _casters = [];
|
private DirectionalShadowCaster[] _casters = [];
|
||||||
|
private bool[] _selectedCasters = [];
|
||||||
private int[] _refreshCasterSlots = [];
|
private int[] _refreshCasterSlots = [];
|
||||||
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
|
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
|
||||||
private bool[] _changedCasterFlags = [];
|
private bool[] _changedCasterFlags = [];
|
||||||
|
|
@ -123,9 +127,16 @@ internal sealed class DirectionalShadowCasterFrame
|
||||||
|
|
||||||
public ulong BuildSequence { get; private set; }
|
public ulong BuildSequence { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Per-frame active-selection publication identity. It is
|
||||||
|
/// intentionally independent from retained topology <see cref="BuildSequence"/>.</summary>
|
||||||
|
public ulong SelectionSequence { get; private set; }
|
||||||
|
|
||||||
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
|
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
|
||||||
_casters.AsSpan(0, _casterCount);
|
_casters.AsSpan(0, _casterCount);
|
||||||
|
|
||||||
|
internal ReadOnlySpan<bool> SelectedCasters =>
|
||||||
|
_selectedCasters.AsSpan(0, _casterCount);
|
||||||
|
|
||||||
internal ReadOnlySpan<int> RefreshCasterSlots =>
|
internal ReadOnlySpan<int> RefreshCasterSlots =>
|
||||||
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
|
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
|
||||||
|
|
||||||
|
|
@ -144,6 +155,7 @@ internal sealed class DirectionalShadowCasterFrame
|
||||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||||
+ (long)_casters.Length
|
+ (long)_casters.Length
|
||||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
|
||||||
|
+ _selectedCasters.Length
|
||||||
+ (long)_refreshCasterSlots.Length * sizeof(int)
|
+ (long)_refreshCasterSlots.Length * sizeof(int)
|
||||||
+ (long)_changedCasterPoses.Length
|
+ (long)_changedCasterPoses.Length
|
||||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||||
|
|
@ -233,6 +245,7 @@ internal sealed class DirectionalShadowCasterFrame
|
||||||
RenderSceneIndex.OutdoorDynamic,
|
RenderSceneIndex.OutdoorDynamic,
|
||||||
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
|
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
|
||||||
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
|
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
|
||||||
|
EnsureCapacity(ref _selectedCasters, checked(staticCount + dynamicCount));
|
||||||
_casterCount = 0;
|
_casterCount = 0;
|
||||||
|
|
||||||
int rejectedNotDrawable = 0;
|
int rejectedNotDrawable = 0;
|
||||||
|
|
@ -394,6 +407,67 @@ internal sealed class DirectionalShadowCasterFrame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Projects the prior-completed retail landscape product onto the retained
|
||||||
|
/// caster topology. This performs no render-scene copy, classification,
|
||||||
|
/// mesh lookup, or topology revision change.
|
||||||
|
/// </summary>
|
||||||
|
internal void Select(
|
||||||
|
in RetailLandscapeVisibilityFrame visibility,
|
||||||
|
IDirectionalShadowCellMembership membership)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(membership);
|
||||||
|
IReadOnlySet<uint> visible = visibility.CellIds
|
||||||
|
?? RetailLandscapeVisibilityFrame.None.CellIds;
|
||||||
|
int selected = 0;
|
||||||
|
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
||||||
|
{
|
||||||
|
ref readonly DirectionalShadowCaster caster =
|
||||||
|
ref _casters[casterIndex];
|
||||||
|
bool active = visibility.HasCompletedWorldView
|
||||||
|
&& SelectsCaster(in caster, visible, membership);
|
||||||
|
_selectedCasters[casterIndex] = active;
|
||||||
|
if (active)
|
||||||
|
selected++;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionSequence = checked(SelectionSequence + 1);
|
||||||
|
Stats = Stats with { ActiveSelected = selected };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SelectsCaster(
|
||||||
|
in DirectionalShadowCaster caster,
|
||||||
|
IReadOnlySet<uint> visible,
|
||||||
|
IDirectionalShadowCellMembership membership)
|
||||||
|
{
|
||||||
|
RenderSourceMetadata source = caster.Projection.Source;
|
||||||
|
if (caster.Kind is DirectionalShadowCasterKind.Building)
|
||||||
|
return IsOutdoorLandCell(source.EffectCellId)
|
||||||
|
&& visible.Contains(source.EffectCellId);
|
||||||
|
|
||||||
|
if (!membership.TryGetRetailCellArray(
|
||||||
|
source.LocalEntityId,
|
||||||
|
out IReadOnlyList<uint>? cells)
|
||||||
|
|| cells.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
|
||||||
|
{
|
||||||
|
uint cellId = cells[cellIndex];
|
||||||
|
if (IsOutdoorLandCell(cellId) && visible.Contains(cellId))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsOutdoorLandCell(uint cellId)
|
||||||
|
{
|
||||||
|
uint low = cellId & 0xFFFFu;
|
||||||
|
return low != 0u && low < 0x0100u;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure pre-check for the deferral gate: would refreshing from the journal
|
/// Pure pre-check for the deferral gate: would refreshing from the journal
|
||||||
/// demand the dense by-id re-copy? The journal copy is a read; the state
|
/// demand the dense by-id re-copy? The journal copy is a read; the state
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,48 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
internal readonly record struct DirectionalShadowTerrainRange(
|
internal readonly record struct DirectionalShadowTerrainRange(
|
||||||
uint FirstIndex,
|
uint FirstIndex,
|
||||||
int IndexCount);
|
int IndexCount,
|
||||||
|
uint LandblockId = 0u);
|
||||||
|
|
||||||
internal readonly record struct DirectionalShadowTerrainGeometry(
|
internal readonly record struct DirectionalShadowTerrainGeometry(
|
||||||
IGpuBuffer VertexBuffer,
|
IGpuBuffer VertexBuffer,
|
||||||
IGpuBuffer IndexBuffer);
|
IGpuBuffer IndexBuffer);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The complete resident terrain arena expressed once as indirect commands.
|
/// The complete resident terrain arena expressed once as retained slot ranges,
|
||||||
/// It deliberately has no camera, PView, portal, or cascade input.
|
/// plus a separately sequenced active indirect projection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class DirectionalShadowTerrainPreparedDraws
|
internal sealed class DirectionalShadowTerrainPreparedDraws
|
||||||
{
|
{
|
||||||
private DrawElementsIndirectCommand[] _commands = [];
|
private DirectionalShadowTerrainRange[] _ranges = [];
|
||||||
private int _count;
|
private DrawElementsIndirectCommand[] _activeCommands = [];
|
||||||
|
private int _rangeCount;
|
||||||
|
private int _activeCount;
|
||||||
private bool _building;
|
private bool _building;
|
||||||
|
|
||||||
public long SourceFrameSequence { get; private set; }
|
public long SourceFrameSequence { get; private set; }
|
||||||
|
|
||||||
public ulong BuildSequence { get; private set; }
|
public ulong BuildSequence { get; private set; }
|
||||||
|
|
||||||
|
public ulong ActiveSelectionSequence { get; private set; }
|
||||||
|
|
||||||
public ReadOnlySpan<DrawElementsIndirectCommand> Commands =>
|
public ReadOnlySpan<DrawElementsIndirectCommand> Commands =>
|
||||||
_commands.AsSpan(0, _count);
|
_activeCommands.AsSpan(0, _activeCount);
|
||||||
|
|
||||||
|
public ReadOnlySpan<DirectionalShadowTerrainRange> ResidentRanges =>
|
||||||
|
_ranges.AsSpan(0, _rangeCount);
|
||||||
|
|
||||||
public long RetainedScratchBytes =>
|
public long RetainedScratchBytes =>
|
||||||
checked((long)_commands.Length
|
checked(
|
||||||
|
(long)_ranges.Length
|
||||||
|
* Unsafe.SizeOf<DirectionalShadowTerrainRange>()
|
||||||
|
+ (long)_activeCommands.Length
|
||||||
* Unsafe.SizeOf<DrawElementsIndirectCommand>());
|
* Unsafe.SizeOf<DrawElementsIndirectCommand>());
|
||||||
|
|
||||||
public bool TryBegin(long frameSequence, int estimatedCommands)
|
public bool TryBegin(long frameSequence, int estimatedCommands)
|
||||||
|
|
@ -45,7 +57,7 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
EnsureCapacity(estimatedCommands);
|
EnsureCapacity(estimatedCommands);
|
||||||
_count = 0;
|
_rangeCount = 0;
|
||||||
_building = true;
|
_building = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -57,15 +69,8 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
|
||||||
"Begin a terrain shadow draw build before adding ranges.");
|
"Begin a terrain shadow draw build before adding ranges.");
|
||||||
if (range.IndexCount <= 0)
|
if (range.IndexCount <= 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(range));
|
throw new ArgumentOutOfRangeException(nameof(range));
|
||||||
EnsureCapacity(checked(_count + 1));
|
EnsureCapacity(checked(_rangeCount + 1));
|
||||||
_commands[_count++] = new DrawElementsIndirectCommand
|
_ranges[_rangeCount++] = range;
|
||||||
{
|
|
||||||
Count = checked((uint)range.IndexCount),
|
|
||||||
InstanceCount = 1,
|
|
||||||
FirstIndex = range.FirstIndex,
|
|
||||||
BaseVertex = 0,
|
|
||||||
BaseInstance = 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Complete(long frameSequence)
|
public void Complete(long frameSequence)
|
||||||
|
|
@ -78,22 +83,73 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
|
||||||
SourceFrameSequence = frameSequence;
|
SourceFrameSequence = frameSequence;
|
||||||
BuildSequence = checked(BuildSequence + 1);
|
BuildSequence = checked(BuildSequence + 1);
|
||||||
_building = false;
|
_building = false;
|
||||||
|
RebuildActiveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ApplySelection(in RetailLandscapeVisibilityFrame visibility)
|
||||||
|
{
|
||||||
|
IReadOnlySet<uint> visible = visibility.CellIds
|
||||||
|
?? RetailLandscapeVisibilityFrame.None.CellIds;
|
||||||
|
_activeCount = 0;
|
||||||
|
if (visibility.HasCompletedWorldView)
|
||||||
|
{
|
||||||
|
for (int rangeIndex = 0; rangeIndex < _rangeCount; rangeIndex++)
|
||||||
|
{
|
||||||
|
DirectionalShadowTerrainRange range = _ranges[rangeIndex];
|
||||||
|
uint prefix = range.LandblockId & 0xFFFF0000u;
|
||||||
|
bool selected = false;
|
||||||
|
for (uint low = 1u; low <= 64u; low++)
|
||||||
|
{
|
||||||
|
if (visible.Contains(prefix | low))
|
||||||
|
{
|
||||||
|
selected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected)
|
||||||
|
Emit(in range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildActiveAll()
|
||||||
|
{
|
||||||
|
EnsureCapacity(_rangeCount);
|
||||||
|
_activeCount = 0;
|
||||||
|
for (int rangeIndex = 0; rangeIndex < _rangeCount; rangeIndex++)
|
||||||
|
Emit(in _ranges[rangeIndex]);
|
||||||
|
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Emit(in DirectionalShadowTerrainRange range)
|
||||||
|
{
|
||||||
|
_activeCommands[_activeCount++] = new DrawElementsIndirectCommand
|
||||||
|
{
|
||||||
|
Count = checked((uint)range.IndexCount),
|
||||||
|
InstanceCount = 1,
|
||||||
|
FirstIndex = range.FirstIndex,
|
||||||
|
BaseVertex = 0,
|
||||||
|
BaseInstance = 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort()
|
public void Abort()
|
||||||
{
|
{
|
||||||
_count = 0;
|
_rangeCount = 0;
|
||||||
|
_activeCount = 0;
|
||||||
_building = false;
|
_building = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureCapacity(int required)
|
private void EnsureCapacity(int required)
|
||||||
{
|
{
|
||||||
if (_commands.Length >= required)
|
if (_ranges.Length >= required)
|
||||||
return;
|
return;
|
||||||
int capacity = _commands.Length == 0 ? 16 : _commands.Length;
|
int capacity = _ranges.Length == 0 ? 16 : _ranges.Length;
|
||||||
while (capacity < required)
|
while (capacity < required)
|
||||||
capacity = checked(capacity * 2);
|
capacity = checked(capacity * 2);
|
||||||
Array.Resize(ref _commands, capacity);
|
Array.Resize(ref _ranges, capacity);
|
||||||
|
Array.Resize(ref _activeCommands, capacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,46 +157,107 @@ public sealed partial class TerrainModernRenderer
|
||||||
{
|
{
|
||||||
private readonly DirectionalShadowTerrainPreparedDraws
|
private readonly DirectionalShadowTerrainPreparedDraws
|
||||||
_directionalShadowTerrainDraws = new();
|
_directionalShadowTerrainDraws = new();
|
||||||
|
// BeginFrame's established overflow guard/frame counter remains part of
|
||||||
|
// the ordinary renderer lifecycle; shadow topology no longer keys on it.
|
||||||
private long _directionalShadowFrameSequence;
|
private long _directionalShadowFrameSequence;
|
||||||
|
private uint[] _directionalShadowSlotLandblocks = [];
|
||||||
|
private uint[] _directionalShadowSlotFirstIndices = [];
|
||||||
|
private int[] _directionalShadowSlotIndexCounts = [];
|
||||||
|
private bool[] _directionalShadowSlotPresent = [];
|
||||||
|
private bool _directionalShadowTopologySnapshotValid;
|
||||||
|
private long _directionalShadowTopologySequence;
|
||||||
|
|
||||||
internal DirectionalShadowTerrainGeometry GetDirectionalShadowGeometry() => new(
|
internal DirectionalShadowTerrainGeometry GetDirectionalShadowGeometry() => new(
|
||||||
_vertexStore ?? throw new InvalidOperationException("Terrain has no vertex store."),
|
_vertexStore ?? throw new InvalidOperationException("Terrain has no vertex store."),
|
||||||
_indexStore ?? throw new InvalidOperationException("Terrain has no index store."));
|
_indexStore ?? throw new InvalidOperationException("Terrain has no index store."));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds one all-resident indirect list for the current frame. Repeated
|
/// Retains exact loaded-slot topology and projects the borrowed prior-view
|
||||||
/// calls by individual cascades return the same retained product.
|
/// selection into a bounded indirect list. Camera-only changes do not
|
||||||
|
/// advance the topology build sequence or rebuild terrain geometry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal DirectionalShadowTerrainPreparedDraws
|
internal DirectionalShadowTerrainPreparedDraws
|
||||||
PrepareDirectionalShadowDraws()
|
PrepareDirectionalShadowDraws(
|
||||||
|
in RetailLandscapeVisibilityFrame visibility)
|
||||||
{
|
{
|
||||||
if (!_directionalShadowTerrainDraws.TryBegin(
|
EnsureDirectionalShadowSlotCapacity(_slots.Length);
|
||||||
_directionalShadowFrameSequence,
|
bool topologyChanged = !_directionalShadowTopologySnapshotValid;
|
||||||
_alloc.LoadedCount))
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
{
|
{
|
||||||
return _directionalShadowTerrainDraws;
|
SlotData? data = _slots[slot];
|
||||||
|
bool present = data is not null;
|
||||||
|
if (_directionalShadowSlotPresent[slot] != present
|
||||||
|
|| present
|
||||||
|
&& (_directionalShadowSlotLandblocks[slot] != data!.LandblockId
|
||||||
|
|| _directionalShadowSlotFirstIndices[slot] != data.FirstIndex
|
||||||
|
|| _directionalShadowSlotIndexCounts[slot] != data.IndexCount))
|
||||||
|
{
|
||||||
|
topologyChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (topologyChanged)
|
||||||
|
{
|
||||||
|
_directionalShadowTopologySequence = checked(
|
||||||
|
_directionalShadowTopologySequence + 1);
|
||||||
|
_directionalShadowTerrainDraws.TryBegin(
|
||||||
|
_directionalShadowTopologySequence,
|
||||||
|
_alloc.LoadedCount);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int slot = 0; slot < _slots.Length; slot++)
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
{
|
{
|
||||||
SlotData? data = _slots[slot];
|
SlotData? data = _slots[slot];
|
||||||
if (data is null)
|
bool present = data is not null;
|
||||||
|
_directionalShadowSlotPresent[slot] = present;
|
||||||
|
if (!present)
|
||||||
|
{
|
||||||
|
_directionalShadowSlotLandblocks[slot] = 0u;
|
||||||
|
_directionalShadowSlotFirstIndices[slot] = 0u;
|
||||||
|
_directionalShadowSlotIndexCounts[slot] = 0;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
_directionalShadowSlotLandblocks[slot] = data!.LandblockId;
|
||||||
|
_directionalShadowSlotFirstIndices[slot] = data.FirstIndex;
|
||||||
|
_directionalShadowSlotIndexCounts[slot] = data.IndexCount;
|
||||||
var range = new DirectionalShadowTerrainRange(
|
var range = new DirectionalShadowTerrainRange(
|
||||||
data.FirstIndex,
|
data.FirstIndex,
|
||||||
data.IndexCount);
|
data.IndexCount,
|
||||||
|
data.LandblockId);
|
||||||
_directionalShadowTerrainDraws.Add(in range);
|
_directionalShadowTerrainDraws.Add(in range);
|
||||||
}
|
}
|
||||||
_directionalShadowTerrainDraws.Complete(
|
_directionalShadowTerrainDraws.Complete(
|
||||||
_directionalShadowFrameSequence);
|
_directionalShadowTopologySequence);
|
||||||
return _directionalShadowTerrainDraws;
|
_directionalShadowTopologySnapshotValid = true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// The snapshot fields are populated while the retained
|
||||||
|
// product is built. If publication fails, force the next
|
||||||
|
// frame to retry even when those fields already match the
|
||||||
|
// live slots; an aborted build is never a valid topology.
|
||||||
|
_directionalShadowTopologySnapshotValid = false;
|
||||||
_directionalShadowTerrainDraws.Abort();
|
_directionalShadowTerrainDraws.Abort();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_directionalShadowTerrainDraws.ApplySelection(in visibility);
|
||||||
|
return _directionalShadowTerrainDraws;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureDirectionalShadowSlotCapacity(int required)
|
||||||
|
{
|
||||||
|
if (_directionalShadowSlotPresent.Length >= required)
|
||||||
|
return;
|
||||||
|
int capacity = _directionalShadowSlotPresent.Length == 0
|
||||||
|
? 16
|
||||||
|
: _directionalShadowSlotPresent.Length;
|
||||||
|
while (capacity < required)
|
||||||
|
capacity = checked(capacity * 2);
|
||||||
|
Array.Resize(ref _directionalShadowSlotLandblocks, capacity);
|
||||||
|
Array.Resize(ref _directionalShadowSlotFirstIndices, capacity);
|
||||||
|
Array.Resize(ref _directionalShadowSlotIndexCounts, capacity);
|
||||||
|
Array.Resize(ref _directionalShadowSlotPresent, capacity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,21 @@ internal interface IWorldSceneParticleVisibility
|
||||||
void AbortFrame();
|
void AbortFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Borrowed prior-completed retail landscape visibility. <see cref="CellIds"/>
|
||||||
|
/// is the controller's one retained completed set, not a reconstructed or
|
||||||
|
/// current-camera answer. The borrow is valid through the enhanced-world
|
||||||
|
/// prepass that captured it.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RetailLandscapeVisibilityFrame(
|
||||||
|
IReadOnlySet<uint> CellIds,
|
||||||
|
bool HasCompletedWorldView)
|
||||||
|
{
|
||||||
|
internal static RetailLandscapeVisibilityFrame None { get; } = new(
|
||||||
|
System.Collections.Frozen.FrozenSet<uint>.Empty,
|
||||||
|
HasCompletedWorldView: false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bridges the retained retail PView result into the next physics update's
|
/// Bridges the retained retail PView result into the next physics update's
|
||||||
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
|
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
|
||||||
|
|
@ -115,6 +130,13 @@ public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
|
||||||
rangeMultiplier);
|
rangeMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Borrows the exact prior-completed landscape product for the opt-in
|
||||||
|
/// directional-shadow prepass. No set is copied or reconstructed.
|
||||||
|
/// </summary>
|
||||||
|
internal RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility() =>
|
||||||
|
new(_completedCellIds, _hasCompletedWorldView);
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_buildingCellIds.Clear();
|
_buildingCellIds.Clear();
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ internal readonly record struct DirectionalShadowPreparationStats(
|
||||||
int RejectedTransparentBatches,
|
int RejectedTransparentBatches,
|
||||||
int RejectedFadedParts,
|
int RejectedFadedParts,
|
||||||
int MissingMeshes,
|
int MissingMeshes,
|
||||||
int UnresolvedAlphaCutoutTextures);
|
int UnresolvedAlphaCutoutTextures,
|
||||||
|
int ActiveInstances = 0,
|
||||||
|
int ActiveCommands = 0);
|
||||||
|
|
||||||
internal readonly record struct DirectionalShadowMeshGeometry(
|
internal readonly record struct DirectionalShadowMeshGeometry(
|
||||||
IGpuBuffer VertexBuffer,
|
IGpuBuffer VertexBuffer,
|
||||||
|
|
@ -55,6 +57,14 @@ internal readonly record struct DirectionalShadowTransformSource(
|
||||||
bool IsSetupPart,
|
bool IsSetupPart,
|
||||||
Matrix4x4 SetupPartTransform)
|
Matrix4x4 SetupPartTransform)
|
||||||
{
|
{
|
||||||
|
public static DirectionalShadowTransformSource Static(int casterIndex) =>
|
||||||
|
new(
|
||||||
|
false,
|
||||||
|
casterIndex,
|
||||||
|
MeshIndex: 0,
|
||||||
|
IsSetupPart: false,
|
||||||
|
SetupPartTransform: default);
|
||||||
|
|
||||||
public static DirectionalShadowTransformSource Dynamic(
|
public static DirectionalShadowTransformSource Dynamic(
|
||||||
int casterIndex,
|
int casterIndex,
|
||||||
int meshIndex,
|
int meshIndex,
|
||||||
|
|
@ -89,6 +99,9 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
private DrawElementsIndirectCommand[] _commands = [];
|
private DrawElementsIndirectCommand[] _commands = [];
|
||||||
private DirectionalShadowPreparedBatch[] _batches = [];
|
private DirectionalShadowPreparedBatch[] _batches = [];
|
||||||
private DirectionalShadowPreparedRun[] _runs = [];
|
private DirectionalShadowPreparedRun[] _runs = [];
|
||||||
|
private DrawElementsIndirectCommand[] _activeCommands = [];
|
||||||
|
private DirectionalShadowPreparedBatch[] _activeBatches = [];
|
||||||
|
private DirectionalShadowPreparedRun[] _activeRuns = [];
|
||||||
private int[] _drawNextInGroup = [];
|
private int[] _drawNextInGroup = [];
|
||||||
private int[] _groupHead = [];
|
private int[] _groupHead = [];
|
||||||
private int[] _groupTail = [];
|
private int[] _groupTail = [];
|
||||||
|
|
@ -101,6 +114,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
private int _sourceCount;
|
private int _sourceCount;
|
||||||
private int _commandCount;
|
private int _commandCount;
|
||||||
private int _runCount;
|
private int _runCount;
|
||||||
|
private int _activeCommandCount;
|
||||||
|
private int _activeRunCount;
|
||||||
private int _dynamicTransformSlotCount;
|
private int _dynamicTransformSlotCount;
|
||||||
private int _allDynamicTransformSlotCount;
|
private int _allDynamicTransformSlotCount;
|
||||||
private int _mappedCasterCount;
|
private int _mappedCasterCount;
|
||||||
|
|
@ -117,6 +132,10 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
|
|
||||||
public ulong BuildSequence { get; private set; }
|
public ulong BuildSequence { get; private set; }
|
||||||
|
|
||||||
|
public ulong SourceCasterSelectionSequence { get; private set; }
|
||||||
|
|
||||||
|
public ulong ActiveSelectionSequence { get; private set; }
|
||||||
|
|
||||||
public int LastDynamicTransformRefreshCount { get; private set; }
|
public int LastDynamicTransformRefreshCount { get; private set; }
|
||||||
|
|
||||||
public bool LastDynamicTransformRefreshWasDense { get; private set; }
|
public bool LastDynamicTransformRefreshWasDense { get; private set; }
|
||||||
|
|
@ -174,6 +193,27 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
public ReadOnlySpan<DirectionalShadowPreparedRun> AlphaCutoutRuns =>
|
public ReadOnlySpan<DirectionalShadowPreparedRun> AlphaCutoutRuns =>
|
||||||
_runs.AsSpan(OpaqueRunCount, _runCount - OpaqueRunCount);
|
_runs.AsSpan(OpaqueRunCount, _runCount - OpaqueRunCount);
|
||||||
|
|
||||||
|
public int ActiveOpaqueCommandCount { get; private set; }
|
||||||
|
|
||||||
|
public int ActiveAlphaCutoutCommandCount =>
|
||||||
|
_activeCommandCount - ActiveOpaqueCommandCount;
|
||||||
|
|
||||||
|
public int ActiveOpaqueRunCount { get; private set; }
|
||||||
|
|
||||||
|
public ReadOnlySpan<DrawElementsIndirectCommand> ActiveCommands =>
|
||||||
|
_activeCommands.AsSpan(0, _activeCommandCount);
|
||||||
|
|
||||||
|
public ReadOnlySpan<DirectionalShadowPreparedBatch> ActiveBatches =>
|
||||||
|
_activeBatches.AsSpan(0, _activeCommandCount);
|
||||||
|
|
||||||
|
public ReadOnlySpan<DirectionalShadowPreparedRun> ActiveOpaqueRuns =>
|
||||||
|
_activeRuns.AsSpan(0, ActiveOpaqueRunCount);
|
||||||
|
|
||||||
|
public ReadOnlySpan<DirectionalShadowPreparedRun> ActiveAlphaCutoutRuns =>
|
||||||
|
_activeRuns.AsSpan(
|
||||||
|
ActiveOpaqueRunCount,
|
||||||
|
_activeRunCount - ActiveOpaqueRunCount);
|
||||||
|
|
||||||
public DirectionalShadowPreparationStats Stats { get; private set; }
|
public DirectionalShadowPreparationStats Stats { get; private set; }
|
||||||
|
|
||||||
public long RetainedScratchBytes => checked(
|
public long RetainedScratchBytes => checked(
|
||||||
|
|
@ -193,6 +233,9 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
+ (long)_commands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
|
+ (long)_commands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
|
||||||
+ (long)_batches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
|
+ (long)_batches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
|
||||||
+ (long)_runs.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>()
|
+ (long)_runs.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>()
|
||||||
|
+ (long)_activeCommands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
|
||||||
|
+ (long)_activeBatches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
|
||||||
|
+ (long)_activeRuns.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>()
|
||||||
+ (long)_drawNextInGroup.Length * sizeof(int)
|
+ (long)_drawNextInGroup.Length * sizeof(int)
|
||||||
+ (long)_groupHead.Length * sizeof(int)
|
+ (long)_groupHead.Length * sizeof(int)
|
||||||
+ (long)_groupTail.Length * sizeof(int)
|
+ (long)_groupTail.Length * sizeof(int)
|
||||||
|
|
@ -248,6 +291,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
_sourceCount = 0;
|
_sourceCount = 0;
|
||||||
_commandCount = 0;
|
_commandCount = 0;
|
||||||
_runCount = 0;
|
_runCount = 0;
|
||||||
|
_activeCommandCount = 0;
|
||||||
|
_activeRunCount = 0;
|
||||||
_dynamicTransformSlotCount = 0;
|
_dynamicTransformSlotCount = 0;
|
||||||
_allDynamicTransformSlotCount = 0;
|
_allDynamicTransformSlotCount = 0;
|
||||||
if (_mappedCasterCount != 0)
|
if (_mappedCasterCount != 0)
|
||||||
|
|
@ -260,6 +305,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
_mappedCasterCount = 0;
|
_mappedCasterCount = 0;
|
||||||
OpaqueCommandCount = 0;
|
OpaqueCommandCount = 0;
|
||||||
OpaqueRunCount = 0;
|
OpaqueRunCount = 0;
|
||||||
|
ActiveOpaqueCommandCount = 0;
|
||||||
|
ActiveOpaqueRunCount = 0;
|
||||||
Stats = default;
|
Stats = default;
|
||||||
LastDynamicTransformRefreshCount = 0;
|
LastDynamicTransformRefreshCount = 0;
|
||||||
LastDynamicTransformRefreshWasDense = false;
|
LastDynamicTransformRefreshWasDense = false;
|
||||||
|
|
@ -443,6 +490,9 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
EnsureCapacity(ref _commands, _sourceCount);
|
EnsureCapacity(ref _commands, _sourceCount);
|
||||||
EnsureCapacity(ref _batches, _sourceCount);
|
EnsureCapacity(ref _batches, _sourceCount);
|
||||||
EnsureCapacity(ref _runs, _sourceCount);
|
EnsureCapacity(ref _runs, _sourceCount);
|
||||||
|
EnsureCapacity(ref _activeCommands, _sourceCount);
|
||||||
|
EnsureCapacity(ref _activeBatches, _sourceCount);
|
||||||
|
EnsureCapacity(ref _activeRuns, _sourceCount);
|
||||||
|
|
||||||
int maxCasterIndex = -1;
|
int maxCasterIndex = -1;
|
||||||
for (int index = 0; index < _sourceCount; index++)
|
for (int index = 0; index < _sourceCount; index++)
|
||||||
|
|
@ -562,6 +612,152 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
PreparedAlphaCutoutCommands = commandIndex - opaqueCommands,
|
PreparedAlphaCutoutCommands = commandIndex - opaqueCommands,
|
||||||
};
|
};
|
||||||
_building = false;
|
_building = false;
|
||||||
|
RebuildActiveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Projects arbitrary selected caster instances into contiguous transform
|
||||||
|
/// runs without changing retained material/mesh topology or its transform
|
||||||
|
/// address space.
|
||||||
|
/// </summary>
|
||||||
|
internal void ApplySelection(DirectionalShadowCasterFrame casters)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(casters);
|
||||||
|
if (casters.SelectionSequence == 0)
|
||||||
|
return;
|
||||||
|
if (SourceGeneration != casters.Generation
|
||||||
|
|| SourceCasterBuildSequence != casters.BuildSequence)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Directional-shadow selection does not match prepared topology.");
|
||||||
|
}
|
||||||
|
if (SourceCasterSelectionSequence == casters.SelectionSequence)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ApplySelection(casters.SelectedCasters, casters.SelectionSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ApplySelection(
|
||||||
|
ReadOnlySpan<bool> selected,
|
||||||
|
ulong casterSelectionSequence)
|
||||||
|
{
|
||||||
|
if (casterSelectionSequence == 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(casterSelectionSequence));
|
||||||
|
if (SourceCasterSelectionSequence == casterSelectionSequence)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_activeCommandCount = 0;
|
||||||
|
int activeInstances = 0;
|
||||||
|
for (int commandIndex = 0; commandIndex < _commandCount; commandIndex++)
|
||||||
|
{
|
||||||
|
DrawElementsIndirectCommand command = _commands[commandIndex];
|
||||||
|
int first = checked((int)command.BaseInstance);
|
||||||
|
int end = checked(first + (int)command.InstanceCount);
|
||||||
|
int runStart = -1;
|
||||||
|
for (int transformIndex = first; transformIndex < end; transformIndex++)
|
||||||
|
{
|
||||||
|
int casterIndex = _transformSources[transformIndex].CasterIndex;
|
||||||
|
if ((uint)casterIndex >= (uint)selected.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Prepared directional-shadow instance has a stale caster slot.");
|
||||||
|
}
|
||||||
|
bool active = selected[casterIndex];
|
||||||
|
if (active && runStart < 0)
|
||||||
|
runStart = transformIndex;
|
||||||
|
if (!active && runStart >= 0)
|
||||||
|
{
|
||||||
|
EmitActive(commandIndex, in command, runStart, transformIndex - runStart);
|
||||||
|
activeInstances += transformIndex - runStart;
|
||||||
|
runStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (runStart >= 0)
|
||||||
|
{
|
||||||
|
EmitActive(commandIndex, in command, runStart, end - runStart);
|
||||||
|
activeInstances += end - runStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildActiveRuns();
|
||||||
|
SourceCasterSelectionSequence = casterSelectionSequence;
|
||||||
|
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
|
||||||
|
Stats = Stats with
|
||||||
|
{
|
||||||
|
ActiveInstances = activeInstances,
|
||||||
|
ActiveCommands = _activeCommandCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildActiveAll()
|
||||||
|
{
|
||||||
|
EnsureCapacity(ref _activeCommands, _commandCount);
|
||||||
|
EnsureCapacity(ref _activeBatches, _commandCount);
|
||||||
|
EnsureCapacity(ref _activeRuns, _runCount);
|
||||||
|
_commands.AsSpan(0, _commandCount).CopyTo(_activeCommands);
|
||||||
|
_batches.AsSpan(0, _commandCount).CopyTo(_activeBatches);
|
||||||
|
_activeCommandCount = _commandCount;
|
||||||
|
BuildActiveRuns();
|
||||||
|
SourceCasterSelectionSequence = 0;
|
||||||
|
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
|
||||||
|
Stats = Stats with
|
||||||
|
{
|
||||||
|
ActiveInstances = _sourceCount,
|
||||||
|
ActiveCommands = _activeCommandCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitActive(
|
||||||
|
int sourceCommandIndex,
|
||||||
|
in DrawElementsIndirectCommand source,
|
||||||
|
int baseInstance,
|
||||||
|
int instanceCount)
|
||||||
|
{
|
||||||
|
int destination = _activeCommandCount++;
|
||||||
|
_activeCommands[destination] = source with
|
||||||
|
{
|
||||||
|
BaseInstance = checked((uint)baseInstance),
|
||||||
|
InstanceCount = checked((uint)instanceCount),
|
||||||
|
};
|
||||||
|
_activeBatches[destination] = _batches[sourceCommandIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildActiveRuns()
|
||||||
|
{
|
||||||
|
_activeRunCount = 0;
|
||||||
|
ActiveOpaqueCommandCount = 0;
|
||||||
|
while (ActiveOpaqueCommandCount < _activeCommandCount
|
||||||
|
&& _activeBatches[ActiveOpaqueCommandCount].Material
|
||||||
|
is DirectionalShadowCasterMaterial.Opaque)
|
||||||
|
{
|
||||||
|
ActiveOpaqueCommandCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int runStart = 0;
|
||||||
|
while (runStart < _activeCommandCount)
|
||||||
|
{
|
||||||
|
DirectionalShadowPreparedBatch first = _activeBatches[runStart];
|
||||||
|
int runEnd = runStart + 1;
|
||||||
|
while (runEnd < _activeCommandCount
|
||||||
|
&& _activeBatches[runEnd].CullMode == first.CullMode
|
||||||
|
&& _activeBatches[runEnd].Material == first.Material)
|
||||||
|
{
|
||||||
|
runEnd++;
|
||||||
|
}
|
||||||
|
_activeRuns[_activeRunCount++] = new DirectionalShadowPreparedRun(
|
||||||
|
runStart,
|
||||||
|
runEnd - runStart,
|
||||||
|
first.CullMode,
|
||||||
|
first.Material);
|
||||||
|
runStart = runEnd;
|
||||||
|
}
|
||||||
|
ActiveOpaqueRunCount = 0;
|
||||||
|
while (ActiveOpaqueRunCount < _activeRunCount
|
||||||
|
&& _activeRuns[ActiveOpaqueRunCount].Material
|
||||||
|
is DirectionalShadowCasterMaterial.Opaque)
|
||||||
|
{
|
||||||
|
ActiveOpaqueRunCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshDynamicTransforms(
|
public void RefreshDynamicTransforms(
|
||||||
|
|
@ -878,6 +1074,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
_sourceCount = 0;
|
_sourceCount = 0;
|
||||||
_commandCount = 0;
|
_commandCount = 0;
|
||||||
_runCount = 0;
|
_runCount = 0;
|
||||||
|
_activeCommandCount = 0;
|
||||||
|
_activeRunCount = 0;
|
||||||
_dynamicTransformSlotCount = 0;
|
_dynamicTransformSlotCount = 0;
|
||||||
_allDynamicTransformSlotCount = 0;
|
_allDynamicTransformSlotCount = 0;
|
||||||
if (_mappedCasterCount != 0)
|
if (_mappedCasterCount != 0)
|
||||||
|
|
@ -890,11 +1088,14 @@ internal sealed class DirectionalShadowPreparedDraws
|
||||||
_mappedCasterCount = 0;
|
_mappedCasterCount = 0;
|
||||||
OpaqueCommandCount = 0;
|
OpaqueCommandCount = 0;
|
||||||
OpaqueRunCount = 0;
|
OpaqueRunCount = 0;
|
||||||
|
ActiveOpaqueCommandCount = 0;
|
||||||
|
ActiveOpaqueRunCount = 0;
|
||||||
Stats = default;
|
Stats = default;
|
||||||
SourceGeneration = default;
|
SourceGeneration = default;
|
||||||
SourceCasterBuildSequence = 0;
|
SourceCasterBuildSequence = 0;
|
||||||
SourceRenderDataAvailabilityVersion = 0;
|
SourceRenderDataAvailabilityVersion = 0;
|
||||||
SourceTranslucencyFadeRevision = 0;
|
SourceTranslucencyFadeRevision = 0;
|
||||||
|
SourceCasterSelectionSequence = 0;
|
||||||
LastDynamicTransformRefreshCount = 0;
|
LastDynamicTransformRefreshCount = 0;
|
||||||
LastDynamicTransformRefreshWasDense = false;
|
LastDynamicTransformRefreshWasDense = false;
|
||||||
_retryClassificationNextFrame = false;
|
_retryClassificationNextFrame = false;
|
||||||
|
|
@ -1034,6 +1235,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
translucencyFadeRevision))
|
translucencyFadeRevision))
|
||||||
{
|
{
|
||||||
_directionalShadowDraws.RefreshDynamicTransforms(casters);
|
_directionalShadowDraws.RefreshDynamicTransforms(casters);
|
||||||
|
_directionalShadowDraws.ApplySelection(casters);
|
||||||
return _directionalShadowDraws;
|
return _directionalShadowDraws;
|
||||||
}
|
}
|
||||||
// #429 owner-approved pipelining: a deferred frame keeps the retained
|
// #429 owner-approved pipelining: a deferred frame keeps the retained
|
||||||
|
|
@ -1047,6 +1249,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
&& _directionalShadowDraws.SourceGeneration == casters.Generation)
|
&& _directionalShadowDraws.SourceGeneration == casters.Generation)
|
||||||
{
|
{
|
||||||
_directionalShadowDraws.RefreshDynamicTransforms(casters);
|
_directionalShadowDraws.RefreshDynamicTransforms(casters);
|
||||||
|
_directionalShadowDraws.ApplySelection(casters);
|
||||||
return _directionalShadowDraws;
|
return _directionalShadowDraws;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1064,6 +1267,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
renderDataAvailabilityVersion,
|
renderDataAvailabilityVersion,
|
||||||
translucencyFadeRevision))
|
translucencyFadeRevision))
|
||||||
{
|
{
|
||||||
|
_directionalShadowDraws.ApplySelection(casters);
|
||||||
return _directionalShadowDraws;
|
return _directionalShadowDraws;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1168,7 +1372,8 @@ public sealed partial class WbDrawDispatcher
|
||||||
meshIndex,
|
meshIndex,
|
||||||
true,
|
true,
|
||||||
in partTransform)
|
in partTransform)
|
||||||
: default;
|
: DirectionalShadowTransformSource.Static(
|
||||||
|
casterIndex);
|
||||||
AddDirectionalShadowBatches(
|
AddDirectionalShadowBatches(
|
||||||
partData,
|
partData,
|
||||||
in candidate,
|
in candidate,
|
||||||
|
|
@ -1203,7 +1408,8 @@ public sealed partial class WbDrawDispatcher
|
||||||
meshIndex,
|
meshIndex,
|
||||||
false,
|
false,
|
||||||
in noSetupPart)
|
in noSetupPart)
|
||||||
: default;
|
: DirectionalShadowTransformSource.Static(
|
||||||
|
casterIndex);
|
||||||
AddDirectionalShadowBatches(
|
AddDirectionalShadowBatches(
|
||||||
renderData,
|
renderData,
|
||||||
in candidate,
|
in candidate,
|
||||||
|
|
@ -1236,6 +1442,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
in stats,
|
in stats,
|
||||||
renderDataAvailabilityVersion,
|
renderDataAvailabilityVersion,
|
||||||
translucencyFadeRevision);
|
translucencyFadeRevision);
|
||||||
|
_directionalShadowDraws.ApplySelection(casters);
|
||||||
return _directionalShadowDraws;
|
return _directionalShadowDraws;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,50 @@ internal readonly record struct WorldRenderFrame(
|
||||||
/// directional-shadow prepass.
|
/// directional-shadow prepass.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthoredCelestialShadowSource CelestialShadowSource { get; init; }
|
public AuthoredCelestialShadowSource CelestialShadowSource { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pack-on-only borrow of the prior successfully completed retail
|
||||||
|
/// landscape visibility transaction. It deliberately trails the camera
|
||||||
|
/// resolved by this frame.
|
||||||
|
/// </summary>
|
||||||
|
public RetailLandscapeVisibilityFrame PriorLandscapeVisibility { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Pack-on-only read seam over S2's exact retained CELLARRAY
|
||||||
|
/// owner. The frame never copies or retains registry rows.</summary>
|
||||||
|
public IDirectionalShadowCellMembership? DirectionalShadowCellMembership
|
||||||
|
{ get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IDirectionalShadowCellMembership
|
||||||
|
{
|
||||||
|
bool TryGetRetailCellArray(uint entityId, out IReadOnlyList<uint> cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EmptyDirectionalShadowCellMembership
|
||||||
|
: IDirectionalShadowCellMembership
|
||||||
|
{
|
||||||
|
internal static EmptyDirectionalShadowCellMembership Instance { get; } = new();
|
||||||
|
|
||||||
|
public bool TryGetRetailCellArray(
|
||||||
|
uint entityId,
|
||||||
|
out IReadOnlyList<uint> cells)
|
||||||
|
{
|
||||||
|
_ = entityId;
|
||||||
|
cells = Array.Empty<uint>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RuntimeDirectionalShadowCellMembership(
|
||||||
|
ShadowObjectRegistry source) : IDirectionalShadowCellMembership
|
||||||
|
{
|
||||||
|
private readonly ShadowObjectRegistry _source = source
|
||||||
|
?? throw new ArgumentNullException(nameof(source));
|
||||||
|
|
||||||
|
public bool TryGetRetailCellArray(
|
||||||
|
uint entityId,
|
||||||
|
out IReadOnlyList<uint> cells) =>
|
||||||
|
_source.TryGetRetailCellArray(entityId, out cells);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface IWorldRenderFrameBuilder
|
internal interface IWorldRenderFrameBuilder
|
||||||
|
|
@ -114,6 +158,8 @@ internal interface IWorldFrameRootSource
|
||||||
|
|
||||||
internal interface IWorldFrameVisibilityPreparation
|
internal interface IWorldFrameVisibilityPreparation
|
||||||
{
|
{
|
||||||
|
RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility();
|
||||||
|
|
||||||
void Begin(in WorldCameraFrame camera, bool waitingForLogin);
|
void Begin(in WorldCameraFrame camera, bool waitingForLogin);
|
||||||
|
|
||||||
void PublishViewProjection(in WorldCameraFrame camera);
|
void PublishViewProjection(in WorldCameraFrame camera);
|
||||||
|
|
@ -160,6 +206,7 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
||||||
private readonly IWorldFrameEnvironmentPreparation _environment;
|
private readonly IWorldFrameEnvironmentPreparation _environment;
|
||||||
private readonly IWorldFrameAnimatedEntitySource _animated;
|
private readonly IWorldFrameAnimatedEntitySource _animated;
|
||||||
private readonly IWorldFrameBuildingSource _buildings;
|
private readonly IWorldFrameBuildingSource _buildings;
|
||||||
|
private readonly IDirectionalShadowCellMembership _directionalShadowCells;
|
||||||
|
|
||||||
public WorldRenderFrameBuilder(
|
public WorldRenderFrameBuilder(
|
||||||
IWorldFrameCameraSource camera,
|
IWorldFrameCameraSource camera,
|
||||||
|
|
@ -168,7 +215,8 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
||||||
IWorldFrameRootSource roots,
|
IWorldFrameRootSource roots,
|
||||||
IWorldFrameEnvironmentPreparation environment,
|
IWorldFrameEnvironmentPreparation environment,
|
||||||
IWorldFrameAnimatedEntitySource animated,
|
IWorldFrameAnimatedEntitySource animated,
|
||||||
IWorldFrameBuildingSource buildings)
|
IWorldFrameBuildingSource buildings,
|
||||||
|
IDirectionalShadowCellMembership directionalShadowCells)
|
||||||
{
|
{
|
||||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||||
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
|
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
|
||||||
|
|
@ -177,6 +225,8 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
||||||
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
||||||
_animated = animated ?? throw new ArgumentNullException(nameof(animated));
|
_animated = animated ?? throw new ArgumentNullException(nameof(animated));
|
||||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||||
|
_directionalShadowCells = directionalShadowCells
|
||||||
|
?? throw new ArgumentNullException(nameof(directionalShadowCells));
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldRenderFrame Build(
|
public WorldRenderFrame Build(
|
||||||
|
|
@ -184,6 +234,10 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
||||||
bool waitingForLogin,
|
bool waitingForLogin,
|
||||||
DayGroupData? activeDayGroup)
|
DayGroupData? activeDayGroup)
|
||||||
{
|
{
|
||||||
|
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
|
||||||
|
_visibility.CaptureCompletedLandscapeVisibility();
|
||||||
|
if (waitingForLogin)
|
||||||
|
priorLandscapeVisibility = RetailLandscapeVisibilityFrame.None;
|
||||||
WorldCameraFrame camera = _camera.Resolve();
|
WorldCameraFrame camera = _camera.Resolve();
|
||||||
_visibility.Begin(in camera, waitingForLogin);
|
_visibility.Begin(in camera, waitingForLogin);
|
||||||
_settings.Apply(in camera);
|
_settings.Apply(in camera);
|
||||||
|
|
@ -196,7 +250,11 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
||||||
roots.ViewerRoot,
|
roots.ViewerRoot,
|
||||||
roots.ViewerCellId,
|
roots.ViewerCellId,
|
||||||
in frustum);
|
in frustum);
|
||||||
return new WorldRenderFrame(camera, roots, buildings, animated);
|
return new WorldRenderFrame(camera, roots, buildings, animated)
|
||||||
|
{
|
||||||
|
PriorLandscapeVisibility = priorLandscapeVisibility,
|
||||||
|
DirectionalShadowCellMembership = _directionalShadowCells,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -350,6 +408,9 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
|
||||||
_environmentFrustum = environmentFrustum;
|
_environmentFrustum = environmentFrustum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility() =>
|
||||||
|
_particles.CaptureCompletedLandscapeVisibility();
|
||||||
|
|
||||||
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
||||||
{
|
{
|
||||||
_selection?.SetViewFrustum(camera.Frustum);
|
_selection?.SetViewFrustum(camera.Frustum);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
@ -106,6 +108,107 @@ public sealed class DirectionalShadowCasterFrameTests
|
||||||
Assert.Equal(2, frame.Stats.IndexCopies);
|
Assert.Equal(2, frame.Stats.IndexCopies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PriorLandscapeSelection_UsesExactCellArrayAndBuildingEffectCell()
|
||||||
|
{
|
||||||
|
const uint visible = 0x12340002u;
|
||||||
|
const uint outdoorAboveTerrainRange = 0x12340041u;
|
||||||
|
RenderProjectionRecord[] statics =
|
||||||
|
[
|
||||||
|
Record(201, RenderProjectionClass.OutdoorStatic),
|
||||||
|
Record(204, RenderProjectionClass.OutdoorStatic, parentCell: visible),
|
||||||
|
Record(205, RenderProjectionClass.OutdoorStatic, building: true,
|
||||||
|
effectCell: visible, buildingAnchor: 0x12340100u),
|
||||||
|
Record(206, RenderProjectionClass.OutdoorStatic, building: true,
|
||||||
|
effectCell: 0u, buildingAnchor: visible),
|
||||||
|
];
|
||||||
|
RenderProjectionRecord[] dynamics =
|
||||||
|
[
|
||||||
|
Record(202, RenderProjectionClass.LiveDynamicRoot),
|
||||||
|
Record(203, RenderProjectionClass.EquippedChild),
|
||||||
|
];
|
||||||
|
var source = new QuerySource(statics, dynamics);
|
||||||
|
var frame = new DirectionalShadowCasterFrame();
|
||||||
|
var membership = new RecordingMembership(
|
||||||
|
new Dictionary<uint, IReadOnlyList<uint>>
|
||||||
|
{
|
||||||
|
[201] = [0x12340001u, visible],
|
||||||
|
[202] = [visible],
|
||||||
|
[203] = [outdoorAboveTerrainRange],
|
||||||
|
});
|
||||||
|
var visibleCells = new HashSet<uint>
|
||||||
|
{
|
||||||
|
visible,
|
||||||
|
outdoorAboveTerrainRange,
|
||||||
|
};
|
||||||
|
var visibility = new RetailLandscapeVisibilityFrame(
|
||||||
|
visibleCells,
|
||||||
|
HasCompletedWorldView: true);
|
||||||
|
|
||||||
|
frame.Build(new RenderSceneQuery(source, Generation));
|
||||||
|
ulong topologySequence = frame.BuildSequence;
|
||||||
|
frame.Select(in visibility, membership);
|
||||||
|
|
||||||
|
ulong[] selected = frame.Casters.ToArray()
|
||||||
|
.Zip(frame.SelectedCasters.ToArray())
|
||||||
|
.Where(static pair => pair.Second)
|
||||||
|
.Select(static pair => pair.First.Projection.Id.RawValue)
|
||||||
|
.ToArray();
|
||||||
|
Assert.Equal([201ul, 202ul, 203ul, 205ul], selected);
|
||||||
|
Assert.Equal(4, frame.Stats.ActiveSelected);
|
||||||
|
Assert.Equal(topologySequence, frame.BuildSequence);
|
||||||
|
Assert.Equal(0x12340100u,
|
||||||
|
frame.Casters.ToArray().Single(c => c.Projection.Id.RawValue == 205)
|
||||||
|
.Projection.Source.BuildingShellAnchorCellId);
|
||||||
|
Assert.DoesNotContain(204ul, selected); // ParentCell is not CELLARRAY.
|
||||||
|
Assert.DoesNotContain(206ul, selected); // Anchor is not placement.
|
||||||
|
|
||||||
|
var noCompletedView = new RetailLandscapeVisibilityFrame(
|
||||||
|
visibleCells,
|
||||||
|
HasCompletedWorldView: false);
|
||||||
|
frame.Build(new RenderSceneQuery(source, Generation));
|
||||||
|
frame.Select(in noCompletedView, membership);
|
||||||
|
Assert.Equal(topologySequence, frame.BuildSequence);
|
||||||
|
Assert.Equal(0, frame.Stats.Classifications);
|
||||||
|
Assert.Equal(0, frame.Stats.ActiveSelected);
|
||||||
|
Assert.DoesNotContain(true, frame.SelectedCasters.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarmPriorLandscapeSelection_AllocatesZeroWithStreamingBoundedScratch()
|
||||||
|
{
|
||||||
|
const uint visible = 0x12340002u;
|
||||||
|
var statics = new RenderProjectionRecord[256];
|
||||||
|
var cells = new Dictionary<uint, IReadOnlyList<uint>>(statics.Length);
|
||||||
|
for (int index = 0; index < statics.Length; index++)
|
||||||
|
{
|
||||||
|
uint entityId = checked((uint)(30_000 + index));
|
||||||
|
statics[index] = Record(entityId, RenderProjectionClass.OutdoorStatic);
|
||||||
|
cells.Add(entityId, index % 2 == 0 ? [visible] : [0x12340003u]);
|
||||||
|
}
|
||||||
|
var source = new QuerySource(statics, []);
|
||||||
|
var frame = new DirectionalShadowCasterFrame();
|
||||||
|
RenderSceneQuery query = new(source, Generation);
|
||||||
|
frame.Build(in query);
|
||||||
|
frame.Build(in query);
|
||||||
|
var membership = new RecordingMembership(cells);
|
||||||
|
var visibility = new RetailLandscapeVisibilityFrame(
|
||||||
|
new HashSet<uint> { visible },
|
||||||
|
HasCompletedWorldView: true);
|
||||||
|
frame.Select(in visibility, membership);
|
||||||
|
long retainedBytes = frame.RetainedScratchBytes;
|
||||||
|
|
||||||
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||||
|
"DirectionalShadowCasterFrame.Select prior completed landscape",
|
||||||
|
() => frame.Select(in visibility, membership),
|
||||||
|
batchSize: 256);
|
||||||
|
|
||||||
|
Assert.Equal(128, frame.Stats.ActiveSelected);
|
||||||
|
Assert.Equal(retainedBytes, frame.RetainedScratchBytes);
|
||||||
|
Assert.Equal(1ul, frame.BuildSequence);
|
||||||
|
Assert.Equal(0, frame.Stats.Classifications);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnchangedSecondFrame_ReusesSortedTopologyWithoutIndexCopiesOrClassification()
|
public void UnchangedSecondFrame_ReusesSortedTopologyWithoutIndexCopiesOrClassification()
|
||||||
{
|
{
|
||||||
|
|
@ -575,6 +678,8 @@ public sealed class DirectionalShadowCasterFrameTests
|
||||||
bool building = false,
|
bool building = false,
|
||||||
RenderProjectionFlags extraFlags = RenderProjectionFlags.None,
|
RenderProjectionFlags extraFlags = RenderProjectionFlags.None,
|
||||||
uint parentCell = 0,
|
uint parentCell = 0,
|
||||||
|
uint effectCell = 0,
|
||||||
|
uint buildingAnchor = 0,
|
||||||
ulong? sortKey = null,
|
ulong? sortKey = null,
|
||||||
RenderCasterIdentityKind casterIdentity =
|
RenderCasterIdentityKind casterIdentity =
|
||||||
RenderCasterIdentityKind.Unclassified)
|
RenderCasterIdentityKind.Unclassified)
|
||||||
|
|
@ -606,8 +711,8 @@ public sealed class DirectionalShadowCasterFrameTests
|
||||||
: 0,
|
: 0,
|
||||||
SourceId: (uint)id,
|
SourceId: (uint)id,
|
||||||
ParentCellId: parentCell,
|
ParentCellId: parentCell,
|
||||||
EffectCellId: 0,
|
EffectCellId: effectCell,
|
||||||
BuildingShellAnchorCellId: 0,
|
BuildingShellAnchorCellId: buildingAnchor,
|
||||||
TransformFingerprint: default,
|
TransformFingerprint: default,
|
||||||
GeometryFingerprint: default,
|
GeometryFingerprint: default,
|
||||||
AppearanceFingerprint: default),
|
AppearanceFingerprint: default),
|
||||||
|
|
@ -619,6 +724,24 @@ public sealed class DirectionalShadowCasterFrameTests
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingMembership(
|
||||||
|
IReadOnlyDictionary<uint, IReadOnlyList<uint>> cellsByEntity)
|
||||||
|
: IDirectionalShadowCellMembership
|
||||||
|
{
|
||||||
|
public bool TryGetRetailCellArray(
|
||||||
|
uint entityId,
|
||||||
|
out IReadOnlyList<uint> cells)
|
||||||
|
{
|
||||||
|
if (cellsByEntity.TryGetValue(entityId, out IReadOnlyList<uint>? found))
|
||||||
|
{
|
||||||
|
cells = found;
|
||||||
|
return found.Count != 0;
|
||||||
|
}
|
||||||
|
cells = Array.Empty<uint>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class QuerySource : IRenderSceneQuerySource
|
private sealed class QuerySource : IRenderSceneQuerySource
|
||||||
{
|
{
|
||||||
private RenderProjectionRecord[] _statics;
|
private RenderProjectionRecord[] _statics;
|
||||||
|
|
|
||||||
|
|
@ -939,6 +939,84 @@ public sealed class DirectionalShadowGpuTests
|
||||||
allocation => allocation.Usage == GpuRingUsage.Indirect);
|
allocation => allocation.Usage == GpuRingUsage.Indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveAlternatingRuns_UploadOnlySelectedTransformAddresses()
|
||||||
|
{
|
||||||
|
using var device = new RecordingGpuDevice();
|
||||||
|
using var renderer = new DirectionalSunShadowRenderer(
|
||||||
|
device,
|
||||||
|
DirectionalShadowPreset.Medium);
|
||||||
|
var world = new DirectionalShadowPreparedDraws();
|
||||||
|
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(21);
|
||||||
|
Assert.True(world.TryBegin(generation, 44, estimatedInstances: 6));
|
||||||
|
for (int casterIndex = 0; casterIndex < 6; casterIndex++)
|
||||||
|
{
|
||||||
|
Matrix4x4 transform = Matrix4x4.CreateTranslation(casterIndex, 0f, 0f);
|
||||||
|
DirectionalShadowTransformSource source =
|
||||||
|
DirectionalShadowTransformSource.Static(casterIndex);
|
||||||
|
world.Add(
|
||||||
|
100,
|
||||||
|
7,
|
||||||
|
12,
|
||||||
|
GpuTextureSlot.Unassigned,
|
||||||
|
0,
|
||||||
|
CullMode.CounterClockwise,
|
||||||
|
DirectionalShadowCasterMaterial.Opaque,
|
||||||
|
in transform,
|
||||||
|
in source);
|
||||||
|
}
|
||||||
|
DirectionalShadowPreparationStats stats = default;
|
||||||
|
world.Complete(generation, 44, in stats);
|
||||||
|
world.ApplySelection(
|
||||||
|
[true, false, true, false, true, false],
|
||||||
|
casterSelectionSequence: 1);
|
||||||
|
var terrain = new DirectionalShadowTerrainPreparedDraws();
|
||||||
|
Assert.True(terrain.TryBegin(1, 0));
|
||||||
|
terrain.Complete(1);
|
||||||
|
using IGpuBuffer vertices = Buffer(device, "world-v", GpuBufferUsage.Vertex);
|
||||||
|
using IGpuBuffer indices = Buffer(device, "world-i", GpuBufferUsage.Index);
|
||||||
|
var geometry = new DirectionalShadowMeshGeometry(vertices, indices);
|
||||||
|
|
||||||
|
device.Clear();
|
||||||
|
using IGpuFrame frame = device.BeginFrame();
|
||||||
|
WorldTransformFrameSlice transforms = PublishSharedTransforms(frame, world.Transforms);
|
||||||
|
DirectionalSunShadowDiagnostics diagnostics = renderer.RenderPrepared(
|
||||||
|
frame,
|
||||||
|
EnabledEnvironment(),
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
Matrix4x4.CreatePerspectiveFieldOfView(
|
||||||
|
MathF.PI / 3f,
|
||||||
|
16f / 9f,
|
||||||
|
0.1f,
|
||||||
|
500f),
|
||||||
|
cameraNearMeters: 0.1f,
|
||||||
|
casterDepthPaddingMeters: 48f,
|
||||||
|
world,
|
||||||
|
terrain,
|
||||||
|
geometry,
|
||||||
|
terrainGeometry: null,
|
||||||
|
transforms);
|
||||||
|
|
||||||
|
RecordingGpuBuffer commands = Assert.Single(
|
||||||
|
device.CreatedBuffers,
|
||||||
|
buffer => buffer.Name == "directional-shadow-world-commands-2");
|
||||||
|
Span<byte> bytes = stackalloc byte[3 * 20];
|
||||||
|
commands.Read(0, bytes);
|
||||||
|
ReadOnlySpan<DrawElementsIndirectCommand> uploaded =
|
||||||
|
MemoryMarshal.Cast<byte, DrawElementsIndirectCommand>(bytes);
|
||||||
|
Assert.Equal([0u, 2u, 4u],
|
||||||
|
uploaded.ToArray().Select(static command => command.BaseInstance));
|
||||||
|
Assert.All(uploaded.ToArray(),
|
||||||
|
static command => Assert.Equal(1u, command.InstanceCount));
|
||||||
|
Assert.All(
|
||||||
|
device.OfKind<GpuRecordedMultiDrawIndirect>(),
|
||||||
|
static draw => Assert.Equal(3u, draw.DrawCount));
|
||||||
|
Assert.Equal(6, diagnostics.ResidentWorldInstances);
|
||||||
|
Assert.Equal(3, diagnostics.ActiveWorldInstances);
|
||||||
|
Assert.Equal(1, diagnostics.ResidentWorldCommands);
|
||||||
|
Assert.Equal(3, diagnostics.ActiveWorldCommands);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TopologyRebuild_SwapsRetainedBuffersAndDisposalReleasesTheCurrentSet()
|
public void TopologyRebuild_SwapsRetainedBuffersAndDisposalReleasesTheCurrentSet()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1463,6 +1463,63 @@ public sealed class AtmosphericPostProcessGraphTests
|
||||||
Assert.Equal(first.WindAmplitude, second.WindAmplitude);
|
Assert.Equal(first.WindAmplitude, second.WindAmplitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuiltInAndDeclaredShadowGraphsUseTheSameTypedPriorVisibilitySelector()
|
||||||
|
{
|
||||||
|
string renderingRoot = Path.Combine(
|
||||||
|
RepositoryRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"Packs");
|
||||||
|
string builtIn = File.ReadAllText(
|
||||||
|
Path.Combine(renderingRoot, "AtmosphericPostProcessGraph.cs"));
|
||||||
|
string declared = File.ReadAllText(
|
||||||
|
Path.Combine(renderingRoot, "DeclaredFullscreenRenderPackGraph.cs"));
|
||||||
|
|
||||||
|
AssertGraphUsesTypedSelection(builtIn, "RenderDirectionalShadows(");
|
||||||
|
AssertGraphUsesTypedSelection(declared, "RenderDeclaredDirectionalShadows(");
|
||||||
|
|
||||||
|
string register = File.ReadAllText(Path.Combine(
|
||||||
|
RepositoryRoot(),
|
||||||
|
"docs",
|
||||||
|
"architecture",
|
||||||
|
"retail-divergence-register.md"));
|
||||||
|
string ia24 = Assert.Single(register.Split('\n'), static line =>
|
||||||
|
line.StartsWith("| IA-24 |", StringComparison.Ordinal));
|
||||||
|
Assert.Contains("prior successfully completed", ia24,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("S2's retained retail CELLARRAY", ia24,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("building shells by their outdoor placement `EffectCellId`", ia24,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("pack-off does not build, select, upload, or draw shadow work", ia24,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
static void AssertGraphUsesTypedSelection(string source, string methodName)
|
||||||
|
{
|
||||||
|
int start = source.IndexOf(methodName, StringComparison.Ordinal);
|
||||||
|
Assert.True(start >= 0, $"missing {methodName}");
|
||||||
|
int end = source.IndexOf("public IGpuRenderTarget PrepareWorldTarget", start,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.True(end > start, $"could not bound {methodName}");
|
||||||
|
string method = source[start..end];
|
||||||
|
Assert.Contains(
|
||||||
|
"RetailLandscapeVisibilityFrame priorLandscapeVisibility =",
|
||||||
|
method,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("world.PriorLandscapeVisibility", method,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_shadowCasters.Select(", method, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("world.DirectionalShadowCellMembership", method,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("EmptyDirectionalShadowCellMembership.Instance", method,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("PriorLandscapeVisibility: world.PriorLandscapeVisibility", method,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static AtmosphericFrameUniforms RenderAndReadFrameBlock(
|
private static AtmosphericFrameUniforms RenderAndReadFrameBlock(
|
||||||
RecordingGpuDevice device,
|
RecordingGpuDevice device,
|
||||||
AtmosphericPostProcessGraph graph,
|
AtmosphericPostProcessGraph graph,
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,56 @@ public sealed class ParticleVisibilityControllerTests
|
||||||
Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible);
|
Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BorrowedLandscapeFrame_TracksOnlyCompletedTransactionsByReference()
|
||||||
|
{
|
||||||
|
var controller = new ParticleVisibilityController();
|
||||||
|
RetailLandscapeVisibilityFrame first =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.False(first.HasCompletedWorldView);
|
||||||
|
Assert.Empty(first.CellIds);
|
||||||
|
|
||||||
|
// A completed null-root safety frame is authoritative and empty.
|
||||||
|
controller.BeginFrame(Vector3.Zero);
|
||||||
|
controller.UseWorldView();
|
||||||
|
controller.CompleteFrame();
|
||||||
|
RetailLandscapeVisibilityFrame completedEmpty =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.True(completedEmpty.HasCompletedWorldView);
|
||||||
|
Assert.Empty(completedEmpty.CellIds);
|
||||||
|
Assert.Same(first.CellIds, completedEmpty.CellIds);
|
||||||
|
|
||||||
|
controller.BeginFrame(Vector3.One);
|
||||||
|
controller.UseWorldView();
|
||||||
|
controller.MarkVisibleLandscapeCells([0x12340001u]);
|
||||||
|
RetailLandscapeVisibilityFrame whileBuilding =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.Same(completedEmpty.CellIds, whileBuilding.CellIds);
|
||||||
|
Assert.Empty(whileBuilding.CellIds);
|
||||||
|
controller.AbortFrame();
|
||||||
|
RetailLandscapeVisibilityFrame aborted =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.Same(completedEmpty.CellIds, aborted.CellIds);
|
||||||
|
Assert.True(aborted.HasCompletedWorldView);
|
||||||
|
Assert.Empty(aborted.CellIds);
|
||||||
|
|
||||||
|
controller.BeginFrame(Vector3.One);
|
||||||
|
controller.UseWorldView();
|
||||||
|
controller.MarkVisibleLandscapeCells([0x12340001u]);
|
||||||
|
controller.CompleteFrame();
|
||||||
|
RetailLandscapeVisibilityFrame replaced =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.Same(completedEmpty.CellIds, replaced.CellIds);
|
||||||
|
Assert.Equal([0x12340001u], replaced.CellIds);
|
||||||
|
|
||||||
|
controller.Reset();
|
||||||
|
RetailLandscapeVisibilityFrame reset =
|
||||||
|
controller.CaptureCompletedLandscapeVisibility();
|
||||||
|
Assert.Same(completedEmpty.CellIds, reset.CellIds);
|
||||||
|
Assert.False(reset.HasCompletedWorldView);
|
||||||
|
Assert.Empty(reset.CellIds);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CompleteAbortAndResetPublishOnlyCompleteLandscapeTransactions()
|
public void CompleteAbortAndResetPublishOnlyCompleteLandscapeTransactions()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,63 @@ public sealed class DirectionalShadowPreparedDrawTests
|
||||||
Assert.Single(product.Commands.ToArray());
|
Assert.Single(product.Commands.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlternatingCasterSelection_EmitsExactContiguousInstanceRuns()
|
||||||
|
{
|
||||||
|
var product = new DirectionalShadowPreparedDraws();
|
||||||
|
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(18);
|
||||||
|
Assert.True(product.TryBegin(generation, 33, estimatedInstances: 6));
|
||||||
|
for (int casterIndex = 0; casterIndex < 6; casterIndex++)
|
||||||
|
{
|
||||||
|
Matrix4x4 transform = Matrix4x4.CreateTranslation(casterIndex, 0f, 0f);
|
||||||
|
DirectionalShadowTransformSource source =
|
||||||
|
DirectionalShadowTransformSource.Static(casterIndex);
|
||||||
|
product.Add(
|
||||||
|
firstIndex: 100,
|
||||||
|
baseVertex: 7,
|
||||||
|
indexCount: 12,
|
||||||
|
GpuTextureSlot.Unassigned,
|
||||||
|
textureLayer: 0,
|
||||||
|
CullMode.CounterClockwise,
|
||||||
|
DirectionalShadowCasterMaterial.Opaque,
|
||||||
|
in transform,
|
||||||
|
in source);
|
||||||
|
}
|
||||||
|
DirectionalShadowPreparationStats stats = default;
|
||||||
|
product.Complete(generation, 33, in stats);
|
||||||
|
ulong topologySequence = product.BuildSequence;
|
||||||
|
bool[] alternating = [true, false, true, false, true, false];
|
||||||
|
|
||||||
|
product.ApplySelection(alternating, casterSelectionSequence: 1);
|
||||||
|
|
||||||
|
Assert.Single(product.Commands.ToArray());
|
||||||
|
Assert.Equal(6u, product.Commands[0].InstanceCount);
|
||||||
|
Assert.Equal(3, product.ActiveCommands.Length);
|
||||||
|
Assert.Equal([0u, 2u, 4u],
|
||||||
|
product.ActiveCommands.ToArray().Select(static command => command.BaseInstance));
|
||||||
|
Assert.All(product.ActiveCommands.ToArray(),
|
||||||
|
static command => Assert.Equal(1u, command.InstanceCount));
|
||||||
|
Assert.Equal(3, product.ActiveBatches.Length);
|
||||||
|
Assert.Equal(3, product.Stats.ActiveInstances);
|
||||||
|
Assert.Equal(3, product.Stats.ActiveCommands);
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
|
||||||
|
bool[] inverse = [false, true, false, true, false, true];
|
||||||
|
product.ApplySelection(inverse, casterSelectionSequence: 2);
|
||||||
|
Assert.Equal([1u, 3u, 5u],
|
||||||
|
product.ActiveCommands.ToArray().Select(static command => command.BaseInstance));
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
|
||||||
|
ulong selectionSequence = 2;
|
||||||
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||||
|
"DirectionalShadowPreparedDraws.ApplySelection alternating",
|
||||||
|
() => product.ApplySelection(
|
||||||
|
(selectionSequence & 1ul) == 0ul ? alternating : inverse,
|
||||||
|
++selectionSequence),
|
||||||
|
batchSize: 256);
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StableTopology_ComposesSlimPoseWithoutMutatingCasterOrAllocating()
|
public void StableTopology_ComposesSlimPoseWithoutMutatingCasterOrAllocating()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering.Wb;
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -72,4 +74,50 @@ public sealed class DirectionalShadowTerrainPreparedDrawTests
|
||||||
Assert.Single(product.Commands.ToArray());
|
Assert.Single(product.Commands.ToArray());
|
||||||
Assert.Equal(20u, product.Commands[0].FirstIndex);
|
Assert.Equal(20u, product.Commands[0].FirstIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PriorLandscapeSelection_ScansExactAuthoredEightByEightCells()
|
||||||
|
{
|
||||||
|
var product = new DirectionalShadowTerrainPreparedDraws();
|
||||||
|
Assert.True(product.TryBegin(frameSequence: 1, estimatedCommands: 3));
|
||||||
|
DirectionalShadowTerrainRange[] resident =
|
||||||
|
[
|
||||||
|
new(FirstIndex: 0, IndexCount: 384, LandblockId: 0x1111FFFFu),
|
||||||
|
new(FirstIndex: 384, IndexCount: 384, LandblockId: 0x2222FFFFu),
|
||||||
|
new(FirstIndex: 768, IndexCount: 384, LandblockId: 0x3333FFFFu),
|
||||||
|
];
|
||||||
|
foreach (DirectionalShadowTerrainRange range in resident)
|
||||||
|
product.Add(in range);
|
||||||
|
product.Complete(frameSequence: 1);
|
||||||
|
ulong topologySequence = product.BuildSequence;
|
||||||
|
var visible = new HashSet<uint>
|
||||||
|
{
|
||||||
|
0x11110040u,
|
||||||
|
0x22220041u, // valid outdoor family, but not this slot's 8x8 cells.
|
||||||
|
};
|
||||||
|
var visibility = new RetailLandscapeVisibilityFrame(
|
||||||
|
visible,
|
||||||
|
HasCompletedWorldView: true);
|
||||||
|
|
||||||
|
product.ApplySelection(in visibility);
|
||||||
|
|
||||||
|
DrawElementsIndirectCommand selected = Assert.Single(product.Commands.ToArray());
|
||||||
|
Assert.Equal(0u, selected.FirstIndex);
|
||||||
|
Assert.Equal(3, product.ResidentRanges.Length);
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
|
||||||
|
var empty = new RetailLandscapeVisibilityFrame(
|
||||||
|
new HashSet<uint>(),
|
||||||
|
HasCompletedWorldView: true);
|
||||||
|
product.ApplySelection(in empty);
|
||||||
|
Assert.Empty(product.Commands.ToArray());
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
|
||||||
|
product.ApplySelection(in visibility);
|
||||||
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
||||||
|
"DirectionalShadowTerrainPreparedDraws.ApplySelection",
|
||||||
|
() => product.ApplySelection(in visibility),
|
||||||
|
batchSize: 256);
|
||||||
|
Assert.Equal(topologySequence, product.BuildSequence);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Composition;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -46,7 +47,8 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
new RecordingRoots(calls, roots),
|
new RecordingRoots(calls, roots),
|
||||||
environment,
|
environment,
|
||||||
new RecordingAnimated(calls, animated),
|
new RecordingAnimated(calls, animated),
|
||||||
new RecordingBuildings(calls, new WorldBuildingFrame(null, buildings)));
|
new RecordingBuildings(calls, new WorldBuildingFrame(null, buildings)),
|
||||||
|
new RecordingMembership());
|
||||||
|
|
||||||
WorldRenderFrame result = builder.Build(
|
WorldRenderFrame result = builder.Build(
|
||||||
in foundation,
|
in foundation,
|
||||||
|
|
@ -55,6 +57,7 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[
|
[
|
||||||
|
"visibility:capture",
|
||||||
"camera",
|
"camera",
|
||||||
"visibility:begin",
|
"visibility:begin",
|
||||||
"settings",
|
"settings",
|
||||||
|
|
@ -83,21 +86,24 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
var environment = new RecordingEnvironment([]);
|
var environment = new RecordingEnvironment([]);
|
||||||
var animated = new RecordingAnimated([], []);
|
var animated = new RecordingAnimated([], []);
|
||||||
var buildings = new RecordingBuildings([], default);
|
var buildings = new RecordingBuildings([], default);
|
||||||
|
var membership = new RecordingMembership();
|
||||||
|
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(null!, visibility, settings, roots, environment, animated, buildings));
|
new WorldRenderFrameBuilder(null!, visibility, settings, roots, environment, animated, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, null!, settings, roots, environment, animated, buildings));
|
new WorldRenderFrameBuilder(camera, null!, settings, roots, environment, animated, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, visibility, null!, roots, environment, animated, buildings));
|
new WorldRenderFrameBuilder(camera, visibility, null!, roots, environment, animated, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, visibility, settings, null!, environment, animated, buildings));
|
new WorldRenderFrameBuilder(camera, visibility, settings, null!, environment, animated, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, visibility, settings, roots, null!, animated, buildings));
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, null!, animated, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, null!, buildings));
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, null!, buildings, membership));
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, animated, null!));
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, animated, null!, membership));
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, animated, buildings, null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -502,6 +508,12 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
{
|
{
|
||||||
public bool WaitingForLogin { get; private set; }
|
public bool WaitingForLogin { get; private set; }
|
||||||
|
|
||||||
|
public RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility()
|
||||||
|
{
|
||||||
|
calls.Add("visibility:capture");
|
||||||
|
return RetailLandscapeVisibilityFrame.None;
|
||||||
|
}
|
||||||
|
|
||||||
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
||||||
{
|
{
|
||||||
calls.Add("visibility:begin");
|
calls.Add("visibility:begin");
|
||||||
|
|
@ -512,6 +524,66 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
calls.Add("visibility:projection");
|
calls.Add("visibility:projection");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_BorrowsExactPriorCompletedLandscapeBeforeCurrentBegin()
|
||||||
|
{
|
||||||
|
var particles = new ParticleVisibilityController();
|
||||||
|
particles.BeginFrame(Vector3.Zero);
|
||||||
|
particles.UseWorldView();
|
||||||
|
particles.MarkVisibleLandscapeCells([0x12340002u]);
|
||||||
|
particles.CompleteFrame();
|
||||||
|
RetailLandscapeVisibilityFrame ownerBefore =
|
||||||
|
particles.CaptureCompletedLandscapeVisibility();
|
||||||
|
var visibility = new RuntimeWorldFrameVisibilityPreparation(
|
||||||
|
selection: null,
|
||||||
|
particles,
|
||||||
|
terrain: null,
|
||||||
|
reveal: null,
|
||||||
|
environmentFrustum: null);
|
||||||
|
var camera = new FlyCamera();
|
||||||
|
var builder = new WorldRenderFrameBuilder(
|
||||||
|
new RecordingCamera([], CameraFrame(camera)),
|
||||||
|
visibility,
|
||||||
|
new RecordingSettings([]),
|
||||||
|
new RecordingRoots([], default),
|
||||||
|
new RecordingEnvironment([]),
|
||||||
|
new RecordingAnimated([], []),
|
||||||
|
new RecordingBuildings([], default),
|
||||||
|
new RecordingMembership());
|
||||||
|
RenderFrameFoundation foundation = default;
|
||||||
|
|
||||||
|
WorldRenderFrame world = builder.Build(
|
||||||
|
in foundation,
|
||||||
|
waitingForLogin: false,
|
||||||
|
activeDayGroup: null);
|
||||||
|
|
||||||
|
Assert.Same(ownerBefore.CellIds, world.PriorLandscapeVisibility.CellIds);
|
||||||
|
Assert.True(world.PriorLandscapeVisibility.HasCompletedWorldView);
|
||||||
|
Assert.Contains(0x12340002u, world.PriorLandscapeVisibility.CellIds);
|
||||||
|
Assert.Same(
|
||||||
|
ownerBefore.CellIds,
|
||||||
|
particles.CaptureCompletedLandscapeVisibility().CellIds);
|
||||||
|
|
||||||
|
WorldRenderFrame login = builder.Build(
|
||||||
|
in foundation,
|
||||||
|
waitingForLogin: true,
|
||||||
|
activeDayGroup: null);
|
||||||
|
Assert.False(login.PriorLandscapeVisibility.HasCompletedWorldView);
|
||||||
|
Assert.Empty(login.PriorLandscapeVisibility.CellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingMembership : IDirectionalShadowCellMembership
|
||||||
|
{
|
||||||
|
public bool TryGetRetailCellArray(
|
||||||
|
uint entityId,
|
||||||
|
out IReadOnlyList<uint> cells)
|
||||||
|
{
|
||||||
|
_ = entityId;
|
||||||
|
cells = Array.Empty<uint>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingSettings(List<string> calls)
|
private sealed class RecordingSettings(List<string> calls)
|
||||||
: IWorldFrameSettingsPreview
|
: IWorldFrameSettingsPreview
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue