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:
Erik 2026-09-04 17:52:29 +02:00
parent 5c106bcdff
commit a4de2efc4e
19 changed files with 1198 additions and 100 deletions

View file

@ -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) |
--- ---

View file

@ -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; c3c5 and closeout remain | G4 | Packet §§7 and 1214 are binding; never merge main before G4. | | S5 | IN FLIGHT — c1 landed/provisionally self-gated; c2 implementation candidate awaiting clean evidence + dual review; c3c5 and closeout remain | G4 | Packet §§7 and 1214 are binding; never merge main before G4. |
--- ---

View file

@ -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.

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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);
}
} }

View file

@ -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();

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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()
{ {

View file

@ -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,

View file

@ -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()
{ {

View file

@ -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()
{ {

View file

@ -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);
}
} }

View file

@ -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
{ {