perf(streaming): cursor publication across frame budgets
This commit is contained in:
parent
bb16f74fd4
commit
98f1ac8934
32 changed files with 2215 additions and 823 deletions
10
AGENTS.md
10
AGENTS.md
|
|
@ -140,10 +140,12 @@ explicit bake/equivalence/UI-Studio tooling. Capped, uncapped, dense-Arwic,
|
||||||
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
||||||
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
||||||
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
||||||
typed residency policy/accounting and bounded retained caches. Slice E0–E3 now
|
typed residency policy/accounting and bounded retained caches. Slice E0–E4 now
|
||||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||||
and metered full-window/recenter retirement; E4 cursor-budgeted publication is
|
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||||
next. Physical GPU/FPS gates are paused while the desktop is RDP. Slices F/G
|
render/physics/static/building/EnvCell publication; E5 destination reservation
|
||||||
|
and reveal-generation joining is next. The physical console is available and
|
||||||
|
reserved for E6 connected gates. Slices F/G
|
||||||
remain behind the explicit ECS decision gate and Slice J behind its separate
|
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||||
gameplay-owner gate. Evidence:
|
gameplay-owner gate. Evidence:
|
||||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||||
|
|
@ -168,7 +170,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
||||||
truth remain correct.
|
truth remain correct.
|
||||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E4+, and #225's
|
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E5+, and #225's
|
||||||
lifestone/particle alpha visual gate.
|
lifestone/particle alpha visual gate.
|
||||||
|
|
||||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||||
|
|
|
||||||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -138,10 +138,12 @@ explicit bake/equivalence/UI-Studio tooling. Capped, uncapped, dense-Arwic,
|
||||||
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
||||||
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
||||||
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
||||||
typed residency policy/accounting and bounded retained caches. Slice E0–E3 now
|
typed residency policy/accounting and bounded retained caches. Slice E0–E4 now
|
||||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||||
and metered full-window/recenter retirement; E4 cursor-budgeted publication is
|
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||||
next. Physical GPU/FPS gates are paused while the desktop is RDP. Slices F/G
|
render/physics/static/building/EnvCell publication; E5 destination reservation
|
||||||
|
and reveal-generation joining is next. The physical console is available and
|
||||||
|
reserved for E6 connected gates. Slices F/G
|
||||||
remain behind the explicit ECS decision gate and Slice J behind its separate
|
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||||
gameplay-owner gate. Evidence:
|
gameplay-owner gate. Evidence:
|
||||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||||
|
|
@ -166,7 +168,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
||||||
truth remain correct.
|
truth remain correct.
|
||||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E4+, and #225's
|
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E5+, and #225's
|
||||||
lifestone/particle alpha visual gate.
|
lifestone/particle alpha visual gate.
|
||||||
|
|
||||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,12 @@ What does NOT go here:
|
||||||
smooth and physics, collision, and wire state remain correct.
|
smooth and physics, collision, and wire state remain correct.
|
||||||
- **Build hygiene:** `#228` records 17 clean-Release test-project warnings;
|
- **Build hygiene:** `#228` records 17 clean-Release test-project warnings;
|
||||||
production compilation and all tests pass.
|
production compilation and all tests pass.
|
||||||
- **Performance side track:** Slices A–D and Slice E0–E3 are complete. Typed
|
- **Performance side track:** Slices A–D and Slice E0–E4 are complete. Typed
|
||||||
completion admission, immediate generation-scoped old-world quiescence, and
|
completion admission, immediate generation-scoped old-world quiescence, and
|
||||||
budgeted full-window/recenter retirement are live; E4 cursor-budgeted
|
budgeted full-window/recenter retirement are live. Publication now advances
|
||||||
publication is next. Physical performance/visual evidence is paused while
|
from retained render/physics/static/building/EnvCell cursors without replay;
|
||||||
the desktop is RDP. Evidence:
|
E5 destination reservation/reveal joining is next. The physical console is
|
||||||
|
available and remains reserved for the E6 connected gate. Evidence:
|
||||||
[`research/2026-07-24-slice-d-unified-residency-report.md`](research/2026-07-24-slice-d-unified-residency-report.md).
|
[`research/2026-07-24-slice-d-unified-residency-report.md`](research/2026-07-24-slice-d-unified-residency-report.md).
|
||||||
|
|
||||||
The [documentation map](README.md) defines how this tactical ledger relates to
|
The [documentation map](README.md) defines how this tactical ledger relates to
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@ banner cannot silently override the current program state.
|
||||||
[`plans/2026-07-23-world-interaction-completion.md`](plans/2026-07-23-world-interaction-completion.md).
|
[`plans/2026-07-23-world-interaction-completion.md`](plans/2026-07-23-world-interaction-completion.md).
|
||||||
The eight-slice `GameWindow` decomposition and its connected visual matrix
|
The eight-slice `GameWindow` decomposition and its connected visual matrix
|
||||||
are complete.
|
are complete.
|
||||||
- **Current active engineering program:** Slices A–D and Slice E0–E3 of
|
- **Current active engineering program:** Slices A–D and Slice E0–E4 of
|
||||||
[`plans/2026-07-24-modern-runtime-architecture.md`](plans/2026-07-24-modern-runtime-architecture.md)
|
[`plans/2026-07-24-modern-runtime-architecture.md`](plans/2026-07-24-modern-runtime-architecture.md)
|
||||||
are complete. Production world-mesh streaming consumes the validated
|
are complete. Production world-mesh streaming consumes the validated
|
||||||
prepared package, typed residency is bounded/accounted, and streaming now
|
prepared package, typed residency is bounded/accounted, and streaming now
|
||||||
has typed admission plus immediate old-generation quiescence and metered
|
has typed admission plus immediate old-generation quiescence, metered
|
||||||
retirement cursors. E4 cursor-budgeted publication is next. Physical
|
retirement, and retained cursor-budgeted publication across render, physics,
|
||||||
GPU/FPS gates wait for the physical console because the desktop is RDP. See
|
statics, building registries, collision/reflood, and EnvCells. E5 destination
|
||||||
|
reservation/reveal joining is next. The physical console is available; its
|
||||||
|
connected performance and visual evidence remains the E6 gate. See
|
||||||
the [Slice D residency report](research/2026-07-24-slice-d-unified-residency-report.md).
|
the [Slice D residency report](research/2026-07-24-slice-d-unified-residency-report.md).
|
||||||
- **Completed gates:** R6 locomotion/collision/projectile/teleport/radar and
|
- **Completed gates:** R6 locomotion/collision/projectile/teleport/radar and
|
||||||
the final two-client portal-out/materialization observer comparison (`#218`).
|
the final two-client portal-out/materialization observer comparison (`#218`).
|
||||||
|
|
@ -32,7 +34,7 @@ banner cannot silently override the current program state.
|
||||||
user decision gate; Linux/headless automation (Track LH) remains future work.
|
user decision gate; Linux/headless automation (Track LH) remains future work.
|
||||||
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
|
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
|
||||||
and UN 5 — plus the retained retired TS-37 history note.
|
and UN 5 — plus the retained retired TS-37 history note.
|
||||||
- **Latest automated baseline:** Release build succeeds; 8,148 tests pass and
|
- **Latest automated baseline:** Release build succeeds; 8,152 tests pass and
|
||||||
five are intentionally skipped. The capped, uncapped, and dense-Arwic
|
five are intentionally skipped. The capped, uncapped, and dense-Arwic
|
||||||
physical prepared-asset routes all completed their portal/input/lifetime
|
physical prepared-asset routes all completed their portal/input/lifetime
|
||||||
assertions and disconnected gracefully.
|
assertions and disconnected gracefully.
|
||||||
|
|
|
||||||
|
|
@ -731,8 +731,18 @@ readiness continue. Canonical live records remain retained until physical
|
||||||
teardown converges, so quiescence neither reconstructs server identity nor
|
teardown converges, so quiescence neither reconstructs server identity nor
|
||||||
invents Hidden/UnHide transitions. Full-window and shared-origin retirement
|
invents Hidden/UnHide transitions. Full-window and shared-origin retirement
|
||||||
advance only from `StreamingController.Tick` through stable resident and
|
advance only from `StreamingController.Tick` through stable resident and
|
||||||
per-owner entity/stage cursors on the frame's one meter. Publication remains
|
per-owner entity/stage cursors on the frame's one meter.
|
||||||
transaction-atomic until Slice E4.
|
|
||||||
|
Accepted publication is likewise retained at its exact priority-queue head.
|
||||||
|
`LandblockPresentationPipeline` meters render, physics, static, building, and
|
||||||
|
EnvCell receipts one operation at a time; each owner captures stable ordered
|
||||||
|
inputs once and retries only its unfinished cursor. Building and EnvCell
|
||||||
|
replacements are constructed off-side and atomically replace their complete
|
||||||
|
owner snapshots. The final `GpuWorldState.MutationBatch` is intentionally one
|
||||||
|
observer-atomic boundary so bucket identity, render pins, activation, and the
|
||||||
|
outer visibility notice cannot be observed partially. Settings and native
|
||||||
|
callbacks may defer policy changes, but cannot bypass the frame meter to drain
|
||||||
|
pending publication.
|
||||||
|
|
||||||
### World-reveal readiness ownership
|
### World-reveal readiness ownership
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| AD-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal `elapsed >= 1.0` state edge. Incoming fades retain the exact timer. | `src/AcDream.Core/World/TeleportAnimSequencer.cs` (`OutgoingViewportReachedTerminalProjection`) | An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | `UIGlobals::GetAnimLevel @ 0x004EE540`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; paired retail/acdream captures documented in `docs/research/2026-07-15-retail-portal-space-pseudocode.md` |
|
| AD-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal `elapsed >= 1.0` state edge. Incoming fades retain the exact timer. | `src/AcDream.Core/World/TeleportAnimSequencer.cs` (`OutgoingViewportReachedTerminalProjection`) | An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | `UIGlobals::GetAnimLevel @ 0x004EE540`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; paired retail/acdream captures documented in `docs/research/2026-07-15-retail-portal-space-pseudocode.md` |
|
||||||
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
|
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
|
||||||
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. `WorldRevealCoordinator` owns this shared lifetime; diagnostic telemetry observes its generation without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. The portal hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (owner wiring + draw eligibility); `src/AcDream.App/Streaming/StreamingController.cs` (`IsRenderNeighborhoodResident`); `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit reaches its existing loud timeout/forced-placement diagnostic. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
|
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. `WorldRevealCoordinator` owns this shared lifetime; diagnostic telemetry observes its generation without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **Slice E4 refinement (2026-07-24):** accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. The portal hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (owner wiring + draw eligibility); `src/AcDream.App/Streaming/StreamingController.cs` (`IsRenderNeighborhoodResident`); `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit reaches its existing loud timeout/forced-placement diagnostic. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
|
||||||
| AD-3 | Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data | `src/AcDream.Core/Physics/CellTransit.cs:503` | Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk | Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building | `CObjCell::find_cell_list` 0052b535-0052b56c (null-CLandCell case) |
|
| AD-3 | Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data | `src/AcDream.Core/Physics/CellTransit.cs:503` | Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk | Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building | `CObjCell::find_cell_list` 0052b535-0052b56c (null-CLandCell case) |
|
||||||
| AD-4 | `point_in_cell` against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells | `src/AcDream.Core/Physics/CellTransit.cs:588` | The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice | During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root | `CEnvCell::find_visible_child_cell` :311397; cell-BSP vtable[0x84] |
|
| AD-4 | `point_in_cell` against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells | `src/AcDream.Core/Physics/CellTransit.cs:588` | The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice | During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root | `CEnvCell::find_visible_child_cell` :311397; cell-BSP vtable[0x84] |
|
||||||
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
|
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
|
||||||
|
|
|
||||||
|
|
@ -1613,7 +1613,10 @@ port in any phase — no separate listing here.
|
||||||
> generation unavailable at the hard reveal edge and freezes its world
|
> generation unavailable at the hard reveal edge and freezes its world
|
||||||
> simulation/audio while full-window and shared-origin teardown advance through
|
> simulation/audio while full-window and shared-origin teardown advance through
|
||||||
> metered resident, entity, and owner-stage cursors with exact retry receipts.
|
> metered resident, entity, and owner-stage cursors with exact retry receipts.
|
||||||
> E4 cursor-budgets publication next. F/G and J retain their explicit approval
|
> E4 now retains the exact accepted queue head and cursor-budgets render,
|
||||||
|
> physics, static, building-registry, EnvCell, collision, and reflood
|
||||||
|
> publication before one observer-atomic spatial swap. E5 destination
|
||||||
|
> reservation/reveal joining is next. F/G and J retain their explicit approval
|
||||||
> gates.
|
> gates.
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||||
|
|
|
||||||
|
|
@ -93,12 +93,14 @@ The separately authorized modern-runtime performance program has completed
|
||||||
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
||||||
production streaming, and typed unified residency. Its physical capped,
|
production streaming, and typed unified residency. Its physical capped,
|
||||||
uncapped, and dense connected gates pass without a visual-quality change.
|
uncapped, and dense connected gates pass without a visual-quality change.
|
||||||
Slice E0–E3 now define and enforce typed completion admission through explicit
|
Slice E0–E4 now define and enforce typed completion admission through explicit
|
||||||
stable priority queues, publish an immediate generation-scoped old-world
|
stable priority queues, publish an immediate generation-scoped old-world
|
||||||
quiesce at login/portal boundaries, and advance full-window/recenter teardown
|
quiesce at login/portal boundaries, and advance full-window/recenter teardown
|
||||||
through exact metered retirement cursors. E4 next makes publication itself
|
through exact metered retirement cursors. Render, physics, static, building,
|
||||||
cursor-budgeted. Slices F/G remain behind their explicit ECS approval gate, and
|
and EnvCell publication now resume from retained exact cursors under the same
|
||||||
Slice J remains behind its gameplay-owner approval gate.
|
frame budget. E5 destination reservation and reveal-generation joining is
|
||||||
|
next. Slices F/G remain behind their explicit ECS approval gate, and Slice J
|
||||||
|
remains behind its gameplay-owner approval gate.
|
||||||
|
|
||||||
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2's
|
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2's
|
||||||
status hand selected-object availability and existing
|
status hand selected-object availability and existing
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
**Date:** 2026-07-24
|
**Date:** 2026-07-24
|
||||||
|
|
||||||
**Status:** Slices A–D complete. Slice E is active (E0–E3 complete).
|
**Status:** Slices A–D complete. Slice E is active (E0–E4 complete).
|
||||||
|
|
||||||
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
||||||
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
||||||
|
|
@ -728,9 +728,10 @@ checkpoint retained staged or retiring bytes. Evidence:
|
||||||
|
|
||||||
**Checkpoint 2026-07-24:** E0 fixed the retail/adaptation contract and current
|
**Checkpoint 2026-07-24:** E0 fixed the retail/adaptation contract and current
|
||||||
owner inventory. E1 landed the validated work profile, deterministic meter and
|
owner inventory. E1 landed the validated work profile, deterministic meter and
|
||||||
completion charge model, plus lifecycle-artifact shadow diagnostics while
|
completion charge model. E2 cut execution over to stable priority FIFOs. E3
|
||||||
preserving the prior scheduler's execution. E2 is the first enforcement
|
added immediate generation quiescence and cursored retirement. E4 now advances
|
||||||
cutover.
|
render/physics/static publication from retained exact receipts; E5 destination
|
||||||
|
reservation and reveal-generation joining is next.
|
||||||
|
|
||||||
**Purpose:** Remove update-thread portal transactions.
|
**Purpose:** Remove update-thread portal transactions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
|
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
|
||||||
|
|
||||||
**Status:** active — E0/E1/E2/E3 complete; E4 cursor-budgeted publication next
|
**Status:** active — E0/E1/E2/E3/E4 complete; E5 destination reservation next
|
||||||
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||||
**Baseline:** Slice D closeout commit `66690805`
|
**Baseline:** Slice D closeout commit `66690805`
|
||||||
**Behavior contract:** no visual-quality or retail-behavior reduction
|
**Behavior contract:** no visual-quality or retail-behavior reduction
|
||||||
|
|
@ -325,6 +325,29 @@ valid GPU/FPS evidence.
|
||||||
- Feed GPU requested-byte and retirement-operation facts from renderer owners.
|
- Feed GPU requested-byte and retirement-operation facts from renderer owners.
|
||||||
- Prove every failure resumes the exact unfinished operation without replay.
|
- Prove every failure resumes the exact unfinished operation without replay.
|
||||||
|
|
||||||
|
**Complete 2026-07-24.** One accepted completion now remains at the exact queue
|
||||||
|
head while `LandblockPresentationPipeline` advances retained render, physics,
|
||||||
|
and static receipts through the frame's single meter. Stable entity/Gfx/building
|
||||||
|
and prior-owner arrays are captured once. Terrain upload bytes are charged at
|
||||||
|
the render owner; physics cells/buildings, static collision/light/plugin work,
|
||||||
|
prior static teardown, shadow reflood, building-registry construction, and
|
||||||
|
EnvCell shell construction advance one bounded operation at a time. Building
|
||||||
|
and EnvCell replacements are prepared off-side and become visible only when
|
||||||
|
complete. Failures retain the unfinished cursor and never replay a committed
|
||||||
|
prefix. Settings/native callbacks can defer policy changes but cannot drain
|
||||||
|
publication outside `StreamingController.Tick`.
|
||||||
|
|
||||||
|
The final `GpuWorldState.MutationBatch` remains one deliberate observer-atomic
|
||||||
|
operation: bucket mutation, render-id ownership, activation, and the outer
|
||||||
|
visibility notification cannot span frames without exposing a partial spatial
|
||||||
|
identity. It is time-metered and named for E6 overrun evidence; all iterable
|
||||||
|
owner work now occurs before it.
|
||||||
|
|
||||||
|
Validation: focused render/building/publication/controller tests passed; the
|
||||||
|
complete App suite passed 3,669 / 3 skipped; the complete Release solution
|
||||||
|
passed 8,152 / 5 skipped; Release solution build green. Physical connected
|
||||||
|
performance and visual evidence remains an E6 gate.
|
||||||
|
|
||||||
### E5 — Destination reservation and reveal generation
|
### E5 — Destination reservation and reveal generation
|
||||||
|
|
||||||
- Replace eager `PriorityRadius` execution with destination reservations.
|
- Replace eager `PriorityRadius` execution with destination reservations.
|
||||||
|
|
|
||||||
|
|
@ -789,11 +789,11 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
foundation.Terrain.RemoveLandblock,
|
foundation.Terrain.RemoveLandblock,
|
||||||
d.CellVisibility,
|
d.CellVisibility,
|
||||||
worldState,
|
worldState,
|
||||||
envCellLease.Resource.CommitLandblock,
|
prepareEnvCells: build => EnvCellMeshPreparationScheduler.Schedule(
|
||||||
build => EnvCellMeshPreparationScheduler.Schedule(
|
|
||||||
build,
|
build,
|
||||||
foundation.MeshAdapter.MeshManager!),
|
foundation.MeshAdapter.MeshManager!),
|
||||||
envCellLease.Resource.RemoveLandblock);
|
removeEnvCells: envCellLease.Resource.RemoveLandblock,
|
||||||
|
envCellPublisher: envCellLease.Resource);
|
||||||
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
|
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
|
||||||
d.PhysicsEngine,
|
d.PhysicsEngine,
|
||||||
world.TerrainBuild.HeightTable);
|
world.TerrainBuild.HeightTable);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,30 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// <c>docs/research/named-retail/acclient.h:32035</c> (<c>BuildInfo</c>) and
|
/// <c>docs/research/named-retail/acclient.h:32035</c> (<c>BuildInfo</c>) and
|
||||||
/// <c>:32094</c> (<c>CBldPortal</c>).</para>
|
/// <c>:32094</c> (<c>CBldPortal</c>).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
internal sealed class BuildingRegistryPublication
|
||||||
|
{
|
||||||
|
internal BuildingRegistryPublication(
|
||||||
|
BuildingInfo[] buildings,
|
||||||
|
uint landblockId,
|
||||||
|
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
|
||||||
|
{
|
||||||
|
Buildings = buildings;
|
||||||
|
LandblockId = landblockId;
|
||||||
|
CellsByCellId = cellsByCellId;
|
||||||
|
PreparationCommitted = buildings.Length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal BuildingInfo[] Buildings { get; }
|
||||||
|
internal uint LandblockId { get; }
|
||||||
|
internal IReadOnlyDictionary<uint, LoadedCell> CellsByCellId { get; }
|
||||||
|
internal BuildingRegistry Registry { get; } = new();
|
||||||
|
internal List<(LoadedCell Cell, uint BuildingId)> CellStamps { get; } = new();
|
||||||
|
internal int BuildingCursor { get; set; }
|
||||||
|
internal uint NextBuildingId { get; set; } = 1;
|
||||||
|
internal bool PreparationCommitted { get; set; }
|
||||||
|
internal bool PublicationCommitted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public static class BuildingLoader
|
public static class BuildingLoader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -39,122 +64,167 @@ public static class BuildingLoader
|
||||||
/// Building IDs are allocated sequentially starting at 1 (0 is reserved for
|
/// Building IDs are allocated sequentially starting at 1 (0 is reserved for
|
||||||
/// "no building" semantics used by <c>LoadedCell.BuildingId</c> in RR4).
|
/// "no building" semantics used by <c>LoadedCell.BuildingId</c> in RR4).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="info">The dat-loaded <see cref="LandBlockInfo"/> for this landblock.</param>
|
|
||||||
/// <param name="landblockId">The 32-bit landblock id (e.g. <c>0xA9B40000</c>).
|
|
||||||
/// The high 16 bits are ORed with each 16-bit <c>OtherCellId</c> to produce
|
|
||||||
/// the full cell id.</param>
|
|
||||||
/// <param name="cellsByCellId">Pre-loaded cells keyed by full 32-bit cell id.
|
|
||||||
/// Used for BFS extension (Step B) and exit-polygon collection (Step C).
|
|
||||||
/// An empty dict is valid for unit tests; Step B and C are skipped per cell.</param>
|
|
||||||
/// <returns>A fully populated <see cref="BuildingRegistry"/>; never null.</returns>
|
|
||||||
public static BuildingRegistry Build(
|
public static BuildingRegistry Build(
|
||||||
LandBlockInfo info,
|
LandBlockInfo info,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
|
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
|
||||||
{
|
{
|
||||||
var reg = new BuildingRegistry();
|
BuildingRegistryPublication publication = PreparePublication(
|
||||||
if (info.Buildings is null || info.Buildings.Count == 0)
|
info,
|
||||||
return reg;
|
landblockId,
|
||||||
|
cellsByCellId);
|
||||||
uint lbMask = landblockId & 0xFFFF0000u;
|
while (!AdvancePreparationOne(publication))
|
||||||
uint nextId = 1;
|
|
||||||
|
|
||||||
foreach (var bInfo in info.Buildings)
|
|
||||||
{
|
{
|
||||||
var envCellIds = new HashSet<uint>();
|
}
|
||||||
var exitPortalPolys = new List<Vector3[]>();
|
CommitPublication(publication);
|
||||||
|
return publication.Registry;
|
||||||
|
}
|
||||||
|
|
||||||
// Step A: seed the cell set from BuildingInfo.Portals (entry portals).
|
internal static BuildingRegistryPublication PreparePublication(
|
||||||
// Each BuildingPortal.OtherCellId is a 16-bit cell-local id; OR with
|
LandBlockInfo info,
|
||||||
// the landblock prefix for the full id.
|
uint landblockId,
|
||||||
// Defensive: skip OtherCellId == 0xFFFF (exit-portal sentinel in
|
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
|
||||||
// BuildingInfo — rare but WB guards against it too at PortalService.cs:58).
|
{
|
||||||
if (bInfo.Portals is not null)
|
ArgumentNullException.ThrowIfNull(info);
|
||||||
|
ArgumentNullException.ThrowIfNull(cellsByCellId);
|
||||||
|
|
||||||
|
int buildingCount = info.Buildings?.Count ?? 0;
|
||||||
|
var buildings = new BuildingInfo[buildingCount];
|
||||||
|
for (int index = 0; index < buildingCount; index++)
|
||||||
|
buildings[index] = info.Buildings![index];
|
||||||
|
return new BuildingRegistryPublication(
|
||||||
|
buildings,
|
||||||
|
landblockId,
|
||||||
|
cellsByCellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool AdvancePreparationOne(
|
||||||
|
BuildingRegistryPublication publication)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(publication);
|
||||||
|
if (publication.PreparationCommitted)
|
||||||
|
return true;
|
||||||
|
if (publication.BuildingCursor >= publication.Buildings.Length)
|
||||||
|
{
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddBuilding(
|
||||||
|
publication,
|
||||||
|
publication.Buildings[publication.BuildingCursor]);
|
||||||
|
publication.BuildingCursor++;
|
||||||
|
if (publication.BuildingCursor >= publication.Buildings.Length)
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return publication.PreparationCommitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void CommitPublication(
|
||||||
|
BuildingRegistryPublication publication)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(publication);
|
||||||
|
if (!publication.PreparationCommitted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A building registry cannot publish before preparation completes.");
|
||||||
|
}
|
||||||
|
if (publication.PublicationCommitted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach ((LoadedCell cell, uint buildingId) in publication.CellStamps)
|
||||||
|
cell.BuildingId = buildingId;
|
||||||
|
publication.PublicationCommitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddBuilding(
|
||||||
|
BuildingRegistryPublication publication,
|
||||||
|
BuildingInfo bInfo)
|
||||||
|
{
|
||||||
|
uint lbMask = publication.LandblockId & 0xFFFF0000u;
|
||||||
|
var envCellIds = new HashSet<uint>();
|
||||||
|
var exitPortalPolys = new List<Vector3[]>();
|
||||||
|
|
||||||
|
// Step A: seed the cell set from BuildingInfo.Portals.
|
||||||
|
if (bInfo.Portals is not null)
|
||||||
|
{
|
||||||
|
foreach (var portal in bInfo.Portals)
|
||||||
{
|
{
|
||||||
foreach (var portal in bInfo.Portals)
|
if (portal.OtherCellId == 0xFFFF) continue;
|
||||||
{
|
envCellIds.Add(lbMask | portal.OtherCellId);
|
||||||
if (portal.OtherCellId == 0xFFFF) continue;
|
|
||||||
envCellIds.Add(lbMask | portal.OtherCellId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step B: BFS through interior CellPortals to find the full cell set.
|
|
||||||
// Uses pre-loaded LoadedCell.Portals (avoids a duplicate dat fetch per
|
|
||||||
// BFS step). Mirrors WB PortalService.cs:67-79.
|
|
||||||
// When cellsByCellId is empty (unit-test path), BFS immediately exits.
|
|
||||||
var queue = new Queue<uint>(envCellIds);
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var current = queue.Dequeue();
|
|
||||||
if (!cellsByCellId.TryGetValue(current, out var cell)) continue;
|
|
||||||
foreach (var p in cell.Portals)
|
|
||||||
{
|
|
||||||
if (p.OtherCellId == 0xFFFF) continue; // exit portal — stop BFS here
|
|
||||||
uint neighbourId = lbMask | p.OtherCellId;
|
|
||||||
if (envCellIds.Add(neighbourId))
|
|
||||||
queue.Enqueue(neighbourId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step C: collect exit portal polygons in world space.
|
|
||||||
// For each interior cell, iterate its portals; for each exit portal
|
|
||||||
// (OtherCellId == 0xFFFF), transform the portal polygon vertices from
|
|
||||||
// cell-local space to world space via WorldTransform.
|
|
||||||
// Mirrors WB PortalService.cs:81-86 (GetPortalsForCell return path).
|
|
||||||
foreach (var cellId in envCellIds)
|
|
||||||
{
|
|
||||||
if (!cellsByCellId.TryGetValue(cellId, out var cell)) continue;
|
|
||||||
for (int pi = 0; pi < cell.Portals.Count; pi++)
|
|
||||||
{
|
|
||||||
if (cell.Portals[pi].OtherCellId != 0xFFFF) continue;
|
|
||||||
if (pi >= cell.PortalPolygons.Count) continue;
|
|
||||||
var localPoly = cell.PortalPolygons[pi];
|
|
||||||
if (localPoly.Length < 3) continue;
|
|
||||||
var worldPoly = new Vector3[localPoly.Length];
|
|
||||||
for (int v = 0; v < localPoly.Length; v++)
|
|
||||||
worldPoly[v] = Vector3.Transform(localPoly[v], cell.WorldTransform);
|
|
||||||
exitPortalPolys.Add(worldPoly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasPortalBounds = false;
|
|
||||||
var portalMin = new Vector3(float.MaxValue);
|
|
||||||
var portalMax = new Vector3(float.MinValue);
|
|
||||||
foreach (var poly in exitPortalPolys)
|
|
||||||
{
|
|
||||||
foreach (var v in poly)
|
|
||||||
{
|
|
||||||
hasPortalBounds = true;
|
|
||||||
portalMin = Vector3.Min(portalMin, v);
|
|
||||||
portalMax = Vector3.Max(portalMax, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WB PortalService.cs:89: skip buildings with no interior cells.
|
|
||||||
if (envCellIds.Count == 0) continue;
|
|
||||||
|
|
||||||
var building = new Building
|
|
||||||
{
|
|
||||||
BuildingId = nextId++,
|
|
||||||
EnvCellIds = envCellIds,
|
|
||||||
ExitPortalPolygons = exitPortalPolys,
|
|
||||||
HasPortalBounds = hasPortalBounds,
|
|
||||||
PortalBounds = hasPortalBounds
|
|
||||||
? new WbBoundingBox(portalMin, portalMax)
|
|
||||||
: default,
|
|
||||||
};
|
|
||||||
reg.Add(building);
|
|
||||||
|
|
||||||
// Step 4: stamp BuildingId on each cell (Option C — both directions
|
|
||||||
// O(1)). The internal setter on LoadedCell.BuildingId is accessible
|
|
||||||
// because this class lives in the same assembly (AcDream.App).
|
|
||||||
foreach (var cellId in envCellIds)
|
|
||||||
{
|
|
||||||
if (cellsByCellId.TryGetValue(cellId, out var cell))
|
|
||||||
cell.BuildingId = building.BuildingId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reg;
|
// Step B: BFS through interior portals.
|
||||||
|
var queue = new Queue<uint>(envCellIds);
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
uint current = queue.Dequeue();
|
||||||
|
if (!publication.CellsByCellId.TryGetValue(current, out var cell))
|
||||||
|
continue;
|
||||||
|
foreach (var portal in cell.Portals)
|
||||||
|
{
|
||||||
|
if (portal.OtherCellId == 0xFFFF) continue;
|
||||||
|
uint neighbourId = lbMask | portal.OtherCellId;
|
||||||
|
if (envCellIds.Add(neighbourId))
|
||||||
|
queue.Enqueue(neighbourId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step C: collect exit portal polygons in world space.
|
||||||
|
foreach (uint cellId in envCellIds)
|
||||||
|
{
|
||||||
|
if (!publication.CellsByCellId.TryGetValue(cellId, out var cell))
|
||||||
|
continue;
|
||||||
|
for (int portalIndex = 0; portalIndex < cell.Portals.Count; portalIndex++)
|
||||||
|
{
|
||||||
|
if (cell.Portals[portalIndex].OtherCellId != 0xFFFF) continue;
|
||||||
|
if (portalIndex >= cell.PortalPolygons.Count) continue;
|
||||||
|
Vector3[] localPolygon = cell.PortalPolygons[portalIndex];
|
||||||
|
if (localPolygon.Length < 3) continue;
|
||||||
|
var worldPolygon = new Vector3[localPolygon.Length];
|
||||||
|
for (int vertexIndex = 0; vertexIndex < localPolygon.Length; vertexIndex++)
|
||||||
|
{
|
||||||
|
worldPolygon[vertexIndex] = Vector3.Transform(
|
||||||
|
localPolygon[vertexIndex],
|
||||||
|
cell.WorldTransform);
|
||||||
|
}
|
||||||
|
exitPortalPolys.Add(worldPolygon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasPortalBounds = false;
|
||||||
|
var portalMin = new Vector3(float.MaxValue);
|
||||||
|
var portalMax = new Vector3(float.MinValue);
|
||||||
|
foreach (Vector3[] polygon in exitPortalPolys)
|
||||||
|
{
|
||||||
|
foreach (Vector3 vertex in polygon)
|
||||||
|
{
|
||||||
|
hasPortalBounds = true;
|
||||||
|
portalMin = Vector3.Min(portalMin, vertex);
|
||||||
|
portalMax = Vector3.Max(portalMax, vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envCellIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint buildingId = publication.NextBuildingId++;
|
||||||
|
publication.Registry.Add(new Building
|
||||||
|
{
|
||||||
|
BuildingId = buildingId,
|
||||||
|
EnvCellIds = envCellIds,
|
||||||
|
ExitPortalPolygons = exitPortalPolys,
|
||||||
|
HasPortalBounds = hasPortalBounds,
|
||||||
|
PortalBounds = hasPortalBounds
|
||||||
|
? new WbBoundingBox(portalMin, portalMax)
|
||||||
|
: default,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stamps remain off-side until the complete registry commits.
|
||||||
|
foreach (uint cellId in envCellIds)
|
||||||
|
{
|
||||||
|
if (publication.CellsByCellId.TryGetValue(cellId, out var cell))
|
||||||
|
publication.CellStamps.Add((cell, buildingId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
Normal file
45
src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrow update-thread publication seam used by streaming. Preparation builds
|
||||||
|
/// one private shell at a time; commit publishes the completed immutable
|
||||||
|
/// landblock snapshot with one owner dictionary replacement.
|
||||||
|
/// </summary>
|
||||||
|
public interface IEnvCellLandblockPublisher
|
||||||
|
{
|
||||||
|
EnvCellLandblockPublication PreparePublication(
|
||||||
|
EnvCellLandblockBuild build);
|
||||||
|
|
||||||
|
bool AdvancePreparationOne(EnvCellLandblockPublication publication);
|
||||||
|
|
||||||
|
void CommitPublication(EnvCellLandblockPublication publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EnvCellLandblockPublication
|
||||||
|
{
|
||||||
|
internal EnvCellLandblockPublication(
|
||||||
|
object owner,
|
||||||
|
EnvCellLandblockBuild build)
|
||||||
|
{
|
||||||
|
Owner = owner;
|
||||||
|
Build = build;
|
||||||
|
Replacement = new EnvCellLandblock
|
||||||
|
{
|
||||||
|
GridX = (int)((build.LandblockId >> 24) & 0xFFu),
|
||||||
|
GridY = (int)((build.LandblockId >> 16) & 0xFFu),
|
||||||
|
};
|
||||||
|
TotalBounds = new WbBoundingBox(
|
||||||
|
new Vector3(float.MaxValue),
|
||||||
|
new Vector3(float.MinValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object Owner { get; }
|
||||||
|
internal EnvCellLandblockBuild Build { get; }
|
||||||
|
internal EnvCellLandblock Replacement { get; }
|
||||||
|
internal WbBoundingBox TotalBounds { get; set; }
|
||||||
|
internal int ShellCursor { get; set; }
|
||||||
|
internal bool PreparationCommitted { get; set; }
|
||||||
|
internal bool PublicationCommitted { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -29,8 +29,11 @@ using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
public sealed unsafe class EnvCellRenderer : IDisposable
|
public sealed unsafe class EnvCellRenderer :
|
||||||
|
IDisposable,
|
||||||
|
IEnvCellLandblockPublisher
|
||||||
{
|
{
|
||||||
|
private readonly object _publicationOwner = new();
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly ObjectMeshManager _meshManager;
|
private readonly ObjectMeshManager _meshManager;
|
||||||
private readonly WbFrustum _frustum;
|
private readonly WbFrustum _frustum;
|
||||||
|
|
@ -381,8 +384,80 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CommitLandblock(EnvCellLandblockBuild build)
|
public void CommitLandblock(EnvCellLandblockBuild build)
|
||||||
{
|
{
|
||||||
_landblocks[build.LandblockId] = CreateCommittedSnapshot(build);
|
EnvCellLandblockPublication publication = PreparePublication(build);
|
||||||
|
while (!AdvancePreparationOne(publication))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
CommitPublication(publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvCellLandblockPublication IEnvCellLandblockPublisher.PreparePublication(
|
||||||
|
EnvCellLandblockBuild build) =>
|
||||||
|
PreparePublication(build);
|
||||||
|
|
||||||
|
bool IEnvCellLandblockPublisher.AdvancePreparationOne(
|
||||||
|
EnvCellLandblockPublication publication) =>
|
||||||
|
AdvancePreparationOne(publication);
|
||||||
|
|
||||||
|
void IEnvCellLandblockPublisher.CommitPublication(
|
||||||
|
EnvCellLandblockPublication publication) =>
|
||||||
|
CommitPublication(publication);
|
||||||
|
|
||||||
|
internal EnvCellLandblockPublication PreparePublication(
|
||||||
|
EnvCellLandblockBuild build)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(build);
|
||||||
|
return new EnvCellLandblockPublication(_publicationOwner, build);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool AdvancePreparationOne(
|
||||||
|
EnvCellLandblockPublication publication)
|
||||||
|
{
|
||||||
|
ValidatePublication(publication);
|
||||||
|
if (publication.PreparationCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (publication.ShellCursor < publication.Build.Shells.Length)
|
||||||
|
{
|
||||||
|
AddShell(
|
||||||
|
publication.Replacement,
|
||||||
|
publication.Build.Shells[publication.ShellCursor]);
|
||||||
|
WbBoundingBox bounds =
|
||||||
|
publication.Build.Shells[publication.ShellCursor].WorldBounds;
|
||||||
|
publication.TotalBounds =
|
||||||
|
publication.ShellCursor == 0
|
||||||
|
? bounds
|
||||||
|
: WbBoundingBox.Union(
|
||||||
|
publication.TotalBounds,
|
||||||
|
bounds);
|
||||||
|
publication.ShellCursor++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication.Replacement.TotalEnvCellBounds =
|
||||||
|
publication.Replacement.EnvCellBounds.Count == 0
|
||||||
|
? new WbBoundingBox(Vector3.Zero, Vector3.Zero)
|
||||||
|
: publication.TotalBounds;
|
||||||
|
publication.Replacement.InstancesReady = true;
|
||||||
|
publication.Replacement.MeshDataReady = true;
|
||||||
|
publication.Replacement.GpuReady = true;
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CommitPublication(
|
||||||
|
EnvCellLandblockPublication publication)
|
||||||
|
{
|
||||||
|
ValidatePublication(publication);
|
||||||
|
if (!publication.PreparationCommitted)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"EnvCell landblock publication cannot commit before preparation.");
|
||||||
|
if (publication.PublicationCommitted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_landblocks[publication.Build.LandblockId] = publication.Replacement;
|
||||||
NeedsPrepare = true;
|
NeedsPrepare = true;
|
||||||
|
publication.PublicationCommitted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -398,35 +473,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var shell in build.Shells)
|
foreach (var shell in build.Shells)
|
||||||
{
|
AddShell(replacement, shell);
|
||||||
replacement.Instances.Add(new EnvCellSceneryInstance
|
|
||||||
{
|
|
||||||
ObjectId = shell.GeometryId,
|
|
||||||
InstanceId = shell.CellId,
|
|
||||||
IsBuilding = true,
|
|
||||||
IsEntryCell = false,
|
|
||||||
WorldPosition = shell.WorldPosition,
|
|
||||||
LocalPosition = Vector3.Zero,
|
|
||||||
Rotation = shell.Rotation,
|
|
||||||
Scale = Vector3.One,
|
|
||||||
Transform = shell.Transform,
|
|
||||||
LocalBoundingBox = shell.LocalBounds,
|
|
||||||
BoundingBox = shell.WorldBounds,
|
|
||||||
});
|
|
||||||
replacement.EnvCellBounds[shell.CellId] = shell.WorldBounds;
|
|
||||||
|
|
||||||
if (!replacement.BuildingPartGroups.TryGetValue(shell.GeometryId, out var instances))
|
|
||||||
{
|
|
||||||
instances = new List<InstanceData>();
|
|
||||||
replacement.BuildingPartGroups[shell.GeometryId] = instances;
|
|
||||||
}
|
|
||||||
instances.Add(new InstanceData
|
|
||||||
{
|
|
||||||
Transform = shell.Transform,
|
|
||||||
CellId = shell.CellId,
|
|
||||||
Flags = 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var total = new WbBoundingBox(new Vector3(float.MaxValue), new Vector3(float.MinValue));
|
var total = new WbBoundingBox(new Vector3(float.MaxValue), new Vector3(float.MinValue));
|
||||||
foreach (var bounds in replacement.EnvCellBounds.Values)
|
foreach (var bounds in replacement.EnvCellBounds.Values)
|
||||||
|
|
@ -441,6 +488,53 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
return replacement;
|
return replacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddShell(
|
||||||
|
EnvCellLandblock replacement,
|
||||||
|
EnvCellShellPlacement shell)
|
||||||
|
{
|
||||||
|
replacement.Instances.Add(new EnvCellSceneryInstance
|
||||||
|
{
|
||||||
|
ObjectId = shell.GeometryId,
|
||||||
|
InstanceId = shell.CellId,
|
||||||
|
IsBuilding = true,
|
||||||
|
IsEntryCell = false,
|
||||||
|
WorldPosition = shell.WorldPosition,
|
||||||
|
LocalPosition = Vector3.Zero,
|
||||||
|
Rotation = shell.Rotation,
|
||||||
|
Scale = Vector3.One,
|
||||||
|
Transform = shell.Transform,
|
||||||
|
LocalBoundingBox = shell.LocalBounds,
|
||||||
|
BoundingBox = shell.WorldBounds,
|
||||||
|
});
|
||||||
|
replacement.EnvCellBounds[shell.CellId] = shell.WorldBounds;
|
||||||
|
|
||||||
|
if (!replacement.BuildingPartGroups.TryGetValue(
|
||||||
|
shell.GeometryId,
|
||||||
|
out var instances))
|
||||||
|
{
|
||||||
|
instances = new List<InstanceData>();
|
||||||
|
replacement.BuildingPartGroups[shell.GeometryId] = instances;
|
||||||
|
}
|
||||||
|
instances.Add(new InstanceData
|
||||||
|
{
|
||||||
|
Transform = shell.Transform,
|
||||||
|
CellId = shell.CellId,
|
||||||
|
Flags = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidatePublication(
|
||||||
|
EnvCellLandblockPublication publication)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(publication);
|
||||||
|
if (!ReferenceEquals(publication.Owner, _publicationOwner))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"The EnvCell publication receipt belongs to another renderer.",
|
||||||
|
nameof(publication));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a landblock from the renderer. Future PrepareRenderBatches will exclude it.
|
/// Removes a landblock from the renderer. Future PrepareRenderBatches will exclude it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
|
@ -15,15 +16,45 @@ public sealed class LandblockPhysicsPublication
|
||||||
internal LandblockPhysicsPublication(
|
internal LandblockPhysicsPublication(
|
||||||
object owner,
|
object owner,
|
||||||
LandblockBuild build,
|
LandblockBuild build,
|
||||||
Vector3 origin)
|
Vector3 origin,
|
||||||
|
uint currentCellId,
|
||||||
|
BuildingInfo[] buildings,
|
||||||
|
uint[] priorStaticOwnerIds)
|
||||||
{
|
{
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
Build = build;
|
Build = build;
|
||||||
Origin = origin;
|
Origin = origin;
|
||||||
|
CurrentCellId = currentCellId;
|
||||||
|
Buildings = buildings;
|
||||||
|
PriorStaticOwnerIds = priorStaticOwnerIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object Owner { get; }
|
internal object Owner { get; }
|
||||||
internal LandblockBuild Build { get; }
|
internal LandblockBuild Build { get; }
|
||||||
|
internal uint CurrentCellId { get; }
|
||||||
|
internal BuildingInfo[] Buildings { get; }
|
||||||
|
internal uint[] PriorStaticOwnerIds { get; }
|
||||||
|
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
|
||||||
|
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
|
||||||
|
internal int PreparationCursor { get; set; }
|
||||||
|
internal bool PreparationCommitted { get; set; }
|
||||||
|
internal bool PriorCacheRemoved { get; set; }
|
||||||
|
internal TerrainSurface? TerrainSurface { get; set; }
|
||||||
|
internal List<CellSurface> CellSurfaces { get; } = new();
|
||||||
|
internal List<PortalPlane> PortalPlanes { get; } = new();
|
||||||
|
internal uint CellCursor { get; set; }
|
||||||
|
internal int BuildingCursor { get; set; }
|
||||||
|
internal bool BaseCommitted { get; set; }
|
||||||
|
internal int GfxCursor { get; set; }
|
||||||
|
internal int PriorStaticCursor { get; set; }
|
||||||
|
internal int StaticCursor { get; set; }
|
||||||
|
internal int BspOwnerCount { get; set; }
|
||||||
|
internal int CylinderOwnerCount { get; set; }
|
||||||
|
internal int NoCollisionCount { get; set; }
|
||||||
|
internal int SceneryTried { get; set; }
|
||||||
|
internal uint[]? RefloodOwnerIds { get; set; }
|
||||||
|
internal int RefloodCursor { get; set; }
|
||||||
|
internal bool RefloodCommitted { get; set; }
|
||||||
internal bool BeginCommitted { get; set; }
|
internal bool BeginCommitted { get; set; }
|
||||||
internal bool CompletionCommitted { get; set; }
|
internal bool CompletionCommitted { get; set; }
|
||||||
|
|
||||||
|
|
@ -142,6 +173,21 @@ public sealed class LandblockPhysicsPublisher
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LandblockPhysicsPublication PreparePublication(
|
public LandblockPhysicsPublication PreparePublication(
|
||||||
LandblockRenderPublication renderPublication)
|
LandblockRenderPublication renderPublication)
|
||||||
|
{
|
||||||
|
LandblockPhysicsPublication publication =
|
||||||
|
CreatePublication(renderPublication);
|
||||||
|
while (!AdvancePreparationOne(publication))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return publication;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures the immutable physics root without walking the entity suffix.
|
||||||
|
/// The concrete pipeline advances that walk under its frame meter.
|
||||||
|
/// </summary>
|
||||||
|
internal LandblockPhysicsPublication CreatePublication(
|
||||||
|
LandblockRenderPublication renderPublication)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(renderPublication);
|
ArgumentNullException.ThrowIfNull(renderPublication);
|
||||||
LandblockBuild build = renderPublication.Build;
|
LandblockBuild build = renderPublication.Build;
|
||||||
|
|
@ -149,10 +195,50 @@ public sealed class LandblockPhysicsPublisher
|
||||||
if (!IsFinite(origin))
|
if (!IsFinite(origin))
|
||||||
throw new ArgumentOutOfRangeException(nameof(renderPublication));
|
throw new ArgumentOutOfRangeException(nameof(renderPublication));
|
||||||
|
|
||||||
|
PhysicsDatBundle datBundle =
|
||||||
|
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
|
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||||
|
?? Array.Empty<BuildingInfo>();
|
||||||
return new LandblockPhysicsPublication(
|
return new LandblockPhysicsPublication(
|
||||||
_receiptOwner,
|
_receiptOwner,
|
||||||
build,
|
build,
|
||||||
origin);
|
origin,
|
||||||
|
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
|
||||||
|
buildings,
|
||||||
|
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||||
|
build.Landblock.LandblockId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances one entity's stable GfxObj-index preparation. No external
|
||||||
|
/// physics state is mutated until this cursor and the static-validation
|
||||||
|
/// cursor both complete.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvancePreparationOne(
|
||||||
|
LandblockPhysicsPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (publication.PreparationCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
IReadOnlyList<WorldEntity> entities =
|
||||||
|
publication.Build.Landblock.Entities;
|
||||||
|
if (publication.PreparationCursor < entities.Count)
|
||||||
|
{
|
||||||
|
WorldEntity entity = entities[publication.PreparationCursor];
|
||||||
|
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||||
|
{
|
||||||
|
uint id = entity.MeshRefs[i].GfxObjId;
|
||||||
|
if ((id & 0xFF000000u) == 0x01000000u)
|
||||||
|
publication.GfxObjectIdSet.Add(id);
|
||||||
|
}
|
||||||
|
publication.PreparationCursor++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -160,10 +246,26 @@ public sealed class LandblockPhysicsPublisher
|
||||||
/// retained receipt.
|
/// retained receipt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void BeginPublication(LandblockPhysicsPublication publication)
|
public void BeginPublication(LandblockPhysicsPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (!publication.PreparationCommitted)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Physics publication cannot begin before preparation commits.");
|
||||||
|
while (!AdvanceBeginOne(publication))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances one exact physics-prefix operation: prior-cache withdrawal,
|
||||||
|
/// terrain construction, one EnvCell, one building, or the final canonical
|
||||||
|
/// physics-engine replacement. All arrays and cursors live on the receipt.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceBeginOne(LandblockPhysicsPublication publication)
|
||||||
{
|
{
|
||||||
ValidateReceipt(publication);
|
ValidateReceipt(publication);
|
||||||
if (publication.BeginCommitted)
|
if (publication.BeginCommitted)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
long started = Stopwatch.GetTimestamp();
|
long started = Stopwatch.GetTimestamp();
|
||||||
LandblockBuild build = publication.Build;
|
LandblockBuild build = publication.Build;
|
||||||
|
|
@ -171,157 +273,76 @@ public sealed class LandblockPhysicsPublisher
|
||||||
LoadedLandblock landblock = build.Landblock;
|
LoadedLandblock landblock = build.Landblock;
|
||||||
PhysicsDatBundle datBundle =
|
PhysicsDatBundle datBundle =
|
||||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
uint currentCellId = _physicsDataCache.CellGraph.CurrCell?.Id ?? 0u;
|
|
||||||
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
|
||||||
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
|
||||||
|
|
||||||
// A publication is an exact replacement for this landblock prefix.
|
|
||||||
// CacheCellStruct/CacheBuilding intentionally use retail first-wins
|
|
||||||
// semantics within one pass, so retire the prior pass before adding
|
|
||||||
// the replacement. Adjacent prefixes remain untouched.
|
|
||||||
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
|
||||||
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
|
||||||
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
|
||||||
landblock.LandblockId);
|
|
||||||
|
|
||||||
// TerrainInfo.Type occupies bits 2-6. The low byte retains those bits
|
|
||||||
// plus Road (bits 0-1), which TerrainSurface masks independently.
|
|
||||||
var terrainBytes = new byte[81];
|
|
||||||
for (int i = 0; i < terrainBytes.Length; i++)
|
|
||||||
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
|
|
||||||
|
|
||||||
var terrainSurface = new TerrainSurface(
|
|
||||||
landblock.Heightmap.Height,
|
|
||||||
_heightTable,
|
|
||||||
landblockX,
|
|
||||||
landblockY,
|
|
||||||
terrainBytes);
|
|
||||||
var cellSurfaces = new List<CellSurface>();
|
|
||||||
var portalPlanes = new List<PortalPlane>();
|
|
||||||
|
|
||||||
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo = datBundle.Info;
|
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo = datBundle.Info;
|
||||||
if (landblockInfo is not null && landblockInfo.NumCells > 0)
|
|
||||||
|
if (!publication.PriorCacheRemoved)
|
||||||
{
|
{
|
||||||
uint firstCellId =
|
// CacheCellStruct/CacheBuilding use first-wins semantics within one
|
||||||
(landblock.LandblockId & 0xFFFF0000u) | 0x0100u;
|
// publication, so the replacement pass starts with one exact
|
||||||
for (uint offset = 0; offset < landblockInfo.NumCells; offset++)
|
// landblock-scoped withdrawal.
|
||||||
|
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
||||||
|
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
||||||
|
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
||||||
|
landblock.LandblockId);
|
||||||
|
publication.PriorCacheRemoved = true;
|
||||||
|
}
|
||||||
|
else if (publication.TerrainSurface is null)
|
||||||
|
{
|
||||||
|
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
||||||
|
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
||||||
|
var terrainBytes = new byte[81];
|
||||||
|
for (int i = 0; i < terrainBytes.Length; i++)
|
||||||
|
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
|
||||||
|
publication.TerrainSurface = new TerrainSurface(
|
||||||
|
landblock.Heightmap.Height,
|
||||||
|
_heightTable,
|
||||||
|
landblockX,
|
||||||
|
landblockY,
|
||||||
|
terrainBytes);
|
||||||
|
}
|
||||||
|
else if (landblockInfo is not null
|
||||||
|
&& publication.CellCursor < landblockInfo.NumCells)
|
||||||
|
{
|
||||||
|
PublishCell(publication, datBundle, publication.CellCursor);
|
||||||
|
publication.CellCursor++;
|
||||||
|
}
|
||||||
|
else if (publication.BuildingCursor < publication.Buildings.Length)
|
||||||
|
{
|
||||||
|
PublishBuilding(
|
||||||
|
landblock,
|
||||||
|
datBundle,
|
||||||
|
publication.TerrainSurface,
|
||||||
|
origin,
|
||||||
|
publication.Buildings[publication.BuildingCursor]);
|
||||||
|
publication.BuildingCursor++;
|
||||||
|
}
|
||||||
|
else if (!publication.BaseCommitted)
|
||||||
|
{
|
||||||
|
_physicsEngine.AddLandblock(
|
||||||
|
landblock.LandblockId,
|
||||||
|
publication.TerrainSurface,
|
||||||
|
publication.CellSurfaces,
|
||||||
|
publication.PortalPlanes,
|
||||||
|
origin.X,
|
||||||
|
origin.Y);
|
||||||
|
if ((publication.CurrentCellId & 0xFFFF0000u)
|
||||||
|
== (landblock.LandblockId & 0xFFFF0000u))
|
||||||
{
|
{
|
||||||
uint envCellId = firstCellId + offset;
|
_physicsEngine.UpdatePlayerCurrCell(publication.CurrentCellId);
|
||||||
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell))
|
|
||||||
continue;
|
|
||||||
if (envCell.EnvironmentId == 0)
|
|
||||||
continue;
|
|
||||||
if (!datBundle.Environments.TryGetValue(
|
|
||||||
0x0D000000u | envCell.EnvironmentId,
|
|
||||||
out var environment))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!environment.Cells.TryGetValue(
|
|
||||||
envCell.CellStructure,
|
|
||||||
out var cellStruct))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion rotation = envCell.Position.Orientation;
|
|
||||||
Vector3 cellOriginWorld = envCell.Position.Origin + origin;
|
|
||||||
Matrix4x4 physicsCellTransform =
|
|
||||||
Matrix4x4.CreateFromQuaternion(rotation)
|
|
||||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
|
||||||
|
|
||||||
_physicsDataCache.CacheCellStruct(
|
|
||||||
envCellId,
|
|
||||||
envCell,
|
|
||||||
cellStruct,
|
|
||||||
physicsCellTransform);
|
|
||||||
|
|
||||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
|
||||||
cellStruct.VertexArray.Vertices.Count);
|
|
||||||
foreach ((ushort vertexId, var vertex) in
|
|
||||||
cellStruct.VertexArray.Vertices)
|
|
||||||
{
|
|
||||||
Vector3 worldPosition =
|
|
||||||
Vector3.Transform(vertex.Origin, rotation)
|
|
||||||
+ cellOriginWorld;
|
|
||||||
worldVertices[(ushort)vertexId] = worldPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
var polygonVertexIds = new List<List<short>>(
|
|
||||||
cellStruct.PhysicsPolygons.Count);
|
|
||||||
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
|
||||||
{
|
|
||||||
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
|
||||||
foreach (short vertexId in polygon.VertexIds)
|
|
||||||
vertexIds.Add(vertexId);
|
|
||||||
polygonVertexIds.Add(vertexIds);
|
|
||||||
}
|
|
||||||
cellSurfaces.Add(new CellSurface(
|
|
||||||
envCellId,
|
|
||||||
worldVertices,
|
|
||||||
polygonVertexIds));
|
|
||||||
|
|
||||||
// CellPortal.PolygonId indexes rendering Polygons, not
|
|
||||||
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
|
||||||
foreach (var portal in envCell.CellPortals)
|
|
||||||
{
|
|
||||||
if (!cellStruct.Polygons.TryGetValue(
|
|
||||||
portal.PolygonId,
|
|
||||||
out var polygon)
|
|
||||||
|| polygon.VertexIds.Count < 3)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var portalVertices = new Vector3[polygon.VertexIds.Count];
|
|
||||||
bool allFound = true;
|
|
||||||
for (int index = 0; index < polygon.VertexIds.Count; index++)
|
|
||||||
{
|
|
||||||
if (!worldVertices.TryGetValue(
|
|
||||||
(ushort)polygon.VertexIds[index],
|
|
||||||
out portalVertices[index]))
|
|
||||||
{
|
|
||||||
allFound = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!allFound)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
portalPlanes.Add(PortalPlane.FromVertices(
|
|
||||||
portalVertices.AsSpan(),
|
|
||||||
portal.OtherCellId,
|
|
||||||
envCellId & 0xFFFFu,
|
|
||||||
(ushort)portal.Flags));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
publication.BaseCommitted = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
int buildingCount = PublishBuildings(
|
|
||||||
landblock,
|
|
||||||
datBundle,
|
|
||||||
landblockInfo,
|
|
||||||
terrainSurface,
|
|
||||||
origin);
|
|
||||||
_physicsEngine.AddLandblock(
|
|
||||||
landblock.LandblockId,
|
|
||||||
terrainSurface,
|
|
||||||
cellSurfaces,
|
|
||||||
portalPlanes,
|
|
||||||
origin.X,
|
|
||||||
origin.Y);
|
|
||||||
if ((currentCellId & 0xFFFF0000u)
|
|
||||||
== (landblock.LandblockId & 0xFFFF0000u))
|
|
||||||
{
|
{
|
||||||
_physicsEngine.UpdatePlayerCurrCell(currentCellId);
|
_cellSurfaceCount += publication.CellSurfaces.Count;
|
||||||
|
_portalPlaneCount += publication.PortalPlanes.Count;
|
||||||
|
_buildingCount += publication.Buildings.Length;
|
||||||
|
publication.BeginCommitted = true;
|
||||||
|
_beginCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cellSurfaceCount += cellSurfaces.Count;
|
|
||||||
_portalPlaneCount += portalPlanes.Count;
|
|
||||||
_buildingCount += buildingCount;
|
|
||||||
publication.BeginCommitted = true;
|
|
||||||
_beginCount++;
|
|
||||||
_basePublishTicks += Stopwatch.GetTimestamp() - started;
|
_basePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
|
return publication.BeginCommitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -338,209 +359,93 @@ public sealed class LandblockPhysicsPublisher
|
||||||
if (!publication.BeginCommitted)
|
if (!publication.BeginCommitted)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Physics publication cannot complete before its prefix commits.");
|
"Physics publication cannot complete before its prefix commits.");
|
||||||
if (publication.CompletionCommitted)
|
while (!AdvanceCompleteOne(publication, beforeStaticCollision))
|
||||||
return;
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
long completedStarted = Stopwatch.GetTimestamp();
|
/// <summary>
|
||||||
|
/// Advances one exact physics-suffix operation: one GfxObj cache entry,
|
||||||
|
/// prior static withdrawal, one static light/collision entity, or reflood.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceCompleteOne(
|
||||||
|
LandblockPhysicsPublication publication,
|
||||||
|
Action<WorldEntity>? beforeStaticCollision = null)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (!publication.BeginCommitted)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Physics publication cannot complete before its prefix commits.");
|
||||||
|
if (publication.CompletionCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
long started = Stopwatch.GetTimestamp();
|
||||||
LoadedLandblock landblock = publication.Build.Landblock;
|
LoadedLandblock landblock = publication.Build.Landblock;
|
||||||
PhysicsDatBundle datBundle =
|
PhysicsDatBundle datBundle =
|
||||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
|
|
||||||
long cacheStarted = Stopwatch.GetTimestamp();
|
if (publication.GfxCursor < publication.GfxObjectIds.Length)
|
||||||
foreach (WorldEntity entity in landblock.Entities)
|
|
||||||
{
|
{
|
||||||
foreach (MeshRef meshRef in entity.MeshRefs)
|
long cacheStarted = Stopwatch.GetTimestamp();
|
||||||
{
|
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||||
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u)
|
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||||
continue;
|
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
|
||||||
if (datBundle.GfxObjs.TryGetValue(meshRef.GfxObjId, out var gfx))
|
publication.GfxCursor++;
|
||||||
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
else if (publication.PriorStaticCursor
|
||||||
|
< publication.PriorStaticOwnerIds.Length)
|
||||||
// The immutable entity list is an exact Near snapshot. Retire every
|
|
||||||
// static owner created by the prior snapshot before registering this
|
|
||||||
// one; RegisterMultiPart can replace matching IDs but cannot discover
|
|
||||||
// owners omitted by a ForceReload/reapply. Logical-owner removal also
|
|
||||||
// clears footprints flooded across an adjacent landblock seam.
|
|
||||||
_physicsEngine.ShadowObjects.DeregisterStaticOwnersForLandblock(
|
|
||||||
landblock.LandblockId);
|
|
||||||
|
|
||||||
int landblockBspCount = 0;
|
|
||||||
int landblockCylinderCount = 0;
|
|
||||||
int landblockNoCollisionCount = 0;
|
|
||||||
int sceneryTried = 0;
|
|
||||||
foreach (WorldEntity entity in landblock.Entities)
|
|
||||||
{
|
{
|
||||||
|
_physicsEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
|
||||||
|
publication.PriorStaticOwnerIds[
|
||||||
|
publication.PriorStaticCursor],
|
||||||
|
landblock.LandblockId);
|
||||||
|
publication.PriorStaticCursor++;
|
||||||
|
}
|
||||||
|
else if (publication.StaticCursor < landblock.Entities.Count)
|
||||||
|
{
|
||||||
|
WorldEntity entity = landblock.Entities[publication.StaticCursor];
|
||||||
beforeStaticCollision?.Invoke(entity);
|
beforeStaticCollision?.Invoke(entity);
|
||||||
|
PublishStaticEntity(publication, entity);
|
||||||
// Retail building shells collide exclusively through the
|
publication.StaticCursor++;
|
||||||
// per-landcell building channel. They never become ordinary
|
|
||||||
// shadow objects, even when their source Setup has cylinders.
|
|
||||||
if (entity.IsBuildingShell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int entityBspCount = 0;
|
|
||||||
int entityCylinderCount = 0;
|
|
||||||
uint sourcePrefix =
|
|
||||||
entity.SourceGfxObjOrSetupId & 0xFF000000u;
|
|
||||||
bool isOutdoorMesh =
|
|
||||||
(entity.Id & 0x80000000u) != 0
|
|
||||||
|| (entity.Id < 0x40000000u
|
|
||||||
&& (sourcePrefix == 0x01000000u
|
|
||||||
|| sourcePrefix == 0x02000000u));
|
|
||||||
if (isOutdoorMesh)
|
|
||||||
sceneryTried++;
|
|
||||||
|
|
||||||
IReadOnlyList<ShadowShape> bspShapes =
|
|
||||||
ShadowShapeBuilder.FromLandblockBspParts(
|
|
||||||
entity.MeshRefs,
|
|
||||||
entity.IsBuildingShell,
|
|
||||||
_physicsDataCache.GetGfxObj);
|
|
||||||
entityBspCount = bspShapes.Count;
|
|
||||||
if (entityBspCount > 0)
|
|
||||||
{
|
|
||||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
|
||||||
entity.Id,
|
|
||||||
entity.Position,
|
|
||||||
entity.Rotation,
|
|
||||||
bspShapes,
|
|
||||||
0u,
|
|
||||||
EntityCollisionFlags.None,
|
|
||||||
publication.Origin.X,
|
|
||||||
publication.Origin.Y,
|
|
||||||
landblock.LandblockId,
|
|
||||||
seedCellId: entity.ParentCellId ?? 0u,
|
|
||||||
isStatic: true);
|
|
||||||
LogMultipartRegistration(landblock, entity, bspShapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetupPhysics? setup =
|
|
||||||
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
|
||||||
if (setup is not null && entityBspCount == 0)
|
|
||||||
{
|
|
||||||
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
|
||||||
var setupShapes = new List<ShadowShape>();
|
|
||||||
for (int cylinderIndex = 0;
|
|
||||||
cylinderIndex < setup.CylSpheres.Count;
|
|
||||||
cylinderIndex++)
|
|
||||||
{
|
|
||||||
DatReaderWriter.Types.CylSphere cylinder =
|
|
||||||
setup.CylSpheres[cylinderIndex];
|
|
||||||
float radius = cylinder.Radius * scale;
|
|
||||||
float baseHeight = cylinder.Height > 0f
|
|
||||||
? cylinder.Height
|
|
||||||
: cylinder.Radius * 4f;
|
|
||||||
float height = baseHeight * scale;
|
|
||||||
if (radius <= 0f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Vector3 localOffset = cylinder.Origin * scale;
|
|
||||||
setupShapes.Add(new ShadowShape(
|
|
||||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
||||||
LocalPosition: localOffset,
|
|
||||||
LocalRotation: Quaternion.Identity,
|
|
||||||
Scale: scale,
|
|
||||||
CollisionType: ShadowCollisionType.Cylinder,
|
|
||||||
Radius: radius,
|
|
||||||
CylHeight: height));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setup.CylSpheres.Count == 0)
|
|
||||||
{
|
|
||||||
for (int sphereIndex = 0;
|
|
||||||
sphereIndex < setup.Spheres.Count;
|
|
||||||
sphereIndex++)
|
|
||||||
{
|
|
||||||
DatReaderWriter.Types.Sphere sphere =
|
|
||||||
setup.Spheres[sphereIndex];
|
|
||||||
if (sphere.Radius <= 0f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float radius = sphere.Radius * scale;
|
|
||||||
Vector3 localOffset = sphere.Origin * scale;
|
|
||||||
Vector3 localBaseOffset = localOffset
|
|
||||||
+ Vector3.Transform(
|
|
||||||
-Vector3.UnitZ * radius,
|
|
||||||
Quaternion.Inverse(entity.Rotation));
|
|
||||||
setupShapes.Add(new ShadowShape(
|
|
||||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
||||||
LocalPosition: localBaseOffset,
|
|
||||||
LocalRotation: Quaternion.Identity,
|
|
||||||
Scale: scale,
|
|
||||||
CollisionType: ShadowCollisionType.Cylinder,
|
|
||||||
Radius: radius,
|
|
||||||
CylHeight: radius * 2f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setup.CylSpheres.Count == 0
|
|
||||||
&& setup.Spheres.Count == 0
|
|
||||||
&& setup.Radius > 0f)
|
|
||||||
{
|
|
||||||
float radius = setup.Radius * scale;
|
|
||||||
float height = (setup.Height > 0f
|
|
||||||
? setup.Height
|
|
||||||
: setup.Radius * 2f) * scale;
|
|
||||||
setupShapes.Add(new ShadowShape(
|
|
||||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
||||||
LocalPosition: Vector3.Zero,
|
|
||||||
LocalRotation: Quaternion.Identity,
|
|
||||||
Scale: scale,
|
|
||||||
CollisionType: ShadowCollisionType.Cylinder,
|
|
||||||
Radius: radius,
|
|
||||||
CylHeight: height));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setupShapes.Count > 0)
|
|
||||||
{
|
|
||||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
|
||||||
entity.Id,
|
|
||||||
entity.Position,
|
|
||||||
entity.Rotation,
|
|
||||||
setupShapes,
|
|
||||||
0u,
|
|
||||||
EntityCollisionFlags.None,
|
|
||||||
publication.Origin.X,
|
|
||||||
publication.Origin.Y,
|
|
||||||
landblock.LandblockId,
|
|
||||||
seedCellId: entity.ParentCellId ?? 0u,
|
|
||||||
isStatic: true);
|
|
||||||
LogSetupRegistration(landblock, entity, setupShapes);
|
|
||||||
entityCylinderCount = setupShapes.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityBspCount > 0)
|
|
||||||
landblockBspCount++;
|
|
||||||
if (entityCylinderCount > 0)
|
|
||||||
landblockCylinderCount++;
|
|
||||||
if (entityBspCount == 0 && entityCylinderCount == 0
|
|
||||||
&& (sourcePrefix == 0x01000000u
|
|
||||||
|| sourcePrefix == 0x02000000u))
|
|
||||||
{
|
|
||||||
landblockNoCollisionCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if (publication.RefloodOwnerIds is null)
|
||||||
if (PhysicsDiagnostics.ProbeBuildingEnabled && sceneryTried > 0)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
publication.RefloodOwnerIds =
|
||||||
$"lb 0x{landblock.LandblockId:X8}: scenery tried={sceneryTried} " +
|
_physicsEngine.ShadowObjects
|
||||||
$"(outdoorNone={landblockNoCollisionCount})");
|
.CaptureRefloodOwnersForLandblock(landblock.LandblockId);
|
||||||
|
}
|
||||||
|
else if (publication.RefloodCursor
|
||||||
|
< publication.RefloodOwnerIds.Length)
|
||||||
|
{
|
||||||
|
_physicsEngine.ShadowObjects.RefloodOwnerForLandblock(
|
||||||
|
publication.RefloodOwnerIds[publication.RefloodCursor],
|
||||||
|
landblock.LandblockId);
|
||||||
|
publication.RefloodCursor++;
|
||||||
|
}
|
||||||
|
else if (!publication.RefloodCommitted)
|
||||||
|
{
|
||||||
|
if (PhysicsDiagnostics.ProbeBuildingEnabled
|
||||||
|
&& publication.SceneryTried > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
|
||||||
|
$"(outdoorNone={publication.NoCollisionCount})");
|
||||||
|
}
|
||||||
|
LogMissingSceneryBounds(landblock);
|
||||||
|
_refloodCount++;
|
||||||
|
publication.RefloodCommitted = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_staticBspOwnerCount += publication.BspOwnerCount;
|
||||||
|
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
|
||||||
|
publication.CompletionCommitted = true;
|
||||||
|
_completeCount++;
|
||||||
}
|
}
|
||||||
LogMissingSceneryBounds(landblock);
|
|
||||||
|
|
||||||
_physicsEngine.ShadowObjects.RefloodLandblock(landblock.LandblockId);
|
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
_refloodCount++;
|
return publication.CompletionCommitted;
|
||||||
|
|
||||||
_staticBspOwnerCount += landblockBspCount;
|
|
||||||
_staticCylinderOwnerCount += landblockCylinderCount;
|
|
||||||
publication.CompletionCommitted = true;
|
|
||||||
_completeCount++;
|
|
||||||
_completePublishTicks +=
|
|
||||||
Stopwatch.GetTimestamp() - completedStarted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DemoteToTerrain(uint landblockId)
|
public void DemoteToTerrain(uint landblockId)
|
||||||
|
|
@ -555,54 +460,292 @@ public sealed class LandblockPhysicsPublisher
|
||||||
_fullRemovalCount++;
|
_fullRemovalCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int PublishBuildings(
|
private void PublishCell(
|
||||||
|
LandblockPhysicsPublication publication,
|
||||||
|
PhysicsDatBundle datBundle,
|
||||||
|
uint offset)
|
||||||
|
{
|
||||||
|
LoadedLandblock landblock = publication.Build.Landblock;
|
||||||
|
uint envCellId =
|
||||||
|
(landblock.LandblockId & 0xFFFF0000u) | (0x0100u + offset);
|
||||||
|
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell)
|
||||||
|
|| envCell.EnvironmentId == 0
|
||||||
|
|| !datBundle.Environments.TryGetValue(
|
||||||
|
0x0D000000u | envCell.EnvironmentId,
|
||||||
|
out var environment)
|
||||||
|
|| !environment.Cells.TryGetValue(
|
||||||
|
envCell.CellStructure,
|
||||||
|
out var cellStruct))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quaternion rotation = envCell.Position.Orientation;
|
||||||
|
Vector3 cellOriginWorld = envCell.Position.Origin + publication.Origin;
|
||||||
|
Matrix4x4 physicsCellTransform =
|
||||||
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
|
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||||
|
|
||||||
|
_physicsDataCache.CacheCellStruct(
|
||||||
|
envCellId,
|
||||||
|
envCell,
|
||||||
|
cellStruct,
|
||||||
|
physicsCellTransform);
|
||||||
|
|
||||||
|
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||||
|
cellStruct.VertexArray.Vertices.Count);
|
||||||
|
foreach ((ushort vertexId, var vertex) in
|
||||||
|
cellStruct.VertexArray.Vertices)
|
||||||
|
{
|
||||||
|
Vector3 worldPosition =
|
||||||
|
Vector3.Transform(vertex.Origin, rotation)
|
||||||
|
+ cellOriginWorld;
|
||||||
|
worldVertices[(ushort)vertexId] = worldPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
var polygonVertexIds = new List<List<short>>(
|
||||||
|
cellStruct.PhysicsPolygons.Count);
|
||||||
|
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
||||||
|
{
|
||||||
|
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
||||||
|
foreach (short vertexId in polygon.VertexIds)
|
||||||
|
vertexIds.Add(vertexId);
|
||||||
|
polygonVertexIds.Add(vertexIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
var portalPlanes = new List<PortalPlane>();
|
||||||
|
// CellPortal.PolygonId indexes rendering Polygons, not
|
||||||
|
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
||||||
|
foreach (var portal in envCell.CellPortals)
|
||||||
|
{
|
||||||
|
if (!cellStruct.Polygons.TryGetValue(
|
||||||
|
portal.PolygonId,
|
||||||
|
out var polygon)
|
||||||
|
|| polygon.VertexIds.Count < 3)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var portalVertices = new Vector3[polygon.VertexIds.Count];
|
||||||
|
bool allFound = true;
|
||||||
|
for (int index = 0; index < polygon.VertexIds.Count; index++)
|
||||||
|
{
|
||||||
|
if (!worldVertices.TryGetValue(
|
||||||
|
(ushort)polygon.VertexIds[index],
|
||||||
|
out portalVertices[index]))
|
||||||
|
{
|
||||||
|
allFound = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!allFound)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
portalPlanes.Add(PortalPlane.FromVertices(
|
||||||
|
portalVertices.AsSpan(),
|
||||||
|
portal.OtherCellId,
|
||||||
|
envCellId & 0xFFFFu,
|
||||||
|
(ushort)portal.Flags));
|
||||||
|
}
|
||||||
|
|
||||||
|
publication.CellSurfaces.Add(new CellSurface(
|
||||||
|
envCellId,
|
||||||
|
worldVertices,
|
||||||
|
polygonVertexIds));
|
||||||
|
publication.PortalPlanes.AddRange(portalPlanes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishBuilding(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
PhysicsDatBundle datBundle,
|
PhysicsDatBundle datBundle,
|
||||||
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo,
|
|
||||||
TerrainSurface terrainSurface,
|
TerrainSurface terrainSurface,
|
||||||
Vector3 origin)
|
Vector3 origin,
|
||||||
|
BuildingInfo building)
|
||||||
{
|
{
|
||||||
if (landblockInfo is null || landblockInfo.Buildings.Count == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
uint landblockPrefix = landblock.LandblockId & 0xFFFF0000u;
|
uint landblockPrefix = landblock.LandblockId & 0xFFFF0000u;
|
||||||
foreach (var building in landblockInfo.Buildings)
|
var portals = new List<BldPortalInfo>(building.Portals.Count);
|
||||||
|
foreach (var portal in building.Portals)
|
||||||
{
|
{
|
||||||
var portals = new List<BldPortalInfo>(building.Portals.Count);
|
portals.Add(new BldPortalInfo(
|
||||||
foreach (var portal in building.Portals)
|
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
|
||||||
{
|
otherPortalId: unchecked((short)portal.OtherPortalId),
|
||||||
portals.Add(new BldPortalInfo(
|
flags: (ushort)portal.Flags));
|
||||||
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
|
}
|
||||||
otherPortalId: unchecked((short)portal.OtherPortalId),
|
|
||||||
flags: (ushort)portal.Flags));
|
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
||||||
}
|
Matrix4x4 buildingTransform =
|
||||||
|
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
|
||||||
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
* Matrix4x4.CreateTranslation(buildingOrigin);
|
||||||
Matrix4x4 buildingTransform =
|
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
|
||||||
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
|
building.Frame.Origin.X,
|
||||||
* Matrix4x4.CreateTranslation(buildingOrigin);
|
building.Frame.Origin.Y);
|
||||||
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
|
uint landcellId = landblockPrefix | landcellLow;
|
||||||
building.Frame.Origin.X,
|
|
||||||
building.Frame.Origin.Y);
|
uint shellPartZero = building.ModelId;
|
||||||
uint landcellId = landblockPrefix | landcellLow;
|
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
||||||
|
{
|
||||||
uint shellPartZero = building.ModelId;
|
datBundle.Setups.TryGetValue(
|
||||||
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
building.ModelId,
|
||||||
{
|
out var setup);
|
||||||
datBundle.Setups.TryGetValue(
|
shellPartZero = setup is not null && setup.Parts.Count > 0
|
||||||
building.ModelId,
|
? setup.Parts[0]
|
||||||
out var setup);
|
: 0u;
|
||||||
shellPartZero = setup is not null && setup.Parts.Count > 0
|
}
|
||||||
? setup.Parts[0]
|
_physicsDataCache.CacheBuilding(
|
||||||
: 0u;
|
landcellId,
|
||||||
}
|
portals,
|
||||||
_physicsDataCache.CacheBuilding(
|
buildingTransform,
|
||||||
landcellId,
|
modelId: shellPartZero);
|
||||||
portals,
|
}
|
||||||
buildingTransform,
|
|
||||||
modelId: shellPartZero);
|
private void PublishStaticEntity(
|
||||||
|
LandblockPhysicsPublication publication,
|
||||||
|
WorldEntity entity)
|
||||||
|
{
|
||||||
|
LoadedLandblock landblock = publication.Build.Landblock;
|
||||||
|
// Retail building shells collide exclusively through the per-landcell
|
||||||
|
// building channel. They never become ordinary shadow objects.
|
||||||
|
if (entity.IsBuildingShell)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int entityBspCount = 0;
|
||||||
|
int entityCylinderCount = 0;
|
||||||
|
uint sourcePrefix = entity.SourceGfxObjOrSetupId & 0xFF000000u;
|
||||||
|
bool isOutdoorMesh =
|
||||||
|
(entity.Id & 0x80000000u) != 0
|
||||||
|
|| (entity.Id < 0x40000000u
|
||||||
|
&& (sourcePrefix == 0x01000000u
|
||||||
|
|| sourcePrefix == 0x02000000u));
|
||||||
|
if (isOutdoorMesh)
|
||||||
|
publication.SceneryTried++;
|
||||||
|
|
||||||
|
IReadOnlyList<ShadowShape> bspShapes =
|
||||||
|
ShadowShapeBuilder.FromLandblockBspParts(
|
||||||
|
entity.MeshRefs,
|
||||||
|
entity.IsBuildingShell,
|
||||||
|
_physicsDataCache.GetGfxObj);
|
||||||
|
entityBspCount = bspShapes.Count;
|
||||||
|
if (entityBspCount > 0)
|
||||||
|
{
|
||||||
|
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||||
|
entity.Id,
|
||||||
|
entity.Position,
|
||||||
|
entity.Rotation,
|
||||||
|
bspShapes,
|
||||||
|
0u,
|
||||||
|
EntityCollisionFlags.None,
|
||||||
|
publication.Origin.X,
|
||||||
|
publication.Origin.Y,
|
||||||
|
landblock.LandblockId,
|
||||||
|
seedCellId: entity.ParentCellId ?? 0u,
|
||||||
|
isStatic: true);
|
||||||
|
LogMultipartRegistration(landblock, entity, bspShapes);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupPhysics? setup =
|
||||||
|
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||||
|
if (setup is not null && entityBspCount == 0)
|
||||||
|
{
|
||||||
|
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
||||||
|
var setupShapes = new List<ShadowShape>();
|
||||||
|
for (int cylinderIndex = 0;
|
||||||
|
cylinderIndex < setup.CylSpheres.Count;
|
||||||
|
cylinderIndex++)
|
||||||
|
{
|
||||||
|
CylSphere cylinder = setup.CylSpheres[cylinderIndex];
|
||||||
|
float radius = cylinder.Radius * scale;
|
||||||
|
float baseHeight = cylinder.Height > 0f
|
||||||
|
? cylinder.Height
|
||||||
|
: cylinder.Radius * 4f;
|
||||||
|
float height = baseHeight * scale;
|
||||||
|
if (radius <= 0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Vector3 localOffset = cylinder.Origin * scale;
|
||||||
|
setupShapes.Add(new ShadowShape(
|
||||||
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||||
|
LocalPosition: localOffset,
|
||||||
|
LocalRotation: Quaternion.Identity,
|
||||||
|
Scale: scale,
|
||||||
|
CollisionType: ShadowCollisionType.Cylinder,
|
||||||
|
Radius: radius,
|
||||||
|
CylHeight: height));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setup.CylSpheres.Count == 0)
|
||||||
|
{
|
||||||
|
for (int sphereIndex = 0;
|
||||||
|
sphereIndex < setup.Spheres.Count;
|
||||||
|
sphereIndex++)
|
||||||
|
{
|
||||||
|
Sphere sphere = setup.Spheres[sphereIndex];
|
||||||
|
if (sphere.Radius <= 0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float radius = sphere.Radius * scale;
|
||||||
|
Vector3 localOffset = sphere.Origin * scale;
|
||||||
|
Vector3 localBaseOffset = localOffset
|
||||||
|
+ Vector3.Transform(
|
||||||
|
-Vector3.UnitZ * radius,
|
||||||
|
Quaternion.Inverse(entity.Rotation));
|
||||||
|
setupShapes.Add(new ShadowShape(
|
||||||
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||||
|
LocalPosition: localBaseOffset,
|
||||||
|
LocalRotation: Quaternion.Identity,
|
||||||
|
Scale: scale,
|
||||||
|
CollisionType: ShadowCollisionType.Cylinder,
|
||||||
|
Radius: radius,
|
||||||
|
CylHeight: radius * 2f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setup.CylSpheres.Count == 0
|
||||||
|
&& setup.Spheres.Count == 0
|
||||||
|
&& setup.Radius > 0f)
|
||||||
|
{
|
||||||
|
float radius = setup.Radius * scale;
|
||||||
|
float height = (setup.Height > 0f
|
||||||
|
? setup.Height
|
||||||
|
: setup.Radius * 2f) * scale;
|
||||||
|
setupShapes.Add(new ShadowShape(
|
||||||
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||||
|
LocalPosition: Vector3.Zero,
|
||||||
|
LocalRotation: Quaternion.Identity,
|
||||||
|
Scale: scale,
|
||||||
|
CollisionType: ShadowCollisionType.Cylinder,
|
||||||
|
Radius: radius,
|
||||||
|
CylHeight: height));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupShapes.Count > 0)
|
||||||
|
{
|
||||||
|
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||||
|
entity.Id,
|
||||||
|
entity.Position,
|
||||||
|
entity.Rotation,
|
||||||
|
setupShapes,
|
||||||
|
0u,
|
||||||
|
EntityCollisionFlags.None,
|
||||||
|
publication.Origin.X,
|
||||||
|
publication.Origin.Y,
|
||||||
|
landblock.LandblockId,
|
||||||
|
seedCellId: entity.ParentCellId ?? 0u,
|
||||||
|
isStatic: true);
|
||||||
|
LogSetupRegistration(landblock, entity, setupShapes);
|
||||||
|
entityCylinderCount = setupShapes.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityBspCount > 0)
|
||||||
|
publication.BspOwnerCount++;
|
||||||
|
if (entityCylinderCount > 0)
|
||||||
|
publication.CylinderOwnerCount++;
|
||||||
|
if (entityBspCount == 0 && entityCylinderCount == 0
|
||||||
|
&& (sourcePrefix == 0x01000000u
|
||||||
|
|| sourcePrefix == 0x02000000u))
|
||||||
|
{
|
||||||
|
publication.NoCollisionCount++;
|
||||||
}
|
}
|
||||||
return landblockInfo.Buildings.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogSetupRegistration(
|
private static void LogSetupRegistration(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ public readonly record struct LandblockPresentationDiagnostics(
|
||||||
LandblockPhysicsPublisherDiagnostics Physics,
|
LandblockPhysicsPublisherDiagnostics Physics,
|
||||||
LandblockStaticPresentationDiagnostics Statics);
|
LandblockStaticPresentationDiagnostics Statics);
|
||||||
|
|
||||||
|
internal readonly record struct LandblockPublicationAdvance(
|
||||||
|
bool Completed,
|
||||||
|
bool Progressed);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Render-thread transaction coordinator for one accepted landblock streaming
|
/// Render-thread transaction coordinator for one accepted landblock streaming
|
||||||
/// result. Streaming policy (desired tier, generation, priority, and apply
|
/// result. Streaming policy (desired tier, generation, priority, and apply
|
||||||
|
|
@ -48,6 +52,7 @@ public sealed class LandblockPresentationPipeline
|
||||||
public required LandblockBuild Build;
|
public required LandblockBuild Build;
|
||||||
public required LandblockMeshData MeshData;
|
public required LandblockMeshData MeshData;
|
||||||
public required uint LandblockId;
|
public required uint LandblockId;
|
||||||
|
public required LandblockStreamCostEstimate Cost;
|
||||||
public LandblockStreamTier Tier;
|
public LandblockStreamTier Tier;
|
||||||
public bool PresentationCommitted;
|
public bool PresentationCommitted;
|
||||||
public LandblockRenderPublication? RenderPublication;
|
public LandblockRenderPublication? RenderPublication;
|
||||||
|
|
@ -186,7 +191,19 @@ public sealed class LandblockPresentationPipeline
|
||||||
ArgumentNullException.ThrowIfNull(result);
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
|
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
|
||||||
return;
|
return;
|
||||||
Advance(result, transaction);
|
Advance(result, transaction, meter: null, ensureProgress: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal LandblockPublicationAdvance ResumePublication(
|
||||||
|
LandblockStreamResult result,
|
||||||
|
StreamingWorkMeter meter,
|
||||||
|
bool ensureProgress)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
ArgumentNullException.ThrowIfNull(meter);
|
||||||
|
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
|
||||||
|
return new LandblockPublicationAdvance(Completed: true, Progressed: false);
|
||||||
|
return Advance(result, transaction, meter, ensureProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdvanceRetirements() => _retirements.Advance();
|
public void AdvanceRetirements() => _retirements.Advance();
|
||||||
|
|
@ -223,8 +240,28 @@ public sealed class LandblockPresentationPipeline
|
||||||
loaded.Build,
|
loaded.Build,
|
||||||
loaded.MeshData,
|
loaded.MeshData,
|
||||||
loaded.LandblockId,
|
loaded.LandblockId,
|
||||||
loaded.Tier);
|
loaded.Tier,
|
||||||
Advance(loaded, transaction);
|
estimate: null);
|
||||||
|
Advance(loaded, transaction, meter: null, ensureProgress: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal LandblockPublicationAdvance PublishLoaded(
|
||||||
|
LandblockStreamResult.Loaded loaded,
|
||||||
|
LandblockStreamCostEstimate estimate,
|
||||||
|
StreamingWorkMeter meter,
|
||||||
|
bool ensureProgress)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(loaded);
|
||||||
|
ArgumentNullException.ThrowIfNull(meter);
|
||||||
|
PublicationTransaction transaction = GetOrCreate(
|
||||||
|
loaded,
|
||||||
|
PublicationKind.Loaded,
|
||||||
|
loaded.Build,
|
||||||
|
loaded.MeshData,
|
||||||
|
loaded.LandblockId,
|
||||||
|
loaded.Tier,
|
||||||
|
estimate);
|
||||||
|
return Advance(loaded, transaction, meter, ensureProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PublishPromoted(
|
public void PublishPromoted(
|
||||||
|
|
@ -241,8 +278,32 @@ public sealed class LandblockPresentationPipeline
|
||||||
promoted.Build,
|
promoted.Build,
|
||||||
promoted.MeshData,
|
promoted.MeshData,
|
||||||
promoted.LandblockId,
|
promoted.LandblockId,
|
||||||
LandblockStreamTier.Near);
|
LandblockStreamTier.Near,
|
||||||
Advance(promoted, transaction);
|
estimate: null);
|
||||||
|
Advance(promoted, transaction, meter: null, ensureProgress: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal LandblockPublicationAdvance PublishPromoted(
|
||||||
|
LandblockStreamResult.Promoted promoted,
|
||||||
|
bool mergeIntoExistingLandblock,
|
||||||
|
LandblockStreamCostEstimate estimate,
|
||||||
|
StreamingWorkMeter meter,
|
||||||
|
bool ensureProgress)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(promoted);
|
||||||
|
ArgumentNullException.ThrowIfNull(meter);
|
||||||
|
PublicationKind kind = mergeIntoExistingLandblock
|
||||||
|
? PublicationKind.PromoteExisting
|
||||||
|
: PublicationKind.PromoteSelfContained;
|
||||||
|
PublicationTransaction transaction = GetOrCreate(
|
||||||
|
promoted,
|
||||||
|
kind,
|
||||||
|
promoted.Build,
|
||||||
|
promoted.MeshData,
|
||||||
|
promoted.LandblockId,
|
||||||
|
LandblockStreamTier.Near,
|
||||||
|
estimate);
|
||||||
|
return Advance(promoted, transaction, meter, ensureProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PublishAsFar(
|
public void PublishAsFar(
|
||||||
|
|
@ -270,6 +331,11 @@ public sealed class LandblockPresentationPipeline
|
||||||
Origin: completedBuild.Origin),
|
Origin: completedBuild.Origin),
|
||||||
MeshData = meshData,
|
MeshData = meshData,
|
||||||
LandblockId = farLandblock.LandblockId,
|
LandblockId = farLandblock.LandblockId,
|
||||||
|
Cost = LandblockStreamResultCost.Estimate(
|
||||||
|
new LandblockBuild(
|
||||||
|
farLandblock,
|
||||||
|
Origin: completedBuild.Origin),
|
||||||
|
meshData),
|
||||||
Tier = LandblockStreamTier.Far,
|
Tier = LandblockStreamTier.Far,
|
||||||
};
|
};
|
||||||
_publications.Add(acceptedResult, transaction);
|
_publications.Add(acceptedResult, transaction);
|
||||||
|
|
@ -280,7 +346,56 @@ public sealed class LandblockPresentationPipeline
|
||||||
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
|
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Advance(acceptedResult, transaction);
|
Advance(
|
||||||
|
acceptedResult,
|
||||||
|
transaction,
|
||||||
|
meter: null,
|
||||||
|
ensureProgress: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal LandblockPublicationAdvance PublishAsFar(
|
||||||
|
LandblockStreamResult acceptedResult,
|
||||||
|
LandblockBuild completedBuild,
|
||||||
|
LandblockMeshData meshData,
|
||||||
|
StreamingWorkMeter meter,
|
||||||
|
bool ensureProgress)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(acceptedResult);
|
||||||
|
ArgumentNullException.ThrowIfNull(completedBuild);
|
||||||
|
ArgumentNullException.ThrowIfNull(meshData);
|
||||||
|
ArgumentNullException.ThrowIfNull(meter);
|
||||||
|
|
||||||
|
if (!_publications.TryGetValue(
|
||||||
|
acceptedResult,
|
||||||
|
out PublicationTransaction? transaction))
|
||||||
|
{
|
||||||
|
LoadedLandblock completed = completedBuild.Landblock;
|
||||||
|
var farLandblock = new LoadedLandblock(
|
||||||
|
completed.LandblockId,
|
||||||
|
completed.Heightmap,
|
||||||
|
Array.Empty<WorldEntity>(),
|
||||||
|
PhysicsDatBundle.Empty);
|
||||||
|
LandblockBuild farBuild = new(
|
||||||
|
farLandblock,
|
||||||
|
Origin: completedBuild.Origin);
|
||||||
|
transaction = new PublicationTransaction
|
||||||
|
{
|
||||||
|
Kind = PublicationKind.Far,
|
||||||
|
Build = farBuild,
|
||||||
|
MeshData = meshData,
|
||||||
|
LandblockId = farLandblock.LandblockId,
|
||||||
|
Cost = LandblockStreamResultCost.Estimate(farBuild, meshData),
|
||||||
|
Tier = LandblockStreamTier.Far,
|
||||||
|
};
|
||||||
|
_publications.Add(acceptedResult, transaction);
|
||||||
|
}
|
||||||
|
else if (transaction.Kind != PublicationKind.Far)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Advance(acceptedResult, transaction, meter, ensureProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PublicationTransaction GetOrCreate(
|
private PublicationTransaction GetOrCreate(
|
||||||
|
|
@ -289,7 +404,8 @@ public sealed class LandblockPresentationPipeline
|
||||||
LandblockBuild build,
|
LandblockBuild build,
|
||||||
LandblockMeshData meshData,
|
LandblockMeshData meshData,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
LandblockStreamTier tier)
|
LandblockStreamTier tier,
|
||||||
|
LandblockStreamCostEstimate? estimate)
|
||||||
{
|
{
|
||||||
if (_publications.TryGetValue(result, out PublicationTransaction? existing))
|
if (_publications.TryGetValue(result, out PublicationTransaction? existing))
|
||||||
{
|
{
|
||||||
|
|
@ -311,71 +427,317 @@ public sealed class LandblockPresentationPipeline
|
||||||
Build = build,
|
Build = build,
|
||||||
MeshData = meshData,
|
MeshData = meshData,
|
||||||
LandblockId = landblockId,
|
LandblockId = landblockId,
|
||||||
|
Cost = estimate
|
||||||
|
?? LandblockStreamResultCost.Estimate(build, meshData),
|
||||||
Tier = tier,
|
Tier = tier,
|
||||||
};
|
};
|
||||||
_publications.Add(result, created);
|
_publications.Add(result, created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Advance(
|
private LandblockPublicationAdvance Advance(
|
||||||
LandblockStreamResult result,
|
LandblockStreamResult result,
|
||||||
PublicationTransaction transaction)
|
PublicationTransaction transaction,
|
||||||
|
StreamingWorkMeter? meter,
|
||||||
|
bool ensureProgress)
|
||||||
{
|
{
|
||||||
|
bool progressed = false;
|
||||||
|
bool TryRun(
|
||||||
|
StreamingWorkCost cost,
|
||||||
|
string stage,
|
||||||
|
Action operation)
|
||||||
|
{
|
||||||
|
if (meter is null)
|
||||||
|
{
|
||||||
|
operation();
|
||||||
|
progressed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamingWorkAdmission admission = meter.TryReserve(
|
||||||
|
cost,
|
||||||
|
stage,
|
||||||
|
ensureProgress && !progressed);
|
||||||
|
if (admission == StreamingWorkAdmission.Yielded)
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
operation();
|
||||||
|
meter.Complete();
|
||||||
|
progressed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
meter.Fail();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!transaction.PresentationCommitted)
|
if (!transaction.PresentationCommitted)
|
||||||
{
|
{
|
||||||
if (_renderPublisher is not null
|
if (_renderPublisher is not null
|
||||||
&& _physicsPublisher is not null
|
&& _physicsPublisher is not null
|
||||||
&& _staticPublisher is not null)
|
&& _staticPublisher is not null)
|
||||||
{
|
{
|
||||||
transaction.RenderPublication ??=
|
while (transaction.RenderPublication is null)
|
||||||
_renderPublisher.PreparePublication(
|
{
|
||||||
transaction.Build,
|
if (!TryRun(
|
||||||
transaction.MeshData);
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
transaction.PhysicsPublication ??=
|
"publication-prepare-render",
|
||||||
_physicsPublisher.PreparePublication(
|
() => transaction.RenderPublication =
|
||||||
transaction.RenderPublication);
|
_renderPublisher.PreparePublication(
|
||||||
transaction.StaticPublication ??=
|
transaction.Build,
|
||||||
_staticPublisher.PreparePublication(
|
transaction.MeshData)))
|
||||||
transaction.PhysicsPublication);
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (transaction.PhysicsPublication is null)
|
||||||
|
{
|
||||||
|
if (!TryRun(
|
||||||
|
default,
|
||||||
|
"publication-prepare-physics",
|
||||||
|
() => transaction.PhysicsPublication =
|
||||||
|
_physicsPublisher.CreatePublication(
|
||||||
|
transaction.RenderPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!transaction.PhysicsPublication.PreparationCommitted)
|
||||||
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.PhysicsPublication.PreparationCursor
|
||||||
|
< transaction.Build.Landblock.Entities.Count
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-index-physics",
|
||||||
|
() => _physicsPublisher.AdvancePreparationOne(
|
||||||
|
transaction.PhysicsPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (transaction.StaticPublication is null)
|
||||||
|
{
|
||||||
|
if (!TryRun(
|
||||||
|
default,
|
||||||
|
"publication-prepare-statics",
|
||||||
|
() => transaction.StaticPublication =
|
||||||
|
_staticPublisher.CreatePublication(
|
||||||
|
transaction.PhysicsPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!transaction.StaticPublication.PreparationCommitted)
|
||||||
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.StaticPublication.PreparationCursor
|
||||||
|
< transaction.Build.Landblock.Entities.Count
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-validate-statics",
|
||||||
|
() => _staticPublisher.AdvancePreparationOne(
|
||||||
|
transaction.StaticPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare every immutable receipt before the first external
|
// Prepare every immutable receipt before the first external
|
||||||
// mutation. Cross-owner identity/data errors therefore block
|
// mutation. Cross-owner identity/data errors therefore block
|
||||||
// the publication without leaving a committed prefix.
|
// the publication without leaving a committed prefix.
|
||||||
_renderPublisher.BeginPublication(transaction.RenderPublication);
|
while (!transaction.RenderPublication.BeginCommitted)
|
||||||
_physicsPublisher.BeginPublication(transaction.PhysicsPublication);
|
{
|
||||||
|
StreamingWorkCost cost =
|
||||||
|
!transaction.RenderPublication.TerrainCommitted
|
||||||
|
? new StreamingWorkCost(
|
||||||
|
GpuUploadBytes:
|
||||||
|
transaction.Cost.TerrainPayloadBytes)
|
||||||
|
: !transaction.RenderPublication.VisibilityCommitted
|
||||||
|
? new StreamingWorkCost(
|
||||||
|
EntityOperations:
|
||||||
|
transaction.Cost.VisibilityCells)
|
||||||
|
: default;
|
||||||
|
if (!TryRun(
|
||||||
|
cost,
|
||||||
|
"publication-render-prefix",
|
||||||
|
() => _renderPublisher.AdvanceBeginOne(
|
||||||
|
transaction.RenderPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!transaction.PhysicsPublication.BeginCommitted)
|
||||||
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.PhysicsPublication.TerrainSurface is not null
|
||||||
|
&& transaction.PhysicsPublication.Build.Landblock.PhysicsDats?
|
||||||
|
.Info is { } info
|
||||||
|
&& transaction.PhysicsPublication.CellCursor < info.NumCells
|
||||||
|
? 1
|
||||||
|
: transaction.PhysicsPublication.BuildingCursor
|
||||||
|
< transaction.PhysicsPublication.Buildings.Length
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-physics-prefix",
|
||||||
|
() => _physicsPublisher.AdvanceBeginOne(
|
||||||
|
transaction.PhysicsPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Retail establishes the building/visible-cell suffix before
|
// Retail establishes the building/visible-cell suffix before
|
||||||
// each static object publishes lights, collision, and scripts.
|
// each static object publishes lights, collision, and scripts.
|
||||||
_renderPublisher.CompletePublication(
|
while (!transaction.RenderPublication.CompletionCommitted)
|
||||||
transaction.RenderPublication);
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.RenderPublication.BuildingPublication is
|
||||||
|
{ PreparationCommitted: false } buildingPublication
|
||||||
|
&& buildingPublication.BuildingCursor
|
||||||
|
< buildingPublication.Buildings.Length
|
||||||
|
? 1
|
||||||
|
: transaction.RenderPublication.EnvCellPublication is
|
||||||
|
{ PreparationCommitted: false } envCellPublication
|
||||||
|
&& envCellPublication.ShellCursor
|
||||||
|
< envCellPublication.Build.Shells.Length
|
||||||
|
? 1
|
||||||
|
: !transaction.RenderPublication.EnvCellsCommitted
|
||||||
|
&& transaction.RenderPublication
|
||||||
|
.EnvCellPublication is null
|
||||||
|
? transaction.Cost.EnvCellShells
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-render-suffix",
|
||||||
|
() => _renderPublisher.AdvanceCompleteOne(
|
||||||
|
transaction.RenderPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_staticPublisher.BeginPublication(transaction.StaticPublication);
|
while (!transaction.StaticPublication.BeginCommitted)
|
||||||
_physicsPublisher.CompletePublication(
|
{
|
||||||
transaction.PhysicsPublication,
|
int entityOperations =
|
||||||
entity => _staticPublisher.PrepareEntityBeforeCollision(
|
transaction.StaticPublication.PriorCleanupCursor
|
||||||
transaction.StaticPublication,
|
< transaction.StaticPublication
|
||||||
entity));
|
.OrderedPreviouslyActiveIds.Count
|
||||||
_staticPublisher.CompletePublication(
|
? 1
|
||||||
transaction.StaticPublication);
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-static-cleanup",
|
||||||
|
() => _staticPublisher.AdvanceBeginOne(
|
||||||
|
transaction.StaticPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!transaction.PhysicsPublication.CompletionCommitted)
|
||||||
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.PhysicsPublication.GfxCursor
|
||||||
|
< transaction.PhysicsPublication.GfxObjectIds.Length
|
||||||
|
? 1
|
||||||
|
: transaction.PhysicsPublication.PriorStaticCursor
|
||||||
|
< transaction.PhysicsPublication
|
||||||
|
.PriorStaticOwnerIds.Length
|
||||||
|
? 1
|
||||||
|
: transaction.PhysicsPublication.StaticCursor
|
||||||
|
< transaction.Build.Landblock.Entities.Count
|
||||||
|
? 1
|
||||||
|
: transaction.PhysicsPublication
|
||||||
|
.RefloodOwnerIds is not null
|
||||||
|
&& transaction.PhysicsPublication
|
||||||
|
.RefloodCursor
|
||||||
|
< transaction.PhysicsPublication
|
||||||
|
.RefloodOwnerIds.Length
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-physics-statics",
|
||||||
|
() => _physicsPublisher.AdvanceCompleteOne(
|
||||||
|
transaction.PhysicsPublication,
|
||||||
|
entity =>
|
||||||
|
_staticPublisher
|
||||||
|
.PrepareEntityBeforeCollision(
|
||||||
|
transaction.StaticPublication,
|
||||||
|
entity))))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!transaction.StaticPublication.CompletionCommitted)
|
||||||
|
{
|
||||||
|
int entityOperations =
|
||||||
|
transaction.StaticPublication.PluginCursor
|
||||||
|
< transaction.StaticPublication.OrderedSnapshots.Count
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
if (!TryRun(
|
||||||
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: entityOperations),
|
||||||
|
"publication-plugin-projection",
|
||||||
|
() => _staticPublisher.AdvanceCompleteOne(
|
||||||
|
transaction.StaticPublication)))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
StreamingWorkCost legacyCost = new(
|
||||||
|
EntityOperations: transaction.Cost.Entities,
|
||||||
|
GpuUploadBytes: transaction.Cost.TerrainPayloadBytes);
|
||||||
|
bool ran = TryRun(
|
||||||
|
legacyCost,
|
||||||
|
"publication-legacy-presentation",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(_publishBeforeSpatialCommit
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"The presentation pipeline has no publication owners."))(
|
||||||
|
transaction.Build,
|
||||||
|
transaction.MeshData);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
// Compatibility callbacks cannot report an exact retained
|
||||||
|
// receipt. Preserve the established fail-fast rule only
|
||||||
|
// for callbacks that explicitly report committed mutation.
|
||||||
|
if (error is StreamingMutationException
|
||||||
|
{
|
||||||
|
MutationCommitted: true,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
_publications.Remove(result);
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!ran)
|
||||||
{
|
{
|
||||||
(_publishBeforeSpatialCommit
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"The presentation pipeline has no publication owners."))(
|
|
||||||
transaction.Build,
|
|
||||||
transaction.MeshData);
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
// Compatibility callbacks cannot report an exact retained
|
|
||||||
// receipt. Preserve the established fail-fast rule only
|
|
||||||
// for callbacks that explicitly report committed mutation.
|
|
||||||
if (error is StreamingMutationException { MutationCommitted: true })
|
|
||||||
_publications.Remove(result);
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transaction.PresentationCommitted = true;
|
transaction.PresentationCommitted = true;
|
||||||
|
|
@ -387,46 +749,70 @@ public sealed class LandblockPresentationPipeline
|
||||||
// boundary: bucket mutation, pin/script reconciliation, then the
|
// boundary: bucket mutation, pin/script reconciliation, then the
|
||||||
// outer spatial observer commit. If pinning throws, the receipt is
|
// outer spatial observer commit. If pinning throws, the receipt is
|
||||||
// retained and only that suffix retries.
|
// retained and only that suffix retries.
|
||||||
using GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
|
if (!TryRun(
|
||||||
if (!transaction.SpatialCommitted)
|
default,
|
||||||
{
|
"publication-spatial-commit",
|
||||||
IEnumerable<ulong>? renderIds =
|
() =>
|
||||||
transaction.Build.EnvCells?.Shells.Select(static shell => shell.GeometryId);
|
{
|
||||||
transaction.SpatialPublication = transaction.Kind switch
|
using GpuWorldState.MutationBatch mutation =
|
||||||
{
|
_state.BeginMutationBatch();
|
||||||
PublicationKind.Loaded
|
if (!transaction.SpatialCommitted)
|
||||||
or PublicationKind.PromoteSelfContained
|
{
|
||||||
or PublicationKind.Far =>
|
IEnumerable<ulong>? renderIds =
|
||||||
_state.CommitLandblockSpatial(
|
transaction.Build.EnvCells?.Shells.Select(
|
||||||
transaction.Build.Landblock,
|
static shell => shell.GeometryId);
|
||||||
renderIds,
|
transaction.SpatialPublication = transaction.Kind switch
|
||||||
transaction.Tier),
|
{
|
||||||
PublicationKind.PromoteExisting =>
|
PublicationKind.Loaded
|
||||||
_state.CommitEntitiesToExistingLandblockSpatial(
|
or PublicationKind.PromoteSelfContained
|
||||||
transaction.LandblockId,
|
or PublicationKind.Far =>
|
||||||
transaction.Build.Landblock.Entities,
|
_state.CommitLandblockSpatial(
|
||||||
renderIds),
|
transaction.Build.Landblock,
|
||||||
_ => throw new InvalidOperationException(
|
renderIds,
|
||||||
$"Unknown landblock publication kind {transaction.Kind}."),
|
transaction.Tier),
|
||||||
};
|
PublicationKind.PromoteExisting =>
|
||||||
transaction.SpatialCommitted = true;
|
_state.CommitEntitiesToExistingLandblockSpatial(
|
||||||
}
|
transaction.LandblockId,
|
||||||
|
transaction.Build.Landblock.Entities,
|
||||||
|
renderIds),
|
||||||
|
_ => throw new InvalidOperationException(
|
||||||
|
$"Unknown landblock publication kind {transaction.Kind}."),
|
||||||
|
};
|
||||||
|
transaction.SpatialCommitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
_state.ActivateLandblockPresentation(
|
_state.ActivateLandblockPresentation(
|
||||||
transaction.SpatialPublication
|
transaction.SpatialPublication
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"A committed spatial publication has no activation receipt."));
|
"A committed spatial publication has no activation receipt."));
|
||||||
transaction.SpatialPresentationCommitted = true;
|
transaction.SpatialPresentationCommitted = true;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transaction.EnvCellReplayCommitted)
|
if (!transaction.EnvCellReplayCommitted)
|
||||||
{
|
{
|
||||||
if (transaction.Kind != PublicationKind.Far
|
if (!TryRun(
|
||||||
&& transaction.Build.EnvCells is { Shells.Length: > 0 } envCells)
|
new StreamingWorkCost(
|
||||||
|
EntityOperations: transaction.Cost.EnvCellShells),
|
||||||
|
"publication-envcell-replay",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (transaction.Kind != PublicationKind.Far
|
||||||
|
&& transaction.Build.EnvCells is
|
||||||
|
{
|
||||||
|
Shells.Length: > 0,
|
||||||
|
} envCells)
|
||||||
|
{
|
||||||
|
_ensureEnvCellMeshes?.Invoke(envCells);
|
||||||
|
}
|
||||||
|
transaction.EnvCellReplayCommitted = true;
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
_ensureEnvCellMeshes?.Invoke(envCells);
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
}
|
}
|
||||||
transaction.EnvCellReplayCommitted = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transaction.LiveRecoveryCommitted)
|
if (!transaction.LiveRecoveryCommitted)
|
||||||
|
|
@ -434,10 +820,20 @@ public sealed class LandblockPresentationPipeline
|
||||||
// Retained live projections can materialize only after the
|
// Retained live projections can materialize only after the
|
||||||
// canonical bucket exists. Landblock load never reconstructs their
|
// canonical bucket exists. Landblock load never reconstructs their
|
||||||
// identity or replays create-time resources.
|
// identity or replays create-time resources.
|
||||||
_onLandblockLoaded?.Invoke(transaction.LandblockId);
|
if (!TryRun(
|
||||||
transaction.LiveRecoveryCommitted = true;
|
default,
|
||||||
|
"publication-live-recovery",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_onLandblockLoaded?.Invoke(transaction.LandblockId);
|
||||||
|
transaction.LiveRecoveryCommitted = true;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return new LandblockPublicationAdvance(false, progressed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_publications.Remove(result);
|
_publications.Remove(result);
|
||||||
|
return new LandblockPublicationAdvance(true, progressed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ public sealed class LandblockRenderPublication
|
||||||
Vector3 origin,
|
Vector3 origin,
|
||||||
Dictionary<uint, LoadedCell> visibilityCells,
|
Dictionary<uint, LoadedCell> visibilityCells,
|
||||||
Vector3 aabbMin,
|
Vector3 aabbMin,
|
||||||
Vector3 aabbMax)
|
Vector3 aabbMax,
|
||||||
|
BuildingRegistryPublication? buildingPublication,
|
||||||
|
EnvCellLandblockPublication? envCellPublication)
|
||||||
{
|
{
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
Build = build;
|
Build = build;
|
||||||
|
|
@ -33,6 +35,8 @@ public sealed class LandblockRenderPublication
|
||||||
_visibilityCells = new ReadOnlyDictionary<uint, LoadedCell>(visibilityCells);
|
_visibilityCells = new ReadOnlyDictionary<uint, LoadedCell>(visibilityCells);
|
||||||
AabbMin = aabbMin;
|
AabbMin = aabbMin;
|
||||||
AabbMax = aabbMax;
|
AabbMax = aabbMax;
|
||||||
|
BuildingPublication = buildingPublication;
|
||||||
|
EnvCellPublication = envCellPublication;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object Owner { get; }
|
internal object Owner { get; }
|
||||||
|
|
@ -40,6 +44,8 @@ public sealed class LandblockRenderPublication
|
||||||
internal LandblockMeshData MeshData { get; }
|
internal LandblockMeshData MeshData { get; }
|
||||||
internal Vector3 AabbMin { get; }
|
internal Vector3 AabbMin { get; }
|
||||||
internal Vector3 AabbMax { get; }
|
internal Vector3 AabbMax { get; }
|
||||||
|
internal BuildingRegistryPublication? BuildingPublication { get; }
|
||||||
|
internal EnvCellLandblockPublication? EnvCellPublication { get; }
|
||||||
internal bool TerrainCommitted { get; set; }
|
internal bool TerrainCommitted { get; set; }
|
||||||
internal bool VisibilityCommitted { get; set; }
|
internal bool VisibilityCommitted { get; set; }
|
||||||
internal bool AabbCommitted { get; set; }
|
internal bool AabbCommitted { get; set; }
|
||||||
|
|
@ -92,6 +98,7 @@ public sealed class LandblockRenderPublisher
|
||||||
private readonly CellVisibility _cellVisibility;
|
private readonly CellVisibility _cellVisibility;
|
||||||
private readonly GpuWorldState _worldState;
|
private readonly GpuWorldState _worldState;
|
||||||
private readonly Action<EnvCellLandblockBuild>? _commitEnvCells;
|
private readonly Action<EnvCellLandblockBuild>? _commitEnvCells;
|
||||||
|
private readonly IEnvCellLandblockPublisher? _envCellPublisher;
|
||||||
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
|
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
|
||||||
private readonly Action<uint>? _removeEnvCells;
|
private readonly Action<uint>? _removeEnvCells;
|
||||||
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
|
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
|
||||||
|
|
@ -114,7 +121,8 @@ public sealed class LandblockRenderPublisher
|
||||||
GpuWorldState worldState,
|
GpuWorldState worldState,
|
||||||
Action<EnvCellLandblockBuild>? commitEnvCells = null,
|
Action<EnvCellLandblockBuild>? commitEnvCells = null,
|
||||||
Action<EnvCellLandblockBuild>? prepareEnvCells = null,
|
Action<EnvCellLandblockBuild>? prepareEnvCells = null,
|
||||||
Action<uint>? removeEnvCells = null)
|
Action<uint>? removeEnvCells = null,
|
||||||
|
IEnvCellLandblockPublisher? envCellPublisher = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(publishTerrain);
|
ArgumentNullException.ThrowIfNull(publishTerrain);
|
||||||
ArgumentNullException.ThrowIfNull(removeTerrain);
|
ArgumentNullException.ThrowIfNull(removeTerrain);
|
||||||
|
|
@ -126,6 +134,13 @@ public sealed class LandblockRenderPublisher
|
||||||
_cellVisibility = cellVisibility;
|
_cellVisibility = cellVisibility;
|
||||||
_worldState = worldState;
|
_worldState = worldState;
|
||||||
_commitEnvCells = commitEnvCells;
|
_commitEnvCells = commitEnvCells;
|
||||||
|
_envCellPublisher = envCellPublisher;
|
||||||
|
if (_commitEnvCells is not null && _envCellPublisher is not null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Supply either the retained EnvCell publisher or the compatibility callback, not both.",
|
||||||
|
nameof(envCellPublisher));
|
||||||
|
}
|
||||||
_prepareEnvCells = prepareEnvCells;
|
_prepareEnvCells = prepareEnvCells;
|
||||||
_removeEnvCells = removeEnvCells;
|
_removeEnvCells = removeEnvCells;
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +212,17 @@ public sealed class LandblockRenderPublisher
|
||||||
visibilityCells[cell.CellId] = cell;
|
visibilityCells[cell.CellId] = cell;
|
||||||
}
|
}
|
||||||
(Vector3 aabbMin, Vector3 aabbMax) = ComputeAabb(meshData, origin);
|
(Vector3 aabbMin, Vector3 aabbMax) = ComputeAabb(meshData, origin);
|
||||||
|
BuildingRegistryPublication? buildingPublication =
|
||||||
|
build.Landblock.PhysicsDats?.Info is { } info
|
||||||
|
? BuildingLoader.PreparePublication(
|
||||||
|
info,
|
||||||
|
landblockId,
|
||||||
|
visibilityCells)
|
||||||
|
: null;
|
||||||
|
EnvCellLandblockPublication? envCellPublication =
|
||||||
|
build.EnvCells is { } retainedEnvCells
|
||||||
|
? _envCellPublisher?.PreparePublication(retainedEnvCells)
|
||||||
|
: null;
|
||||||
return new LandblockRenderPublication(
|
return new LandblockRenderPublication(
|
||||||
_receiptOwner,
|
_receiptOwner,
|
||||||
build,
|
build,
|
||||||
|
|
@ -204,7 +230,9 @@ public sealed class LandblockRenderPublisher
|
||||||
origin,
|
origin,
|
||||||
visibilityCells,
|
visibilityCells,
|
||||||
aabbMin,
|
aabbMin,
|
||||||
aabbMax);
|
aabbMax,
|
||||||
|
buildingPublication,
|
||||||
|
envCellPublication);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -214,13 +242,25 @@ public sealed class LandblockRenderPublisher
|
||||||
public void BeginPublication(LandblockRenderPublication publication)
|
public void BeginPublication(LandblockRenderPublication publication)
|
||||||
{
|
{
|
||||||
ValidateReceipt(publication);
|
ValidateReceipt(publication);
|
||||||
if (publication.BeginCommitted)
|
while (!AdvanceBeginOne(publication))
|
||||||
return;
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
long beginStarted = Stopwatch.GetTimestamp();
|
/// <summary>
|
||||||
|
/// Advances one exact render-prefix mutation from the retained receipt.
|
||||||
|
/// Terrain, visibility, and AABB publication remain ordered and retries do
|
||||||
|
/// not replay an already committed prefix.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceBeginOne(LandblockRenderPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (publication.BeginCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
long started = Stopwatch.GetTimestamp();
|
||||||
LandblockBuild build = publication.Build;
|
LandblockBuild build = publication.Build;
|
||||||
uint landblockId = publication.LandblockId;
|
uint landblockId = publication.LandblockId;
|
||||||
|
|
||||||
if (!publication.TerrainCommitted)
|
if (!publication.TerrainCommitted)
|
||||||
{
|
{
|
||||||
long terrainStarted = Stopwatch.GetTimestamp();
|
long terrainStarted = Stopwatch.GetTimestamp();
|
||||||
|
|
@ -228,15 +268,13 @@ public sealed class LandblockRenderPublisher
|
||||||
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
|
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
|
||||||
publication.TerrainCommitted = true;
|
publication.TerrainCommitted = true;
|
||||||
}
|
}
|
||||||
|
else if (!publication.VisibilityCommitted)
|
||||||
if (!publication.VisibilityCommitted)
|
|
||||||
{
|
{
|
||||||
if (build.EnvCells is { } envCells)
|
if (build.EnvCells is { } envCells)
|
||||||
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
|
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
|
||||||
publication.VisibilityCommitted = true;
|
publication.VisibilityCommitted = true;
|
||||||
}
|
}
|
||||||
|
else if (!publication.AabbCommitted)
|
||||||
if (!publication.AabbCommitted)
|
|
||||||
{
|
{
|
||||||
_worldState.SetLandblockAabb(
|
_worldState.SetLandblockAabb(
|
||||||
landblockId,
|
landblockId,
|
||||||
|
|
@ -244,10 +282,14 @@ public sealed class LandblockRenderPublisher
|
||||||
publication.AabbMax);
|
publication.AabbMax);
|
||||||
publication.AabbCommitted = true;
|
publication.AabbCommitted = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
publication.BeginCommitted = true;
|
||||||
|
_beginCount++;
|
||||||
|
}
|
||||||
|
|
||||||
publication.BeginCommitted = true;
|
_beginPublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
_beginCount++;
|
return publication.BeginCommitted;
|
||||||
_beginPublishTicks += Stopwatch.GetTimestamp() - beginStarted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -256,40 +298,77 @@ public sealed class LandblockRenderPublisher
|
||||||
/// receives the complete immutable transaction.
|
/// receives the complete immutable transaction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CompletePublication(LandblockRenderPublication publication)
|
public void CompletePublication(LandblockRenderPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (!publication.BeginCommitted)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Render publication cannot complete before its prefix commits.");
|
||||||
|
while (!AdvanceCompleteOne(publication))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances one exact render-suffix mutation. The building registry is
|
||||||
|
/// visible before the EnvCell renderer snapshot, matching the existing
|
||||||
|
/// retail-rooted ordering.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceCompleteOne(LandblockRenderPublication publication)
|
||||||
{
|
{
|
||||||
ValidateReceipt(publication);
|
ValidateReceipt(publication);
|
||||||
if (!publication.BeginCommitted)
|
if (!publication.BeginCommitted)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Render publication cannot complete before its prefix commits.");
|
"Render publication cannot complete before its prefix commits.");
|
||||||
if (publication.CompletionCommitted)
|
if (publication.CompletionCommitted)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
long started = Stopwatch.GetTimestamp();
|
long started = Stopwatch.GetTimestamp();
|
||||||
LandblockBuild build = publication.Build;
|
LandblockBuild build = publication.Build;
|
||||||
uint landblockId = publication.LandblockId;
|
uint landblockId = publication.LandblockId;
|
||||||
if (!publication.BuildingRegistryCommitted)
|
if (publication.BuildingPublication is { PreparationCommitted: false }
|
||||||
|
buildingPublication)
|
||||||
{
|
{
|
||||||
if (build.Landblock.PhysicsDats?.Info is { } info)
|
BuildingLoader.AdvancePreparationOne(buildingPublication);
|
||||||
|
}
|
||||||
|
else if (!publication.BuildingRegistryCommitted)
|
||||||
|
{
|
||||||
|
if (publication.BuildingPublication is { } completedBuildings)
|
||||||
{
|
{
|
||||||
|
BuildingLoader.CommitPublication(completedBuildings);
|
||||||
uint registryKey = landblockId & 0xFFFF0000u;
|
uint registryKey = landblockId & 0xFFFF0000u;
|
||||||
_buildingRegistries[registryKey] = BuildingLoader.Build(
|
_buildingRegistries[registryKey] =
|
||||||
info,
|
completedBuildings.Registry;
|
||||||
landblockId,
|
|
||||||
publication.VisibilityCells);
|
|
||||||
}
|
}
|
||||||
publication.BuildingRegistryCommitted = true;
|
publication.BuildingRegistryCommitted = true;
|
||||||
}
|
}
|
||||||
|
else if (publication.EnvCellPublication is { } envCellPublication
|
||||||
if (!publication.EnvCellsCommitted)
|
&& !envCellPublication.PreparationCommitted)
|
||||||
{
|
{
|
||||||
if (build.EnvCells is { } envCells)
|
_envCellPublisher?.AdvancePreparationOne(envCellPublication);
|
||||||
|
}
|
||||||
|
else if (!publication.EnvCellsCommitted)
|
||||||
|
{
|
||||||
|
if (publication.EnvCellPublication is { } retained)
|
||||||
|
{
|
||||||
|
(_envCellPublisher
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"A retained EnvCell receipt has no owning publisher."))
|
||||||
|
.CommitPublication(retained);
|
||||||
|
}
|
||||||
|
else if (build.EnvCells is { } envCells)
|
||||||
|
{
|
||||||
_commitEnvCells?.Invoke(envCells);
|
_commitEnvCells?.Invoke(envCells);
|
||||||
|
}
|
||||||
publication.EnvCellsCommitted = true;
|
publication.EnvCellsCommitted = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
publication.CompletionCommitted = true;
|
||||||
|
_completeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
publication.CompletionCommitted = true;
|
|
||||||
_completeCount++;
|
|
||||||
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
|
return publication.CompletionCommitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -13,26 +13,46 @@ namespace AcDream.App.Streaming;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LandblockStaticPresentationPublication
|
public sealed class LandblockStaticPresentationPublication
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyDictionary<uint, WorldEntity> _entities;
|
private readonly Dictionary<uint, WorldEntity> _entities;
|
||||||
private readonly IReadOnlyDictionary<uint, WorldEntitySnapshot> _snapshots;
|
private readonly Dictionary<uint, WorldEntitySnapshot> _snapshots;
|
||||||
|
private readonly uint[] _orderedPreviouslyActiveIds;
|
||||||
|
private readonly Dictionary<uint, WorldEntitySnapshot> _replacementActive;
|
||||||
|
|
||||||
internal LandblockStaticPresentationPublication(
|
internal LandblockStaticPresentationPublication(
|
||||||
object owner,
|
object owner,
|
||||||
LandblockPhysicsPublication physicsPublication,
|
LandblockPhysicsPublication physicsPublication,
|
||||||
Dictionary<uint, WorldEntity> entities,
|
Dictionary<uint, WorldEntity> entities,
|
||||||
Dictionary<uint, WorldEntitySnapshot> snapshots,
|
Dictionary<uint, WorldEntitySnapshot> snapshots,
|
||||||
HashSet<uint> previouslyActiveIds)
|
IReadOnlyDictionary<uint, WorldEntitySnapshot> priorActive,
|
||||||
|
HashSet<uint> previouslyActiveIds,
|
||||||
|
uint[] orderedPreviouslyActiveIds)
|
||||||
{
|
{
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
PhysicsPublication = physicsPublication;
|
PhysicsPublication = physicsPublication;
|
||||||
_entities = entities;
|
_entities = entities;
|
||||||
_snapshots = snapshots;
|
_snapshots = snapshots;
|
||||||
|
PriorActive = priorActive;
|
||||||
PreviouslyActiveIds = previouslyActiveIds;
|
PreviouslyActiveIds = previouslyActiveIds;
|
||||||
|
_orderedPreviouslyActiveIds = orderedPreviouslyActiveIds;
|
||||||
|
_replacementActive = new Dictionary<uint, WorldEntitySnapshot>(
|
||||||
|
physicsPublication.Build.Landblock.Entities.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object Owner { get; }
|
internal object Owner { get; }
|
||||||
internal LandblockPhysicsPublication PhysicsPublication { get; }
|
internal LandblockPhysicsPublication PhysicsPublication { get; }
|
||||||
|
internal IReadOnlyDictionary<uint, WorldEntitySnapshot> PriorActive { get; }
|
||||||
|
internal Dictionary<uint, WorldEntity> MutableEntities => _entities;
|
||||||
|
internal Dictionary<uint, WorldEntitySnapshot> MutableSnapshots => _snapshots;
|
||||||
internal HashSet<uint> PreviouslyActiveIds { get; }
|
internal HashSet<uint> PreviouslyActiveIds { get; }
|
||||||
|
internal IReadOnlyList<uint> OrderedPreviouslyActiveIds =>
|
||||||
|
_orderedPreviouslyActiveIds;
|
||||||
|
internal IReadOnlyList<KeyValuePair<uint, WorldEntitySnapshot>>
|
||||||
|
OrderedSnapshots { get; set; } =
|
||||||
|
Array.Empty<KeyValuePair<uint, WorldEntitySnapshot>>();
|
||||||
|
internal Dictionary<uint, WorldEntitySnapshot> ReplacementActive =>
|
||||||
|
_replacementActive;
|
||||||
|
internal int PreparationCursor { get; set; }
|
||||||
|
internal bool PreparationCommitted { get; set; }
|
||||||
internal int PriorCleanupCursor { get; set; }
|
internal int PriorCleanupCursor { get; set; }
|
||||||
internal int PluginCursor { get; set; }
|
internal int PluginCursor { get; set; }
|
||||||
internal bool BeginCommitted { get; set; }
|
internal bool BeginCommitted { get; set; }
|
||||||
|
|
@ -116,6 +136,22 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
|
|
||||||
public LandblockStaticPresentationPublication PreparePublication(
|
public LandblockStaticPresentationPublication PreparePublication(
|
||||||
LandblockPhysicsPublication physicsPublication)
|
LandblockPhysicsPublication physicsPublication)
|
||||||
|
{
|
||||||
|
LandblockStaticPresentationPublication publication =
|
||||||
|
CreatePublication(physicsPublication);
|
||||||
|
while (!AdvancePreparationOne(publication))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return publication;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures prior ownership without validating the replacement entity
|
||||||
|
/// list. The concrete pipeline advances replacement validation one entity
|
||||||
|
/// at a time before any render/physics owner mutates external state.
|
||||||
|
/// </summary>
|
||||||
|
internal LandblockStaticPresentationPublication CreatePublication(
|
||||||
|
LandblockPhysicsPublication physicsPublication)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(physicsPublication);
|
ArgumentNullException.ThrowIfNull(physicsPublication);
|
||||||
|
|
||||||
|
|
@ -125,11 +161,43 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
out Dictionary<uint, WorldEntitySnapshot>? active);
|
out Dictionary<uint, WorldEntitySnapshot>? active);
|
||||||
var entities = new Dictionary<uint, WorldEntity>();
|
var entities = new Dictionary<uint, WorldEntity>();
|
||||||
var snapshots = new Dictionary<uint, WorldEntitySnapshot>();
|
var snapshots = new Dictionary<uint, WorldEntitySnapshot>();
|
||||||
foreach (WorldEntity entity in physicsPublication.Build.Landblock.Entities)
|
HashSet<uint> previous = active is not null
|
||||||
|
? active.Keys.ToHashSet()
|
||||||
|
: new HashSet<uint>();
|
||||||
|
uint[] orderedPrevious = previous.Order().ToArray();
|
||||||
|
return new LandblockStaticPresentationPublication(
|
||||||
|
_receiptOwner,
|
||||||
|
physicsPublication,
|
||||||
|
entities,
|
||||||
|
snapshots,
|
||||||
|
active ?? new Dictionary<uint, WorldEntitySnapshot>(),
|
||||||
|
previous,
|
||||||
|
orderedPrevious);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates and snapshots one replacement entity. The final stable plugin
|
||||||
|
/// order is materialized only after the cursor reaches the immutable tail.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvancePreparationOne(
|
||||||
|
LandblockStaticPresentationPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (publication.PreparationCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
IReadOnlyList<WorldEntity> source =
|
||||||
|
publication.PhysicsPublication.Build.Landblock.Entities;
|
||||||
|
uint canonical = Canonicalize(publication.LandblockId);
|
||||||
|
if (publication.PreparationCursor < source.Count)
|
||||||
{
|
{
|
||||||
|
WorldEntity entity = source[publication.PreparationCursor];
|
||||||
if (entity.ServerGuid != 0)
|
if (entity.ServerGuid != 0)
|
||||||
continue;
|
{
|
||||||
if (!entities.TryAdd(entity.Id, entity))
|
publication.PreparationCursor++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (publication.Entities.ContainsKey(entity.Id))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Landblock 0x{canonical:X8} contains duplicate DAT-static ID " +
|
$"Landblock 0x{canonical:X8} contains duplicate DAT-static ID " +
|
||||||
|
|
@ -142,8 +210,9 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
$"DAT-static ID 0x{entity.Id:X8} is already owned by " +
|
$"DAT-static ID 0x{entity.Id:X8} is already owned by " +
|
||||||
$"landblock 0x{owner:X8}.");
|
$"landblock 0x{owner:X8}.");
|
||||||
}
|
}
|
||||||
if (active is not null
|
if (publication.PriorActive.TryGetValue(
|
||||||
&& active.TryGetValue(entity.Id, out WorldEntitySnapshot retained)
|
entity.Id,
|
||||||
|
out WorldEntitySnapshot retained)
|
||||||
&& retained.SourceId != entity.SourceGfxObjOrSetupId)
|
&& retained.SourceId != entity.SourceGfxObjOrSetupId)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -152,18 +221,17 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
$"0x{entity.SourceGfxObjOrSetupId:X8}.");
|
$"0x{entity.SourceGfxObjOrSetupId:X8}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots.Add(entity.Id, Snapshot(entity));
|
publication.MutableEntities.Add(entity.Id, entity);
|
||||||
|
publication.MutableSnapshots.Add(entity.Id, Snapshot(entity));
|
||||||
|
publication.PreparationCursor++;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<uint> previous = active is not null
|
publication.OrderedSnapshots = publication.Snapshots
|
||||||
? active.Keys.ToHashSet()
|
.OrderBy(static pair => pair.Key)
|
||||||
: new HashSet<uint>();
|
.ToArray();
|
||||||
return new LandblockStaticPresentationPublication(
|
publication.PreparationCommitted = true;
|
||||||
_receiptOwner,
|
return true;
|
||||||
physicsPublication,
|
|
||||||
entities,
|
|
||||||
snapshots,
|
|
||||||
previous);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -174,13 +242,31 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
public void BeginPublication(LandblockStaticPresentationPublication publication)
|
public void BeginPublication(LandblockStaticPresentationPublication publication)
|
||||||
{
|
{
|
||||||
ValidateReceipt(publication);
|
ValidateReceipt(publication);
|
||||||
if (publication.BeginCommitted)
|
if (!publication.PreparationCommitted)
|
||||||
return;
|
throw new InvalidOperationException(
|
||||||
|
"Static presentation cannot begin before preparation commits.");
|
||||||
uint[] priorIds = publication.PreviouslyActiveIds.Order().ToArray();
|
while (!AdvanceBeginOne(publication))
|
||||||
while (publication.PriorCleanupCursor < priorIds.Length)
|
|
||||||
{
|
{
|
||||||
uint id = priorIds[publication.PriorCleanupCursor];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances exactly one retained prior-owner cleanup. The cursor advances
|
||||||
|
/// only after the complete owner operation succeeds, so a failure retries
|
||||||
|
/// the same ID without rebuilding or reordering the work list.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceBeginOne(
|
||||||
|
LandblockStaticPresentationPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (publication.BeginCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (publication.PriorCleanupCursor
|
||||||
|
< publication.OrderedPreviouslyActiveIds.Count)
|
||||||
|
{
|
||||||
|
uint id = publication.OrderedPreviouslyActiveIds[
|
||||||
|
publication.PriorCleanupCursor];
|
||||||
bool retained = publication.Entities.ContainsKey(id);
|
bool retained = publication.Entities.ContainsKey(id);
|
||||||
_lighting.UnregisterOwner(id, forgetState: !retained);
|
_lighting.UnregisterOwner(id, forgetState: !retained);
|
||||||
if (!retained)
|
if (!retained)
|
||||||
|
|
@ -192,10 +278,12 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
_pluginRemovalCount++;
|
_pluginRemovalCount++;
|
||||||
}
|
}
|
||||||
publication.PriorCleanupCursor++;
|
publication.PriorCleanupCursor++;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication.BeginCommitted = true;
|
publication.BeginCommitted = true;
|
||||||
_beginCount++;
|
_beginCount++;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -258,15 +346,32 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Static presentation requires completed physics publication.");
|
"Static presentation requires completed physics publication.");
|
||||||
}
|
}
|
||||||
if (publication.CompletionCommitted)
|
while (!AdvanceCompleteOne(publication))
|
||||||
return;
|
|
||||||
|
|
||||||
KeyValuePair<uint, WorldEntitySnapshot>[] snapshots = publication.Snapshots
|
|
||||||
.OrderBy(pair => pair.Key)
|
|
||||||
.ToArray();
|
|
||||||
while (publication.PluginCursor < snapshots.Length)
|
|
||||||
{
|
{
|
||||||
(uint id, WorldEntitySnapshot snapshot) = snapshots[publication.PluginCursor];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances exactly one plugin projection, then atomically publishes the
|
||||||
|
/// retained replacement dictionary after the cursor reaches its tail.
|
||||||
|
/// </summary>
|
||||||
|
internal bool AdvanceCompleteOne(
|
||||||
|
LandblockStaticPresentationPublication publication)
|
||||||
|
{
|
||||||
|
ValidateReceipt(publication);
|
||||||
|
if (!publication.BeginCommitted
|
||||||
|
|| !publication.PhysicsPublication.CompletionCommitted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Static presentation requires completed physics publication.");
|
||||||
|
}
|
||||||
|
if (publication.CompletionCommitted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (publication.PluginCursor < publication.OrderedSnapshots.Count)
|
||||||
|
{
|
||||||
|
(uint id, WorldEntitySnapshot snapshot) =
|
||||||
|
publication.OrderedSnapshots[publication.PluginCursor];
|
||||||
_worldState.Add(snapshot);
|
_worldState.Add(snapshot);
|
||||||
if (publication.PreviouslyActiveIds.Contains(id))
|
if (publication.PreviouslyActiveIds.Contains(id))
|
||||||
{
|
{
|
||||||
|
|
@ -279,21 +384,19 @@ public sealed class LandblockStaticPresentationPublisher
|
||||||
_pluginSpawnCount++;
|
_pluginSpawnCount++;
|
||||||
}
|
}
|
||||||
_landblockByEntityId[id] = Canonicalize(publication.LandblockId);
|
_landblockByEntityId[id] = Canonicalize(publication.LandblockId);
|
||||||
|
publication.ReplacementActive[id] = snapshot;
|
||||||
publication.PluginCursor++;
|
publication.PluginCursor++;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint canonical = Canonicalize(publication.LandblockId);
|
uint canonical = Canonicalize(publication.LandblockId);
|
||||||
if (publication.Snapshots.Count == 0)
|
if (publication.ReplacementActive.Count == 0)
|
||||||
{
|
|
||||||
_activeByLandblock.Remove(canonical);
|
_activeByLandblock.Remove(canonical);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
_activeByLandblock[canonical] = publication.ReplacementActive;
|
||||||
_activeByLandblock[canonical] = publication.Snapshots
|
|
||||||
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
|
||||||
}
|
|
||||||
publication.CompletionCommitted = true;
|
publication.CompletionCommitted = true;
|
||||||
_completeCount++;
|
_completeCount++;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveLighting(WorldEntity entity)
|
public void RemoveLighting(WorldEntity entity)
|
||||||
|
|
|
||||||
|
|
@ -29,18 +29,7 @@ internal readonly record struct StreamingQueuedCompletion(
|
||||||
StreamingCompletionPriority Priority,
|
StreamingCompletionPriority Priority,
|
||||||
ulong Generation,
|
ulong Generation,
|
||||||
long Sequence,
|
long Sequence,
|
||||||
long EnqueuedTimestamp)
|
long EnqueuedTimestamp);
|
||||||
{
|
|
||||||
public StreamingWorkCost AdmissionCost => new(
|
|
||||||
CompletionAdmissions: Estimate.Work.CompletionAdmissions,
|
|
||||||
AdoptedCpuBytes: Estimate.Work.AdoptedCpuBytes);
|
|
||||||
|
|
||||||
public StreamingWorkCost ExecutionCost => new(
|
|
||||||
EntityOperations: Estimate.Work.EntityOperations,
|
|
||||||
GpuUploadBytes: Estimate.Work.GpuUploadBytes,
|
|
||||||
GlRetireOperations:
|
|
||||||
Result is LandblockStreamResult.Unloaded ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct StreamingCompletionQueueSnapshot(
|
internal readonly record struct StreamingCompletionQueueSnapshot(
|
||||||
int Count,
|
int Count,
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// the desired-tier snapshot below is computed. Otherwise a landblock
|
// the desired-tier snapshot below is computed. Otherwise a landblock
|
||||||
// that becomes spatially resident during convergence is absent from
|
// that becomes spatially resident during convergence is absent from
|
||||||
// the reconfiguration mutation ledger.
|
// the reconfiguration mutation ledger.
|
||||||
ConvergePendingPublications();
|
if (!ConvergePendingPublications())
|
||||||
|
{
|
||||||
|
_deferredRadiiRequest = (nearRadius, farRadius);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_collapsed || _region is null)
|
if (_collapsed || _region is null)
|
||||||
{
|
{
|
||||||
|
|
@ -490,6 +494,12 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
_presentation.GetPendingPublicationResults();
|
_presentation.GetPendingPublicationResults();
|
||||||
if (pending.Count == 0)
|
if (pending.Count == 0)
|
||||||
return true;
|
return true;
|
||||||
|
if (_activeWorkMeter is null)
|
||||||
|
{
|
||||||
|
// Settings/native callbacks may request policy changes between
|
||||||
|
// frames, but publication remains owned by the frame-scoped meter.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -497,15 +507,14 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
for (int i = 0; i < pending.Count; i++)
|
for (int i = 0; i < pending.Count; i++)
|
||||||
{
|
{
|
||||||
LandblockStreamResult result = pending[i];
|
LandblockStreamResult result = pending[i];
|
||||||
if (!TryObserveResultOperation(
|
LandblockPublicationAdvance advance =
|
||||||
result,
|
_presentation.ResumePublication(
|
||||||
"publication-retry",
|
result,
|
||||||
() => _presentation.ResumePublication(result),
|
_activeWorkMeter,
|
||||||
ensureProgress: !progressed))
|
ensureProgress: !progressed);
|
||||||
{
|
progressed |= advance.Progressed;
|
||||||
|
if (!advance.Completed)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
progressed = true;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1487,23 +1496,16 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
StreamingQueuedCompletion work = completion
|
StreamingQueuedCompletion work = completion
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"The completion queue returned a null head.");
|
"The completion queue returned a null head.");
|
||||||
StreamingWorkCost executionCost =
|
|
||||||
IsStaleGeneration(work.Result)
|
|
||||||
? default
|
|
||||||
: work.ExecutionCost;
|
|
||||||
StreamingWorkAdmission admission = meter.TryReserve(
|
|
||||||
executionCost,
|
|
||||||
$"execute-{work.Priority}",
|
|
||||||
ensureProgress: !executed);
|
|
||||||
if (admission == StreamingWorkAdmission.Yielded)
|
|
||||||
break;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ApplyResult(work.Result);
|
LandblockPublicationAdvance advance = ApplyResult(
|
||||||
|
work,
|
||||||
|
meter,
|
||||||
|
ensureProgress: !executed);
|
||||||
|
executed |= advance.Progressed;
|
||||||
|
if (!advance.Completed)
|
||||||
|
break;
|
||||||
_completionQueue.RemoveHead(work);
|
_completionQueue.RemoveHead(work);
|
||||||
meter.Complete();
|
|
||||||
executed = true;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -1512,7 +1514,6 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// receipt cannot safely replay, so consume only that result.
|
// receipt cannot safely replay, so consume only that result.
|
||||||
if (!_presentation.HasPendingPublication(work.Result))
|
if (!_presentation.HasPendingPublication(work.Result))
|
||||||
_completionQueue.RemoveHead(work);
|
_completionQueue.RemoveHead(work);
|
||||||
meter.Fail();
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1625,39 +1626,24 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryObserveResultOperation(
|
private static LandblockPublicationAdvance RunSimpleResultOperation(
|
||||||
LandblockStreamResult result,
|
StreamingWorkMeter meter,
|
||||||
string stage,
|
string stage,
|
||||||
Action operation,
|
bool ensureProgress,
|
||||||
StreamingWorkCost? knownCost = null,
|
Action operation)
|
||||||
bool ensureProgress = false)
|
|
||||||
{
|
{
|
||||||
StreamingWorkMeter? meter = _activeWorkMeter;
|
|
||||||
if (meter is null)
|
|
||||||
{
|
|
||||||
operation();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamingWorkCost aggregate = knownCost
|
|
||||||
?? LandblockStreamResultCost.Estimate(result).Work;
|
|
||||||
StreamingWorkCost cost = new(
|
|
||||||
EntityOperations: aggregate.EntityOperations,
|
|
||||||
GpuUploadBytes: aggregate.GpuUploadBytes,
|
|
||||||
GlRetireOperations:
|
|
||||||
result is LandblockStreamResult.Unloaded ? 1 : 0);
|
|
||||||
StreamingWorkAdmission admission = meter.TryReserve(
|
StreamingWorkAdmission admission = meter.TryReserve(
|
||||||
cost,
|
default,
|
||||||
stage,
|
stage,
|
||||||
ensureProgress);
|
ensureProgress);
|
||||||
if (admission == StreamingWorkAdmission.Yielded)
|
if (admission == StreamingWorkAdmission.Yielded)
|
||||||
return false;
|
return new LandblockPublicationAdvance(false, false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
operation();
|
operation();
|
||||||
meter.Complete();
|
meter.Complete();
|
||||||
return true;
|
return new LandblockPublicationAdvance(true, true);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -1671,13 +1657,22 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
/// effects: terrain upload, GPU state, and the re-hydration callback.
|
/// effects: terrain upload, GPU state, and the re-hydration callback.
|
||||||
/// All priority queues route through this one publication path.
|
/// All priority queues route through this one publication path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ApplyResult(LandblockStreamResult result)
|
private LandblockPublicationAdvance ApplyResult(
|
||||||
|
StreamingQueuedCompletion work,
|
||||||
|
StreamingWorkMeter meter,
|
||||||
|
bool ensureProgress)
|
||||||
{
|
{
|
||||||
|
LandblockStreamResult result = work.Result;
|
||||||
if (_presentation.HasPendingPublication(result))
|
if (_presentation.HasPendingPublication(result))
|
||||||
{
|
{
|
||||||
_presentation.ResumePublication(result);
|
LandblockPublicationAdvance resumed =
|
||||||
ReconcileCompletedPendingPublication(result.LandblockId);
|
_presentation.ResumePublication(
|
||||||
return;
|
result,
|
||||||
|
meter,
|
||||||
|
ensureProgress);
|
||||||
|
if (resumed.Completed)
|
||||||
|
ReconcileCompletedPendingPublication(result.LandblockId);
|
||||||
|
return resumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsStaleGeneration(result))
|
if (IsStaleGeneration(result))
|
||||||
|
|
@ -1685,7 +1680,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// A worker can finish one old load/unload after a hard recenter.
|
// A worker can finish one old load/unload after a hard recenter.
|
||||||
// Landblock id membership cannot distinguish overlapping windows;
|
// Landblock id membership cannot distinguish overlapping windows;
|
||||||
// generation is the logical streaming incarnation boundary.
|
// generation is the logical streaming incarnation boundary.
|
||||||
return;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-stale-generation",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result is LandblockStreamResult.Unloaded
|
if (result is LandblockStreamResult.Unloaded
|
||||||
|
|
@ -1695,7 +1694,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// rapid away->back can therefore re-own an id while its same-
|
// rapid away->back can therefore re-own an id while its same-
|
||||||
// generation unload is already in the worker outbox. Current
|
// generation unload is already in the worker outbox. Current
|
||||||
// region ownership wins; the old unload must not tear it down.
|
// region ownership wins; the old unload must not tear it down.
|
||||||
return;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-reowned-unload",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result is LandblockStreamResult.Loaded
|
if (result is LandblockStreamResult.Loaded
|
||||||
|
|
@ -1707,7 +1710,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// A hard recenter/collapse can leave one worker job already in
|
// A hard recenter/collapse can leave one worker job already in
|
||||||
// flight. Its completion belongs to the old region and must not
|
// flight. Its completion belongs to the old region and must not
|
||||||
// resurrect a landblock the new StreamingRegion never owns.
|
// resurrect a landblock the new StreamingRegion never owns.
|
||||||
return;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-undesired-load",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNearCompletion = result is LandblockStreamResult.Promoted
|
bool isNearCompletion = result is LandblockStreamResult.Promoted
|
||||||
|
|
@ -1722,7 +1729,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// high-priority jobs. Publication is idempotent at this owner:
|
// high-priority jobs. Publication is idempotent at this owner:
|
||||||
// never append statics, replay defaults, or reapply the complete
|
// never append statics, replay defaults, or reapply the complete
|
||||||
// Near transaction to an already-Near landblock.
|
// Near transaction to an already-Near landblock.
|
||||||
return;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-duplicate-near",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
if (desiredTier == LandblockStreamTier.Far && isNearCompletion)
|
if (desiredTier == LandblockStreamTier.Far && isNearCompletion)
|
||||||
{
|
{
|
||||||
|
|
@ -1739,14 +1750,26 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
switch (result)
|
switch (result)
|
||||||
{
|
{
|
||||||
case LandblockStreamResult.Loaded loaded:
|
case LandblockStreamResult.Loaded loaded:
|
||||||
_presentation.PublishAsFar(loaded, loaded.Build, loaded.MeshData);
|
return _presentation.PublishAsFar(
|
||||||
break;
|
loaded,
|
||||||
|
loaded.Build,
|
||||||
|
loaded.MeshData,
|
||||||
|
meter,
|
||||||
|
ensureProgress);
|
||||||
case LandblockStreamResult.Promoted promoted:
|
case LandblockStreamResult.Promoted promoted:
|
||||||
_presentation.PublishAsFar(promoted, promoted.Build, promoted.MeshData);
|
return _presentation.PublishAsFar(
|
||||||
break;
|
promoted,
|
||||||
|
promoted.Build,
|
||||||
|
promoted.MeshData,
|
||||||
|
meter,
|
||||||
|
ensureProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-already-far",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1759,31 +1782,54 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
// This Far completion was queued before a newer Near load
|
// This Far completion was queued before a newer Near load
|
||||||
// or promotion. Applying it would erase the entity/cell
|
// or promotion. Applying it would erase the entity/cell
|
||||||
// layer that now owns the landblock.
|
// layer that now owns the landblock.
|
||||||
break;
|
return RunSimpleResultOperation(
|
||||||
|
meter,
|
||||||
|
"execute-stale-far-tier",
|
||||||
|
ensureProgress,
|
||||||
|
static () => { });
|
||||||
}
|
}
|
||||||
_presentation.PublishLoaded(loaded);
|
return _presentation.PublishLoaded(
|
||||||
break;
|
loaded,
|
||||||
|
work.Estimate,
|
||||||
|
meter,
|
||||||
|
ensureProgress);
|
||||||
case LandblockStreamResult.Promoted promoted:
|
case LandblockStreamResult.Promoted promoted:
|
||||||
// PromoteToNear carries a complete build and mesh because the
|
// PromoteToNear carries a complete build and mesh because the
|
||||||
// streamer deliberately lets it supersede a queued LoadFar. If
|
// streamer deliberately lets it supersede a queued LoadFar. If
|
||||||
// that Far job never started, publish this as the real Near
|
// that Far job never started, publish this as the real Near
|
||||||
// landblock; if the base is already resident, merge only the
|
// landblock; if the base is already resident, merge only the
|
||||||
// Near layer so existing live projections retain identity.
|
// Near layer so existing live projections retain identity.
|
||||||
_presentation.PublishPromoted(
|
return _presentation.PublishPromoted(
|
||||||
promoted,
|
promoted,
|
||||||
mergeIntoExistingLandblock: _state.IsLoaded(promoted.LandblockId));
|
mergeIntoExistingLandblock:
|
||||||
break;
|
_state.IsLoaded(promoted.LandblockId),
|
||||||
|
work.Estimate,
|
||||||
|
meter,
|
||||||
|
ensureProgress);
|
||||||
case LandblockStreamResult.Unloaded unloaded:
|
case LandblockStreamResult.Unloaded unloaded:
|
||||||
_presentation.EnqueueFullRetirement(unloaded.LandblockId);
|
return RunSimpleResultOperation(
|
||||||
break;
|
meter,
|
||||||
|
"execute-unload",
|
||||||
|
ensureProgress,
|
||||||
|
() => _presentation.EnqueueFullRetirement(
|
||||||
|
unloaded.LandblockId));
|
||||||
case LandblockStreamResult.Failed failed:
|
case LandblockStreamResult.Failed failed:
|
||||||
Console.WriteLine(
|
return RunSimpleResultOperation(
|
||||||
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}");
|
meter,
|
||||||
break;
|
"execute-load-failure",
|
||||||
|
ensureProgress,
|
||||||
|
() => Console.WriteLine(
|
||||||
|
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}"));
|
||||||
case LandblockStreamResult.WorkerCrashed crashed:
|
case LandblockStreamResult.WorkerCrashed crashed:
|
||||||
Console.WriteLine(
|
return RunSimpleResultOperation(
|
||||||
$"streaming: worker CRASHED: {crashed.Error}");
|
meter,
|
||||||
break;
|
"execute-worker-crash",
|
||||||
|
ensureProgress,
|
||||||
|
() => Console.WriteLine(
|
||||||
|
$"streaming: worker CRASHED: {crashed.Error}"));
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Unsupported streaming result {result.GetType().Name}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,18 @@ public sealed class ShadowObjectRegistry
|
||||||
/// cells hydrated after a server spawn landed.
|
/// cells hydrated after a server spawn landed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RefloodLandblock(uint landblockId)
|
public void RefloodLandblock(uint landblockId)
|
||||||
|
{
|
||||||
|
uint[] owners = CaptureRefloodOwnersForLandblock(landblockId);
|
||||||
|
for (int i = 0; i < owners.Length; i++)
|
||||||
|
RefloodOwnerForLandblock(owners[i], landblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures the stable ordered owner set touched by one landblock reflood.
|
||||||
|
/// App-layer streaming can retain this receipt and advance one owner per
|
||||||
|
/// frame without changing the collision registry's ownership rules.
|
||||||
|
/// </summary>
|
||||||
|
public uint[] CaptureRefloodOwnersForLandblock(uint landblockId)
|
||||||
{
|
{
|
||||||
uint lbPrefix = landblockId & 0xFFFF0000u;
|
uint lbPrefix = landblockId & 0xFFFF0000u;
|
||||||
var toReflood = new HashSet<uint>();
|
var toReflood = new HashSet<uint>();
|
||||||
|
|
@ -487,39 +499,81 @@ public sealed class ShadowObjectRegistry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (uint entityId in toReflood)
|
uint[] ordered = toReflood.ToArray();
|
||||||
|
Array.Sort(ordered);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-runs one owner from a retained landblock-reflood receipt. A removed,
|
||||||
|
/// suspended, or otherwise superseded owner is an idempotent no-op.
|
||||||
|
/// </summary>
|
||||||
|
public void RefloodOwnerForLandblock(uint entityId, uint landblockId)
|
||||||
|
{
|
||||||
|
uint lbPrefix = landblockId & 0xFFFF0000u;
|
||||||
|
if (_suspendedEntities.Contains(entityId)
|
||||||
|
|| !_entityReg.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out RegistrationRecord? reg))
|
||||||
{
|
{
|
||||||
var reg = _entityReg[entityId];
|
return;
|
||||||
_withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawnBeforeReflood);
|
}
|
||||||
if (reg.IsMultiPart && _entityShapes.TryGetValue(entityId, out var shapes))
|
|
||||||
{
|
|
||||||
RegisterMultiPart(entityId, reg.EntityWorldPos, reg.EntityWorldRot, shapes,
|
|
||||||
reg.State, reg.Flags, 0f, 0f, lbPrefix,
|
|
||||||
reg.SeedCellId, reg.IsStatic);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Register(entityId, reg.GfxObjId, reg.EntityWorldPos, reg.EntityWorldRot,
|
|
||||||
reg.Radius, 0f, 0f, lbPrefix,
|
|
||||||
reg.CollisionType, reg.CylHeight, reg.Scale,
|
|
||||||
reg.State, reg.Flags, reg.SeedCellId, reg.IsStatic);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register is also the authoritative movement/replacement API and
|
_withdrawnPrefixesByOwner.TryGetValue(
|
||||||
// therefore clears obsolete markers. Only this streaming reflood
|
entityId,
|
||||||
// operation preserves the still-missing prefixes across replacement.
|
out var withdrawnBeforeReflood);
|
||||||
if (withdrawnBeforeReflood is not null)
|
if (reg.IsMultiPart
|
||||||
_withdrawnPrefixesByOwner[entityId] = withdrawnBeforeReflood;
|
&& _entityShapes.TryGetValue(entityId, out var shapes))
|
||||||
|
{
|
||||||
|
RegisterMultiPart(
|
||||||
|
entityId,
|
||||||
|
reg.EntityWorldPos,
|
||||||
|
reg.EntityWorldRot,
|
||||||
|
shapes,
|
||||||
|
reg.State,
|
||||||
|
reg.Flags,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
lbPrefix,
|
||||||
|
reg.SeedCellId,
|
||||||
|
reg.IsStatic);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Register(
|
||||||
|
entityId,
|
||||||
|
reg.GfxObjId,
|
||||||
|
reg.EntityWorldPos,
|
||||||
|
reg.EntityWorldRot,
|
||||||
|
reg.Radius,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
lbPrefix,
|
||||||
|
reg.CollisionType,
|
||||||
|
reg.CylHeight,
|
||||||
|
reg.Scale,
|
||||||
|
reg.State,
|
||||||
|
reg.Flags,
|
||||||
|
reg.SeedCellId,
|
||||||
|
reg.IsStatic);
|
||||||
|
}
|
||||||
|
|
||||||
if (_entityToCells.TryGetValue(entityId, out var refreshedCells)
|
// Register is also the authoritative movement/replacement API and
|
||||||
&& refreshedCells.Exists(cell =>
|
// therefore clears obsolete markers. Only this streaming reflood
|
||||||
(cell & 0xFFFF0000u) == lbPrefix)
|
// operation preserves the still-missing prefixes across replacement.
|
||||||
&& _withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawn))
|
if (withdrawnBeforeReflood is not null)
|
||||||
{
|
_withdrawnPrefixesByOwner[entityId] = withdrawnBeforeReflood;
|
||||||
withdrawn.Remove(lbPrefix);
|
|
||||||
if (withdrawn.Count == 0)
|
if (_entityToCells.TryGetValue(entityId, out var refreshedCells)
|
||||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
&& refreshedCells.Exists(cell =>
|
||||||
}
|
(cell & 0xFFFF0000u) == lbPrefix)
|
||||||
|
&& _withdrawnPrefixesByOwner.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out var withdrawn))
|
||||||
|
{
|
||||||
|
withdrawn.Remove(lbPrefix);
|
||||||
|
if (withdrawn.Count == 0)
|
||||||
|
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,6 +650,17 @@ public sealed class ShadowObjectRegistry
|
||||||
/// are deliberately retained for spatial reflood after streaming changes.
|
/// are deliberately retained for spatial reflood after streaming changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DeregisterStaticOwnersForLandblock(uint landblockId)
|
public void DeregisterStaticOwnersForLandblock(uint landblockId)
|
||||||
|
{
|
||||||
|
uint[] owners = CaptureStaticOwnersForLandblock(landblockId);
|
||||||
|
for (int i = 0; i < owners.Length; i++)
|
||||||
|
DeregisterStaticOwnerForLandblock(owners[i], landblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures a stable ordered receipt for static collision owners rooted in
|
||||||
|
/// one landblock.
|
||||||
|
/// </summary>
|
||||||
|
public uint[] CaptureStaticOwnersForLandblock(uint landblockId)
|
||||||
{
|
{
|
||||||
uint prefix = landblockId & 0xFFFF0000u;
|
uint prefix = landblockId & 0xFFFF0000u;
|
||||||
var owners = new List<uint>();
|
var owners = new List<uint>();
|
||||||
|
|
@ -607,9 +672,27 @@ public sealed class ShadowObjectRegistry
|
||||||
owners.Add(entityId);
|
owners.Add(entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
owners.Sort();
|
||||||
|
return owners.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (uint entityId in owners)
|
/// <summary>
|
||||||
|
/// Removes one static owner from a retained landblock receipt. If the
|
||||||
|
/// owner was already removed or rebound elsewhere, the operation no-ops.
|
||||||
|
/// </summary>
|
||||||
|
public void DeregisterStaticOwnerForLandblock(
|
||||||
|
uint entityId,
|
||||||
|
uint landblockId)
|
||||||
|
{
|
||||||
|
uint prefix = landblockId & 0xFFFF0000u;
|
||||||
|
if (_entityReg.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out RegistrationRecord? registration)
|
||||||
|
&& registration.IsStatic
|
||||||
|
&& (registration.SeedCellId & 0xFFFF0000u) == prefix)
|
||||||
|
{
|
||||||
Deregister(entityId);
|
Deregister(entityId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -86,6 +87,43 @@ public class BuildingLoaderTests
|
||||||
Assert.Equal(new SortedSet<uint> { 1, 2 }, ids); // sequential 1, 2
|
Assert.Equal(new SortedSet<uint> { 1, 2 }, ids); // sequential 1, 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetainedPublication_AdvancesOneBuildingAndCommitsCellStampsAtomically()
|
||||||
|
{
|
||||||
|
LandBlockInfo info = MakeInfo(
|
||||||
|
(0x02000001u, new[] { 0x0150u }),
|
||||||
|
(0x02000002u, new[] { 0x0160u }));
|
||||||
|
LoadedCell cell150 = MinimalCell(0xA9B40150u);
|
||||||
|
LoadedCell cell160 = MinimalCell(0xA9B40160u);
|
||||||
|
var cells = new Dictionary<uint, LoadedCell>
|
||||||
|
{
|
||||||
|
[cell150.CellId] = cell150,
|
||||||
|
[cell160.CellId] = cell160,
|
||||||
|
};
|
||||||
|
|
||||||
|
BuildingRegistryPublication publication =
|
||||||
|
BuildingLoader.PreparePublication(info, 0xA9B40000u, cells);
|
||||||
|
|
||||||
|
Assert.False(publication.PreparationCommitted);
|
||||||
|
Assert.False(BuildingLoader.AdvancePreparationOne(publication));
|
||||||
|
Assert.Equal(1, publication.BuildingCursor);
|
||||||
|
Assert.Equal(1, publication.Registry.Count);
|
||||||
|
Assert.Null(cell150.BuildingId);
|
||||||
|
Assert.Null(cell160.BuildingId);
|
||||||
|
|
||||||
|
Assert.True(BuildingLoader.AdvancePreparationOne(publication));
|
||||||
|
Assert.Equal(2, publication.BuildingCursor);
|
||||||
|
Assert.Equal(2, publication.Registry.Count);
|
||||||
|
Assert.Null(cell150.BuildingId);
|
||||||
|
Assert.Null(cell160.BuildingId);
|
||||||
|
|
||||||
|
BuildingLoader.CommitPublication(publication);
|
||||||
|
|
||||||
|
Assert.Equal(1u, cell150.BuildingId);
|
||||||
|
Assert.Equal(2u, cell160.BuildingId);
|
||||||
|
Assert.True(publication.PublicationCommitted);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_StampsLoadedCellBuildingId()
|
public void Build_StampsLoadedCellBuildingId()
|
||||||
{
|
{
|
||||||
|
|
@ -166,4 +204,16 @@ public class BuildingLoaderTests
|
||||||
Assert.Equal(new Vector3(9, 12, 33), b.PortalBounds.Min);
|
Assert.Equal(new Vector3(9, 12, 33), b.PortalBounds.Min);
|
||||||
Assert.Equal(new Vector3(17, 25, 39), b.PortalBounds.Max);
|
Assert.Equal(new Vector3(17, 25, 39), b.PortalBounds.Max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LoadedCell MinimalCell(uint cellId) => new()
|
||||||
|
{
|
||||||
|
CellId = cellId,
|
||||||
|
Portals = new List<AcDream.App.Rendering.CellPortalInfo>(),
|
||||||
|
PortalPolygons = new List<Vector3[]>(),
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
LocalBoundsMin = new Vector3(-5, -5, -5),
|
||||||
|
LocalBoundsMax = new Vector3(5, 5, 5),
|
||||||
|
ClipPlanes = new List<AcDream.App.Rendering.PortalClipPlane>(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,73 @@ public sealed class LandblockConcretePresentationPipelineTests
|
||||||
Assert.Equal(1, fixture.Static.Diagnostics.CompleteCount);
|
Assert.Equal(1, fixture.Static.Diagnostics.CompleteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MeteredLoaded_RetainsExactHeadAndPublishesSpatiallyOnlyAfterOwnerSuffix()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
ConcreteFixture fixture = Fixture(
|
||||||
|
calls,
|
||||||
|
commitEnvCells: _ => calls.Add("envcell"));
|
||||||
|
fixture.Events.EntitySpawned += _ => calls.Add("plugin");
|
||||||
|
var pipeline = new LandblockPresentationPipeline(
|
||||||
|
fixture.Render,
|
||||||
|
fixture.Physics,
|
||||||
|
fixture.Static,
|
||||||
|
fixture.State,
|
||||||
|
fixture.RetirementOwner,
|
||||||
|
onLandblockLoaded: _ => calls.Add("live-recovery"));
|
||||||
|
LandblockStreamResult.Loaded result =
|
||||||
|
Result(Build(Entity(0x80A9B401u), Entity(0x80A9B402u)));
|
||||||
|
LandblockStreamCostEstimate estimate =
|
||||||
|
LandblockStreamResultCost.Estimate(result);
|
||||||
|
var budget = new StreamingWorkBudget(
|
||||||
|
TimeSpan.FromSeconds(1),
|
||||||
|
maxCompletionAdmissions: 64,
|
||||||
|
maxAdoptedCpuBytes: 1_000_000,
|
||||||
|
maxEntityOperations: 1,
|
||||||
|
maxGpuUploadBytes: 1_000_000,
|
||||||
|
maxGlRetireOperations: 64,
|
||||||
|
destinationReserveFraction: 0.75f);
|
||||||
|
|
||||||
|
LandblockPublicationAdvance advance = default;
|
||||||
|
for (int frame = 0; frame < 64; frame++)
|
||||||
|
{
|
||||||
|
var meter = new StreamingWorkMeter(budget);
|
||||||
|
advance = frame == 0
|
||||||
|
? pipeline.PublishLoaded(
|
||||||
|
result,
|
||||||
|
estimate,
|
||||||
|
meter,
|
||||||
|
ensureProgress: false)
|
||||||
|
: pipeline.ResumePublication(
|
||||||
|
result,
|
||||||
|
meter,
|
||||||
|
ensureProgress: false);
|
||||||
|
meter.FinishFrame();
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
meter.Snapshot.Used.EntityOperations <= 1,
|
||||||
|
$"stage={meter.Snapshot.LastStage}, " +
|
||||||
|
$"entities={meter.Snapshot.Used.EntityOperations}, " +
|
||||||
|
$"oversized={meter.Snapshot.OversizedProgressCount}");
|
||||||
|
if (fixture.Static.Diagnostics.CompleteCount == 0)
|
||||||
|
Assert.False(fixture.State.IsLoaded(LandblockId));
|
||||||
|
if (advance.Completed)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(advance.Completed);
|
||||||
|
Assert.False(pipeline.HasPendingPublication(result));
|
||||||
|
Assert.True(fixture.State.IsNearTier(LandblockId));
|
||||||
|
Assert.Equal(
|
||||||
|
["terrain", "envcell", "plugin", "plugin", "pin", "live-recovery"],
|
||||||
|
calls);
|
||||||
|
Assert.Equal(1, fixture.Render.Diagnostics.BeginCount);
|
||||||
|
Assert.Equal(1, fixture.Physics.Diagnostics.BeginCount);
|
||||||
|
Assert.Equal(1, fixture.Physics.Diagnostics.CompleteCount);
|
||||||
|
Assert.Equal(1, fixture.Static.Diagnostics.CompleteCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RenderSuffixFailure_ResumesConcreteReceiptsWithoutReplayingPrefixes()
|
public void RenderSuffixFailure_ResumesConcreteReceiptsWithoutReplayingPrefixes()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1119,7 +1119,15 @@ public sealed class LandblockPresentationPipelineTests
|
||||||
failReplayOnce = false;
|
failReplayOnce = false;
|
||||||
throw new InvalidOperationException("injected replay failure");
|
throw new InvalidOperationException("injected replay failure");
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||||
|
MaxUpdateMilliseconds: 100,
|
||||||
|
MaxCompletionAdmissions: 64,
|
||||||
|
MaxAdoptedCpuBytes: 1_000_000,
|
||||||
|
MaxEntityOperations: 1_000,
|
||||||
|
MaxGpuUploadBytes: 1_000_000,
|
||||||
|
MaxGlRetireOperations: 1_000,
|
||||||
|
DestinationReserveFraction: 0.75f));
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x90, 0x90));
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x90, 0x90));
|
||||||
Assert.True(state.IsNearTier(landblockId));
|
Assert.True(state.IsNearTier(landblockId));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
|
|
@ -194,6 +195,47 @@ public sealed class LandblockRenderPublisherTests
|
||||||
Assert.Equal(1, first.Diagnostics.CompleteCount);
|
Assert.Equal(1, first.Diagnostics.CompleteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetainedEnvCellPublisher_AdvancesOneShellBeforeAtomicCommit()
|
||||||
|
{
|
||||||
|
var envPublisher = new RecordingEnvCellPublisher();
|
||||||
|
EnvCellLandblockBuild envCells = new(
|
||||||
|
LandblockId,
|
||||||
|
Array.Empty<LoadedCell>(),
|
||||||
|
[
|
||||||
|
Shell(0xA9B40100u, 1),
|
||||||
|
Shell(0xA9B40101u, 2),
|
||||||
|
Shell(0xA9B40102u, 3),
|
||||||
|
]);
|
||||||
|
var publisher = new LandblockRenderPublisher(
|
||||||
|
publishTerrain: static (_, _, _) => { },
|
||||||
|
removeTerrain: static _ => { },
|
||||||
|
cellVisibility: new CellVisibility(),
|
||||||
|
worldState: new GpuWorldState(),
|
||||||
|
envCellPublisher: envPublisher);
|
||||||
|
LandblockRenderPublication receipt = publisher.PreparePublication(
|
||||||
|
Build(envCells),
|
||||||
|
EmptyMesh());
|
||||||
|
publisher.BeginPublication(receipt);
|
||||||
|
|
||||||
|
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||||
|
Assert.Equal(0, envPublisher.AdvancedShells);
|
||||||
|
Assert.Equal(0, envPublisher.CommitCount);
|
||||||
|
for (int shell = 1; shell <= 3; shell++)
|
||||||
|
{
|
||||||
|
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||||
|
Assert.Equal(shell, envPublisher.AdvancedShells);
|
||||||
|
Assert.Equal(0, envPublisher.CommitCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||||
|
Assert.Equal(0, envPublisher.CommitCount);
|
||||||
|
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||||
|
Assert.Equal(1, envPublisher.CommitCount);
|
||||||
|
Assert.True(publisher.AdvanceCompleteOne(receipt));
|
||||||
|
Assert.Equal(1, publisher.Diagnostics.CompleteCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PrepareAndRetirementOperationsStayBalancedAtTheirOwnerBoundary()
|
public void PrepareAndRetirementOperationsStayBalancedAtTheirOwnerBoundary()
|
||||||
{
|
{
|
||||||
|
|
@ -341,6 +383,19 @@ public sealed class LandblockRenderPublisherTests
|
||||||
InverseWorldTransform = Matrix4x4.Identity,
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static EnvCellShellPlacement Shell(uint cellId, ulong geometryId) =>
|
||||||
|
new(
|
||||||
|
cellId,
|
||||||
|
geometryId,
|
||||||
|
EnvironmentId: 1u,
|
||||||
|
CellStructure: 0,
|
||||||
|
Surfaces: ImmutableArray<ushort>.Empty,
|
||||||
|
WorldPosition: Vector3.Zero,
|
||||||
|
Rotation: Quaternion.Identity,
|
||||||
|
Transform: Matrix4x4.Identity,
|
||||||
|
LocalBounds: new WbBoundingBox(Vector3.Zero, Vector3.One),
|
||||||
|
WorldBounds: new WbBoundingBox(Vector3.Zero, Vector3.One));
|
||||||
|
|
||||||
private static LandblockMeshData MeshAtHeights(float first, float second) => new(
|
private static LandblockMeshData MeshAtHeights(float first, float second) => new(
|
||||||
[
|
[
|
||||||
new TerrainVertex(new Vector3(0f, 0f, first), Vector3.UnitZ, 0, 0, 0, 0),
|
new TerrainVertex(new Vector3(0f, 0f, first), Vector3.UnitZ, 0, 0, 0, 0),
|
||||||
|
|
@ -362,4 +417,42 @@ public sealed class LandblockRenderPublisherTests
|
||||||
}
|
}
|
||||||
throw new DirectoryNotFoundException("Could not locate repository root.");
|
throw new DirectoryNotFoundException("Could not locate repository root.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingEnvCellPublisher :
|
||||||
|
IEnvCellLandblockPublisher
|
||||||
|
{
|
||||||
|
private readonly object _owner = new();
|
||||||
|
|
||||||
|
public int AdvancedShells { get; private set; }
|
||||||
|
public int CommitCount { get; private set; }
|
||||||
|
|
||||||
|
public EnvCellLandblockPublication PreparePublication(
|
||||||
|
EnvCellLandblockBuild build) =>
|
||||||
|
new(_owner, build);
|
||||||
|
|
||||||
|
public bool AdvancePreparationOne(
|
||||||
|
EnvCellLandblockPublication publication)
|
||||||
|
{
|
||||||
|
Assert.Same(_owner, publication.Owner);
|
||||||
|
if (publication.PreparationCommitted)
|
||||||
|
return true;
|
||||||
|
if (publication.ShellCursor < publication.Build.Shells.Length)
|
||||||
|
{
|
||||||
|
publication.ShellCursor++;
|
||||||
|
AdvancedShells++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CommitPublication(
|
||||||
|
EnvCellLandblockPublication publication)
|
||||||
|
{
|
||||||
|
Assert.True(publication.PreparationCommitted);
|
||||||
|
publication.PublicationCommitted = true;
|
||||||
|
CommitCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,7 +507,16 @@ public sealed class LandblockRetirementCoordinatorTests
|
||||||
nearRadius: 0,
|
nearRadius: 0,
|
||||||
farRadius: 0,
|
farRadius: 0,
|
||||||
retirementCoordinator: coordinator,
|
retirementCoordinator: coordinator,
|
||||||
workBudgetOptions: workBudgetOptions);
|
workBudgetOptions: workBudgetOptions ?? GenerousWorkBudget());
|
||||||
|
|
||||||
|
private static StreamingWorkBudgetOptions GenerousWorkBudget() => new(
|
||||||
|
MaxUpdateMilliseconds: 100,
|
||||||
|
MaxCompletionAdmissions: 64,
|
||||||
|
MaxAdoptedCpuBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||||
|
MaxEntityOperations: 1_024,
|
||||||
|
MaxGpuUploadBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||||
|
MaxGlRetireOperations: 256,
|
||||||
|
DestinationReserveFraction: 0.75f);
|
||||||
|
|
||||||
private static IReadOnlyList<LandblockStreamResult> Drain(
|
private static IReadOnlyList<LandblockStreamResult> Drain(
|
||||||
Queue<LandblockStreamResult> pending,
|
Queue<LandblockStreamResult> pending,
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,8 @@ public sealed class StreamingControllerReadinessTests
|
||||||
ensureCalls++;
|
ensureCalls++;
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.Tick(0x12, 0x36);
|
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
|
||||||
|
controller.Tick(0x12, 0x36);
|
||||||
|
|
||||||
Assert.Equal(1, ensureCalls);
|
Assert.Equal(1, ensureCalls);
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +295,8 @@ public sealed class StreamingControllerReadinessTests
|
||||||
ensureCalls++;
|
ensureCalls++;
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.Tick(0x12, 0x36);
|
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
|
||||||
|
controller.Tick(0x12, 0x36);
|
||||||
|
|
||||||
Assert.Equal(1, ensureCalls);
|
Assert.Equal(1, ensureCalls);
|
||||||
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
|
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ public sealed class StreamingFrameControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StreamingPublicationMakesSameFrameInboundProjectionResident()
|
public void CompletedStreamingPublicationMakesInboundProjectionResident()
|
||||||
{
|
{
|
||||||
const uint landblock = 0x0A14FFFFu;
|
const uint landblock = 0x0A14FFFFu;
|
||||||
const uint cell = 0x0A140001u;
|
const uint cell = 0x0A140001u;
|
||||||
|
|
@ -359,7 +359,8 @@ public sealed class StreamingFrameControllerTests
|
||||||
farRadius: 0);
|
farRadius: 0);
|
||||||
var fixture = new Fixture(streamingBackend: streaming);
|
var fixture = new Fixture(streamingBackend: streaming);
|
||||||
|
|
||||||
fixture.Controller.Tick();
|
for (int frame = 0; frame < 32 && !state.IsLoaded(landblock); frame++)
|
||||||
|
fixture.Controller.Tick();
|
||||||
Assert.True(state.IsLoaded(landblock));
|
Assert.True(state.IsLoaded(landblock));
|
||||||
|
|
||||||
var runtime = Runtime(state);
|
var runtime = Runtime(state);
|
||||||
|
|
|
||||||
|
|
@ -347,8 +347,8 @@ public sealed class StreamingWorkBudgetTests
|
||||||
Assert.Equal(1, first.DeferredCompletions);
|
Assert.Equal(1, first.DeferredCompletions);
|
||||||
Assert.Equal(44, first.DeferredAdoptedCpuBytes);
|
Assert.Equal(44, first.DeferredAdoptedCpuBytes);
|
||||||
Assert.True(first.OldestDeferredAgeMilliseconds >= 0);
|
Assert.True(first.OldestDeferredAgeMilliseconds >= 0);
|
||||||
Assert.Equal(3, first.LastFrame.Operations);
|
Assert.Equal(6, first.LastFrame.Operations);
|
||||||
Assert.Equal(3, first.LastFrame.CompletedOperations);
|
Assert.Equal(6, first.LastFrame.CompletedOperations);
|
||||||
Assert.Equal(1, first.LastFrame.YieldCount);
|
Assert.Equal(1, first.LastFrame.YieldCount);
|
||||||
Assert.Equal(88, first.LastFrame.Used.AdoptedCpuBytes);
|
Assert.Equal(88, first.LastFrame.Used.AdoptedCpuBytes);
|
||||||
|
|
||||||
|
|
@ -358,7 +358,7 @@ public sealed class StreamingWorkBudgetTests
|
||||||
Assert.Equal(0, second.DeferredCompletions);
|
Assert.Equal(0, second.DeferredCompletions);
|
||||||
Assert.Equal(0, second.DeferredAdoptedCpuBytes);
|
Assert.Equal(0, second.DeferredAdoptedCpuBytes);
|
||||||
Assert.Equal(0, second.OldestDeferredAgeMilliseconds);
|
Assert.Equal(0, second.OldestDeferredAgeMilliseconds);
|
||||||
Assert.Equal(1, second.LastFrame.CompletedOperations);
|
Assert.Equal(4, second.LastFrame.CompletedOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -409,7 +409,7 @@ public sealed class StreamingWorkBudgetTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DestinationAndUnloadPriorityNeverBypassExecutionBudgets()
|
public void DestinationAndEmptyUnloadPriorityNeverBypassPublicationBudget()
|
||||||
{
|
{
|
||||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||||
uint ordinary = StreamingRegion.EncodeLandblockIdForTest(33, 33);
|
uint ordinary = StreamingRegion.EncodeLandblockIdForTest(33, 33);
|
||||||
|
|
@ -432,16 +432,16 @@ public sealed class StreamingWorkBudgetTests
|
||||||
controller.Tick(32, 32);
|
controller.Tick(32, 32);
|
||||||
|
|
||||||
Assert.Equal([center], applied);
|
Assert.Equal([center], applied);
|
||||||
Assert.Equal(2, controller.WorkDiagnostics.DeferredCompletions);
|
Assert.Equal(1, controller.WorkDiagnostics.DeferredCompletions);
|
||||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.YieldCount);
|
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.YieldCount);
|
||||||
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
||||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||||
|
|
||||||
controller.Tick(32, 32);
|
controller.Tick(32, 32);
|
||||||
|
|
||||||
Assert.Equal([center, ordinary], applied);
|
Assert.Equal([center, ordinary], applied);
|
||||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,65 @@ public class ShadowObjectRegistryTests
|
||||||
entry => entry.EntityId == entityId);
|
entry => entry.EntityId == entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StreamingReceipts_AreStableOrderedAndAdvanceOneStaticOwner()
|
||||||
|
{
|
||||||
|
var reg = new ShadowObjectRegistry();
|
||||||
|
reg.Register(
|
||||||
|
9u,
|
||||||
|
0x01000009u,
|
||||||
|
new Vector3(12f, 12f, 50f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
1f,
|
||||||
|
OffX,
|
||||||
|
OffY,
|
||||||
|
LbId,
|
||||||
|
seedCellId: LbId | 1u,
|
||||||
|
isStatic: true);
|
||||||
|
reg.Register(
|
||||||
|
2u,
|
||||||
|
0x01000002u,
|
||||||
|
new Vector3(14f, 12f, 50f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
1f,
|
||||||
|
OffX,
|
||||||
|
OffY,
|
||||||
|
LbId,
|
||||||
|
seedCellId: LbId | 1u,
|
||||||
|
isStatic: true);
|
||||||
|
reg.Register(
|
||||||
|
5u,
|
||||||
|
0x01000005u,
|
||||||
|
new Vector3(16f, 12f, 50f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
1f,
|
||||||
|
OffX,
|
||||||
|
OffY,
|
||||||
|
LbId,
|
||||||
|
seedCellId: LbId | 1u,
|
||||||
|
isStatic: false);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[2u, 9u],
|
||||||
|
reg.CaptureStaticOwnersForLandblock(LbId));
|
||||||
|
Assert.Equal(
|
||||||
|
[2u, 5u, 9u],
|
||||||
|
reg.CaptureRefloodOwnersForLandblock(LbId));
|
||||||
|
|
||||||
|
reg.DeregisterStaticOwnerForLandblock(2u, LbId);
|
||||||
|
|
||||||
|
Assert.Equal(2, reg.RetainedRegistrationCount);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
reg.GetObjectsInCell(LbId | 1u),
|
||||||
|
entry => entry.EntityId == 2u);
|
||||||
|
Assert.Contains(
|
||||||
|
reg.GetObjectsInCell(LbId | 1u),
|
||||||
|
entry => entry.EntityId == 5u);
|
||||||
|
Assert.Contains(
|
||||||
|
reg.GetObjectsInCell(LbId | 1u),
|
||||||
|
entry => entry.EntityId == 9u);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RefloodLandblock_RestoresAdjacentOwnedFootprintWithdrawnByUnload()
|
public void RefloodLandblock_RestoresAdjacentOwnedFootprintWithdrawnByUnload()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,8 @@ public class StreamingControllerTests
|
||||||
System.Array.Empty<uint>());
|
System.Array.Empty<uint>());
|
||||||
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(landblockId, LandblockStreamTier.Near, lb, stubMesh));
|
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(landblockId, LandblockStreamTier.Near, lb, stubMesh));
|
||||||
|
|
||||||
controller.Tick(50, 50);
|
for (int frame = 0; frame < 32 && !state.IsLoaded(landblockId); frame++)
|
||||||
|
controller.Tick(50, 50);
|
||||||
|
|
||||||
Assert.Single(applied);
|
Assert.Single(applied);
|
||||||
Assert.True(state.IsLoaded(landblockId));
|
Assert.True(state.IsLoaded(landblockId));
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,15 @@ public class StreamingControllerTwoTierTests
|
||||||
},
|
},
|
||||||
state: state,
|
state: state,
|
||||||
nearRadius: 2,
|
nearRadius: 2,
|
||||||
farRadius: 2);
|
farRadius: 2,
|
||||||
|
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||||
|
MaxUpdateMilliseconds: 100,
|
||||||
|
MaxCompletionAdmissions: 64,
|
||||||
|
MaxAdoptedCpuBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||||
|
MaxEntityOperations: 512,
|
||||||
|
MaxGpuUploadBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||||
|
MaxGlRetireOperations: 128,
|
||||||
|
DestinationReserveFraction: 0.75f));
|
||||||
|
|
||||||
ctrl.Tick(50, 50); // drains the Promoted result
|
ctrl.Tick(50, 50); // drains the Promoted result
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue