perf(streaming): reserve destination reveal capacity
Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active. Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
98f1ac8934
commit
2ff8f844b0
33 changed files with 870 additions and 230 deletions
17
AGENTS.md
17
AGENTS.md
|
|
@ -115,8 +115,8 @@ and the final two-client portal-out/materialization observer flow are
|
|||
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||
Release builds; the appraisal formula-media/foreground correction passes
|
||||
3,557 App tests / 3 skips and 7,988 complete-solution tests / 5 skips.
|
||||
Release builds; the current performance-program checkpoint passes
|
||||
3,674 App tests / 3 skips and 8,158 complete-solution tests / 5 skips.
|
||||
|
||||
**M4 feature work order (paused while the authorized performance program
|
||||
executes):** the six-slice pre-M4 world-interaction completion
|
||||
|
|
@ -140,12 +140,15 @@ explicit bake/equivalence/UI-Studio tooling. Capped, uncapped, dense-Arwic,
|
|||
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
|
||||
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–E4 now
|
||||
typed residency policy/accounting and bounded retained caches. Slice E0–E5 now
|
||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||
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
|
||||
render/physics/static/building/EnvCell publication. The canonical reveal
|
||||
generation now owns one exact destination reservation across every typed
|
||||
budget dimension; incomplete portal destinations stay in the authored tunnel
|
||||
and receive retail's centered wait cue instead of forced early reveal. The
|
||||
physical console is available for E6 deterministic and connected closeout.
|
||||
Slices F/G
|
||||
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||
gameplay-owner gate. Evidence:
|
||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||
|
|
@ -170,7 +173,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
|||
truth remain correct.
|
||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E5+, and #225's
|
||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E6+, and #225's
|
||||
lifestone/particle alpha visual gate.
|
||||
|
||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||
|
|
|
|||
17
CLAUDE.md
17
CLAUDE.md
|
|
@ -113,8 +113,8 @@ and the final two-client portal-out/materialization observer flow are
|
|||
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||
Release builds; the appraisal formula-media/foreground correction passes
|
||||
3,557 App tests / 3 skips and 7,988 complete-solution tests / 5 skips.
|
||||
Release builds; the current performance-program checkpoint passes
|
||||
3,674 App tests / 3 skips and 8,158 complete-solution tests / 5 skips.
|
||||
|
||||
**M4 feature work order (paused while the authorized performance program
|
||||
executes):** the six-slice pre-M4 world-interaction completion
|
||||
|
|
@ -138,12 +138,15 @@ explicit bake/equivalence/UI-Studio tooling. Capped, uncapped, dense-Arwic,
|
|||
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
|
||||
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–E4 now
|
||||
typed residency policy/accounting and bounded retained caches. Slice E0–E5 now
|
||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||
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
|
||||
render/physics/static/building/EnvCell publication. The canonical reveal
|
||||
generation now owns one exact destination reservation across every typed
|
||||
budget dimension; incomplete portal destinations stay in the authored tunnel
|
||||
and receive retail's centered wait cue instead of forced early reveal. The
|
||||
physical console is available for E6 deterministic and connected closeout.
|
||||
Slices F/G
|
||||
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||
gameplay-owner gate. Evidence:
|
||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||
|
|
@ -168,7 +171,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
|||
truth remain correct.
|
||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E5+, and #225's
|
||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice E6+, and #225's
|
||||
lifestone/particle alpha visual gate.
|
||||
|
||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||
|
|
|
|||
|
|
@ -44,12 +44,14 @@ What does NOT go here:
|
|||
smooth and physics, collision, and wire state remain correct.
|
||||
- **Build hygiene:** `#228` records 17 clean-Release test-project warnings;
|
||||
production compilation and all tests pass.
|
||||
- **Performance side track:** Slices A–D and Slice E0–E4 are complete. Typed
|
||||
- **Performance side track:** Slices A–D and Slice E0–E5 are complete. Typed
|
||||
completion admission, immediate generation-scoped old-world quiescence, and
|
||||
budgeted full-window/recenter retirement are live. Publication now advances
|
||||
from retained render/physics/static/building/EnvCell cursors without replay;
|
||||
E5 destination reservation/reveal joining is next. The physical console is
|
||||
available and remains reserved for the E6 connected gate. Evidence:
|
||||
from retained render/physics/static/building/EnvCell cursors without replay.
|
||||
The canonical reveal generation now owns the destination reservation across
|
||||
every typed budget dimension; incomplete worlds remain in portal space with
|
||||
retail's centered wait cue. The physical console is available for E6
|
||||
deterministic and connected closeout. Evidence:
|
||||
[`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
|
||||
|
|
@ -3507,7 +3509,12 @@ Tests: `DungeonStreamingGateTests` (4), `TeleportArrivalRulesTests` (4). Registe
|
|||
|
||||
**Files:** `GameWindow.cs` `OnLivePositionUpdated` (drop stale center) + `TeleportArrivalReadiness`; `src/AcDream.App/World/TeleportArrivalController.cs` (`TeleportArrivalRules`); `src/AcDream.App/Streaming/DungeonStreamingGate.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`RemoveLandblock` CurrCell clear + NO-LANDBLOCK verbatim).
|
||||
|
||||
**Acceptance:** run into a portal, run out, re-enter — repeatedly in one session, each time placing + streaming correctly. Verify the dungeon→outdoor exit completes promptly (log `teleport complete`, not `teleport HOLD gave up … force-snapping`) with `streaming: dungeon EXIT-expand`, the outdoor world fully streamed, collision working. Also re-checks **#138**.
|
||||
**Acceptance:** run into a portal, run out, re-enter — repeatedly in one
|
||||
session, each time placing + streaming correctly. Verify the dungeon→outdoor
|
||||
exit remains in portal space until canonical destination readiness (with the
|
||||
centered retail wait cue if it exceeds five seconds), then completes with
|
||||
`streaming: dungeon EXIT-expand`, the outdoor world fully streamed, and
|
||||
collision working. Also re-checks **#138**.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ banner cannot silently override the current program state.
|
|||
[`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
|
||||
are complete.
|
||||
- **Current active engineering program:** Slices A–D and Slice E0–E4 of
|
||||
- **Current active engineering program:** Slices A–D and Slice E0–E5 of
|
||||
[`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
|
||||
prepared package, typed residency is bounded/accounted, and streaming now
|
||||
has typed admission plus immediate old-generation quiescence, metered
|
||||
retirement, and retained cursor-budgeted publication across render, physics,
|
||||
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
|
||||
statics, building registries, collision/reflood, and EnvCells. The canonical
|
||||
reveal generation now owns an exact destination reservation across every
|
||||
typed budget dimension; incomplete destinations remain in portal space and
|
||||
receive retail's centered wait cue instead of forced early reveal. The
|
||||
physical console is available for E6 deterministic/connected closeout. See
|
||||
the [Slice D residency report](research/2026-07-24-slice-d-unified-residency-report.md).
|
||||
- **Completed gates:** R6 locomotion/collision/projectile/teleport/radar and
|
||||
the final two-client portal-out/materialization observer comparison (`#218`).
|
||||
|
|
@ -34,7 +36,7 @@ banner cannot silently override the current program state.
|
|||
user decision gate; Linux/headless automation (Track LH) remains future work.
|
||||
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
|
||||
and UN 5 — plus the retained retired TS-37 history note.
|
||||
- **Latest automated baseline:** Release build succeeds; 8,152 tests pass and
|
||||
- **Latest automated baseline:** Release build succeeds; 8,158 tests pass and
|
||||
five are intentionally skipped. The capped, uncapped, and dense-Arwic
|
||||
physical prepared-asset routes all completed their portal/input/lifetime
|
||||
assertions and disconnected gracefully.
|
||||
|
|
|
|||
|
|
@ -717,10 +717,16 @@ single-consumer peek/read source so the update thread prices a result before
|
|||
adopting it. `StreamingController` is the sole scheduler: it admits through the
|
||||
typed meter into reusable destination/control/unload/Near/Far FIFOs, preserves
|
||||
exact reference identity and retry position, rejects stale generations, and
|
||||
executes every class through the same meter. Priority changes order, never the
|
||||
executes every class through the same meter. The canonical reveal generation
|
||||
supplies one exact destination cell/radius reservation; the meter protects its
|
||||
configured share across time, completion admissions, retained CPU bytes,
|
||||
entity operations, requested GPU bytes, and GL-retirement admissions. A
|
||||
completion retains the reveal generation that classified it, so a stale
|
||||
completion or teardown cannot consume or clear a replacement generation's
|
||||
reservation. Priority changes order and reserved share, never the global
|
||||
budget. Lifecycle artifacts publish worker and per-class backlog, retained
|
||||
bytes/age, yields, oversizes, overruns, and pending publication/retirement
|
||||
facts.
|
||||
bytes/age, per-lane work, yields, oversizes, overruns, and pending
|
||||
publication/retirement facts.
|
||||
|
||||
At a hard login/portal reveal edge, `WorldGenerationQuiescence` makes the old
|
||||
generation unavailable through the same `WorldRevealCoordinator` lifetime.
|
||||
|
|
@ -748,12 +754,16 @@ pending publication.
|
|||
|
||||
`WorldRevealCoordinator` is the single App-layer owner of each login or portal
|
||||
reveal lifetime. It composes the canonical `WorldRevealReadinessBarrier` with
|
||||
generation-scoped lifecycle observation, so begin, preparation, readiness,
|
||||
materialization, world visibility, completion, and cancellation cannot be
|
||||
wired independently. The barrier joins `StreamingController`/`GpuWorldState`
|
||||
generation-scoped lifecycle observation and the streaming destination
|
||||
reservation, so begin, preparation, readiness, materialization, world
|
||||
visibility, completion, cancellation, and reserved capacity cannot be wired
|
||||
independently. The barrier joins `StreamingController`/`GpuWorldState`
|
||||
static-mesh publication, `WbDrawDispatcher` composite-texture warmup, and
|
||||
`PhysicsEngine` destination residency. Login's sky-only gate and portal-space
|
||||
transit consume the same predicate; they differ only in presentation.
|
||||
transit consume the same predicate; they differ only in presentation. There is
|
||||
no timeout that reveals incomplete content: a delayed portal keeps its DAT
|
||||
tunnel live and, after five seconds, projects retail's centered
|
||||
`"In Portal Space - Please Wait..."` retained notice until readiness converges.
|
||||
Composite preparation advances after `WbMeshAdapter.Tick` on the render
|
||||
thread, never from the streaming worker. This is acdream's asynchronous equivalent of retail
|
||||
`SmartBox::UseTime` holding position completion while
|
||||
|
|
|
|||
|
|
@ -272,7 +272,9 @@ src/AcDream.App/
|
|||
│ └── SelectionInteractionController.cs # owns selection intents, Use/PickUp, auto-walk deferral
|
||||
├── Streaming/
|
||||
│ ├── LandblockPresentationPipeline.cs # DAT build/apply/retire transaction and resource publication
|
||||
│ └── ... # shipped streamer/world-state/reveal owners
|
||||
│ ├── StreamingController.cs # typed frame scheduler + reveal destination reservation
|
||||
│ ├── WorldRevealCoordinator.cs # canonical readiness, quiesce, and reservation lifetime
|
||||
│ └── ... # shipped streamer/world-state owners
|
||||
├── Input/ # (already exists)
|
||||
├── Audio/ # (already exists)
|
||||
└── Plugins/ # (already exists)
|
||||
|
|
@ -451,7 +453,7 @@ useful ordering seam, but its ownership status is **partial**.
|
|||
| World environment | **Complete ownership** | `WorldEnvironmentController` solely owns WorldTime, the loaded DAT sky, current day group, Weather, server synchronization, AdminEnvirons bridging, and debug time/weather cycles. `GameWindow` keeps ABI-compatible readonly aliases to the same clock/weather instances and composes one render source plus the two live-session callbacks. TS-54/TS-55 register the remaining centered UI sound and full fog/ambient/radar behavior gaps. |
|
||||
| Live identity/lifetime | **Complete App integration** | `LiveEntityRuntime` owns incarnation identity, accepted snapshots/timestamps, runtime components, and logical/spatial lifetime. `LiveEntityHydrationController`, `LiveEntityRuntimeTeardownController`, and `LiveEntityNetworkUpdateController` own DAT-backed hydration, retryable cleanup, and accepted Position/Vector/State/Movement presentation without a second GUID owner (`aa90c646`). |
|
||||
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
|
||||
| World reveal | **Complete** | `WorldRevealCoordinator` owns login/portal readiness and reveal lifetime (`a4ef5788`). The accepted deterministic lifecycle trace did not change after extraction. |
|
||||
| World reveal | **Complete** | `WorldRevealCoordinator` owns login/portal readiness, generation quiescence, and the exact streaming destination reservation. Login auto-entry and portal completion close only their active generation; stale completion cannot clear its replacement. Delayed portals retain the authored tunnel and centered retail wait notice rather than force-revealing incomplete content. |
|
||||
| Retained gameplay UI | **Mostly complete feature ownership** | `RetailUiRuntime` and focused panel/controllers own layout and behavior. `GameWindow.OnLoad` still performs substantial service composition, which is allowed until the final composition cleanup. |
|
||||
| Selection/interaction | **Complete** | `WorldSelectionQuery` owns read-only picking/classification/range queries; `SelectionInteractionController` owns selection intent, Use/PickUp transport, exact-incarnation queues, and auto-walk deferral; `ItemInteractionController` owns ItemHolder policy plus the shared retail inventory-request transaction. `GameWindow` retains construction and narrow lifecycle forwarding only. |
|
||||
| Landblock presentation | **Complete** | `LandblockBuildFactory` owns the captured-origin DAT transaction; concrete render/physics/DAT-static publishers and `LandblockPresentationPipeline` own publication and exact retryable retirement. `StreamingOriginRecenterCoordinator` serializes old-window retirement with teleport/session origin lifetimes. `GameWindow` retains construction and one pipeline field only (`c79d0a49`, closeout `4a205a3e`). |
|
||||
|
|
|
|||
|
|
@ -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-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. **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-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 required 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 unhydratable-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. **Slice E5 refinement (2026-07-24):** the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The 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/Streaming/StreamingController.cs`; `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs`; `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 remains in the authored tunnel and presents the centered wait cue after five seconds. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::UseTime` 0x004D6E30; `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-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) |
|
||||
|
|
@ -212,7 +212,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
|
||||
|
||||
| ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
|
||||
| AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
|
||||
| AP-115 | The DAT-authored portal-space viewport, animation `SoundTweakedHook`, and centered repeating `"In Portal Space - Please Wait..."` display string are live, but the separate `ClientUISystem` enter/exit sound enums are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs` | acdream has no ClientUISystem sound-table-enum resolver yet; inventing direct wave IDs would be less faithful. The notice uses the retained fullscreen UI rather than chat and remains tied to the portal presentation lifetime. | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, animation-authored sound, and centered wait notice, but lacks retail's short UI enter/exit cue sounds. | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
|
||||
| AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
||||
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
||||
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus `ready -> NonCombat`, or post-wield `NonCombat -> ready -> NonCombat`, causes one normal `ChangeCombatMode` request from the trailing notice. Explicit user combat input cancels settlement. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during primary-weapon replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later explicit combat request, replacement, or logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` |
|
||||
|
|
|
|||
|
|
@ -1613,11 +1613,14 @@ port in any phase — no separate listing here.
|
|||
> generation unavailable at the hard reveal edge and freezes its world
|
||||
> simulation/audio while full-window and shared-origin teardown advance through
|
||||
> metered resident, entity, and owner-stage cursors with exact retry receipts.
|
||||
> E4 now retains the exact accepted queue head and cursor-budgets render,
|
||||
> E4 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.
|
||||
> publication before one observer-atomic spatial swap. E5 now joins the
|
||||
> canonical reveal generation to an exact destination reservation, protects
|
||||
> its share across all typed budget dimensions, removes forced incomplete
|
||||
> materialization, and presents retail's centered five-second portal wait cue.
|
||||
> E6 deterministic pressure and physical-local closeout is next. F/G and J
|
||||
> retain their explicit approval gates.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||
umbrella design — read it first). **Goal:** smoothness first (no frame over
|
||||
|
|
|
|||
|
|
@ -93,13 +93,17 @@ The separately authorized modern-runtime performance program has completed
|
|||
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
||||
production streaming, and typed unified residency. Its physical capped,
|
||||
uncapped, and dense connected gates pass without a visual-quality change.
|
||||
Slice E0–E4 now define and enforce typed completion admission through explicit
|
||||
Slice E0–E5 now define and enforce typed completion admission through explicit
|
||||
stable priority queues, publish an immediate generation-scoped old-world
|
||||
quiesce at login/portal boundaries, and advance full-window/recenter teardown
|
||||
through exact metered retirement cursors. Render, physics, static, building,
|
||||
and EnvCell publication now resume from retained exact cursors under the same
|
||||
frame budget. E5 destination reservation and reveal-generation joining is
|
||||
next. Slices F/G remain behind their explicit ECS approval gate, and Slice J
|
||||
frame budget. The reveal coordinator now owns the exact destination
|
||||
generation/cell/radius reservation across every budget dimension; portal
|
||||
transit never force-reveals incomplete content and surfaces retail's centered
|
||||
wait cue after five seconds. E6 deterministic pressure and physical-local
|
||||
closeout 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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Date:** 2026-07-24
|
||||
|
||||
**Status:** Slices A–D complete. Slice E is active (E0–E4 complete).
|
||||
**Status:** Slices A–D complete. Slice E is active (E0–E5 complete; E6 gates next).
|
||||
|
||||
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
||||
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
||||
|
|
@ -729,9 +729,13 @@ checkpoint retained staged or retiring bytes. Evidence:
|
|||
**Checkpoint 2026-07-24:** E0 fixed the retail/adaptation contract and current
|
||||
owner inventory. E1 landed the validated work profile, deterministic meter and
|
||||
completion charge model. E2 cut execution over to stable priority FIFOs. E3
|
||||
added immediate generation quiescence and cursored retirement. E4 now advances
|
||||
render/physics/static publication from retained exact receipts; E5 destination
|
||||
reservation and reveal-generation joining is next.
|
||||
added immediate generation quiescence and cursored retirement. E4 advances
|
||||
render/physics/static publication from retained exact receipts. E5 replaced
|
||||
mutable priority-radius state with the reveal coordinator's exact
|
||||
generation-scoped destination reservation, protected its share across every
|
||||
typed budget dimension, removed forced early materialization, and added
|
||||
retail's centered five-second portal wait cue. E6 deterministic pressure and
|
||||
physical-local closeout is next.
|
||||
|
||||
**Purpose:** Remove update-thread portal transactions.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
|
||||
|
||||
**Status:** active — E0/E1/E2/E3/E4 complete; E5 destination reservation next
|
||||
**Status:** active — E0/E1/E2/E3/E4/E5 complete; E6 gates and closeout next
|
||||
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||
**Baseline:** Slice D closeout commit `66690805`
|
||||
**Behavior contract:** no visual-quality or retail-behavior reduction
|
||||
|
|
@ -356,6 +356,35 @@ performance and visual evidence remains an E6 gate.
|
|||
five seconds.
|
||||
- Remove the last count-only execution/bypass paths.
|
||||
|
||||
**Complete 2026-07-24.** `WorldRevealCoordinator` now owns one exact
|
||||
generation/cell/radius reservation from login or portal begin through complete
|
||||
or cancel. `StreamingController` no longer exposes a mutable priority
|
||||
landblock/radius pair: accepted work records the reveal generation that
|
||||
classified it, stale completion cannot consume a replacement generation's
|
||||
reserved lane, and stale teardown cannot clear the replacement reservation.
|
||||
The frame meter records destination and non-destination use independently and,
|
||||
while a reveal is active, protects the configured destination share across
|
||||
wall time, completion admissions, adopted CPU bytes, entity operations,
|
||||
requested GPU bytes, and GL-retirement admissions. Destination work still
|
||||
uses the same global ceilings and the existing first-indivisible-operation
|
||||
progress diagnostic; it does not regain a budget bypass.
|
||||
|
||||
The former ten-second forced materialization path is deleted. An incomplete
|
||||
destination remains in the authored portal scene until the canonical render,
|
||||
composite, and collision predicate is true. After five seconds the retained
|
||||
gameplay UI displays retail's centered
|
||||
`"In Portal Space - Please Wait..."` notice and lifecycle telemetry records the
|
||||
cue once; the tunnel continues its DAT animation and repeats the notice on the
|
||||
retail camera-rotation cadence. Login completion now ends its reveal
|
||||
reservation/quiescence at the same auto-entry edge that exposes the ready
|
||||
world.
|
||||
|
||||
Validation: 91 focused reservation/reveal/teleport/UI tests passed; the
|
||||
complete App suite passed 3,674 / 3 skipped; the complete Release solution
|
||||
passed 8,158 / 5 skipped; Release solution build green with zero warnings.
|
||||
The physical-local routes and final performance/resource assertions remain
|
||||
E6, not E5.
|
||||
|
||||
### E6 — Gates and closeout
|
||||
|
||||
- Deterministic pressure, reentrancy, failure, cancellation, stale-generation,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ Required asynchronous equivalent:
|
|||
```text
|
||||
WorldRevealReady(destination):
|
||||
if destination claim cannot be hydrated:
|
||||
return true so the existing loud forced-placement path diagnoses it
|
||||
return true so the loud unhydratable-placement path diagnoses it
|
||||
|
||||
if destination is indoor:
|
||||
return render center landblock Near-tier and ready
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
- `CObjectMaint::DestroyObjects @ 0x00508C30`
|
||||
- `CObjectMaint::AddObjectToBeDestroyed @ 0x00508F70`
|
||||
- `SmartBox::UseTime @ 0x00455410`
|
||||
- `gmSmartBoxUI::UseTime @ 0x004D6E30`
|
||||
|
||||
The Sept 2013 named pseudo-C is the behavioral oracle. WorldBuilder has no
|
||||
equivalent live-client teardown scheduler; its extracted mesh/cache owners are
|
||||
|
|
@ -125,6 +126,37 @@ object maintenance, physics, landscape, game time, or ambient sound. UI/event
|
|||
dispatch and the portal presentation continue. This is the retail basis for
|
||||
Slice E's immediate old-generation quiesce.
|
||||
|
||||
## `gmSmartBoxUI::UseTime` portal wait presentation
|
||||
|
||||
The portal `CreatureMode` remains live while `SmartBox` is blocked. Its camera
|
||||
continues one random roll segment at a time:
|
||||
|
||||
```text
|
||||
if now >= rotationStart + rotationDuration:
|
||||
currentAngle = rotationEndAngle
|
||||
rotationStart = now
|
||||
rotationDuration = RandomUniform(0.6, 1.8) seconds
|
||||
rotationStartAngle = currentAngle
|
||||
rotationEndAngle = RandomUniform(0, 360) degrees
|
||||
ECM_UI.SendNotice_DisplayStringInfo(
|
||||
channel = 0x1A,
|
||||
text = "In Portal Space - Please Wait...")
|
||||
else:
|
||||
level = UIGlobals.GetAnimLevel(
|
||||
(now - rotationStart) / rotationDuration)
|
||||
currentAngle = Lerp(
|
||||
rotationStartAngle,
|
||||
rotationEndAngle,
|
||||
level / 1024)
|
||||
|
||||
CreatureMode.SetCameraDirection_Degrees((0, currentAngle, 0))
|
||||
```
|
||||
|
||||
The notice is a centered UI display string, not chat. Slice E's asynchronous
|
||||
adaptation delays its first projection until the recorded five-second reveal
|
||||
stall threshold, then repeats it on the same rotation-segment boundary while
|
||||
the authored tunnel keeps animating.
|
||||
|
||||
## Acdream asynchronous adaptation
|
||||
|
||||
Retail performs each teardown synchronously and can synchronously block cell
|
||||
|
|
@ -156,6 +188,12 @@ reveal only when the one destination-generation readiness state proves:
|
|||
required Near scene ready
|
||||
required composite textures ready
|
||||
camera/player identity belongs to destination generation
|
||||
|
||||
if the portal reveal is still incomplete after five seconds:
|
||||
keep the DAT-authored portal scene animating
|
||||
display the centered retail wait notice
|
||||
emit one generation-scoped diagnostic
|
||||
do not materialize early
|
||||
```
|
||||
|
||||
Release-later never means run-later: an old owner may retain memory until its
|
||||
|
|
|
|||
|
|
@ -829,6 +829,20 @@ internal sealed class LivePresentationCompositionPhase
|
|||
"portal depth mask",
|
||||
() => new PortalDepthMaskRenderer(d.Gl),
|
||||
static value => value.Dispose());
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||
PortalWaitNoticeController>? portalWaitNoticeLease = null;
|
||||
if (interaction.RetainedUi is { } portalRetainedUi)
|
||||
{
|
||||
portalWaitNoticeLease = scope.Acquire(
|
||||
"portal wait notice",
|
||||
() => new PortalWaitNoticeController(
|
||||
portalRetainedUi.Host.Root),
|
||||
static value => value.Dispose());
|
||||
}
|
||||
Action<string?>? displayPortalWaitNotice =
|
||||
portalWaitNoticeLease is { } waitNoticeLease
|
||||
? waitNoticeLease.Resource.Set
|
||||
: null;
|
||||
PortalTunnelPresentation portalTunnel;
|
||||
try
|
||||
{
|
||||
|
|
@ -840,7 +854,9 @@ internal sealed class LivePresentationCompositionPhase
|
|||
d.HookRouter,
|
||||
dispatcherLease.Resource,
|
||||
foundation.SceneLighting,
|
||||
foundation.MeshAdapter),
|
||||
foundation.MeshAdapter,
|
||||
displayPortalWaitNotice,
|
||||
portalWaitNoticeLease?.Resource),
|
||||
static tunnel => tunnel.PrepareResources());
|
||||
}
|
||||
catch (Exception acquisitionFailure)
|
||||
|
|
@ -1006,6 +1022,7 @@ internal sealed class LivePresentationCompositionPhase
|
|||
clipFrameLease.Transfer();
|
||||
portalDepthLease.Transfer();
|
||||
portalTunnelLease.Transfer();
|
||||
portalWaitNoticeLease?.Transfer();
|
||||
skyLease.Transfer();
|
||||
skyShaderLease.Transfer();
|
||||
particleLease.Transfer();
|
||||
|
|
|
|||
|
|
@ -339,7 +339,8 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
|
||||
spawnClaimClassifier.IsUnhydratable,
|
||||
d.Log,
|
||||
worldQuiescence);
|
||||
worldQuiescence,
|
||||
streaming);
|
||||
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
|
||||
|
||||
return CompleteSessionPlayer(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,11 @@ internal sealed class LivePlayerModeAutoEntryContext
|
|||
|
||||
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
||||
|
||||
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
|
||||
public void EnterPlayerMode()
|
||||
{
|
||||
_playerMode.EnterFromAutoEntry();
|
||||
_worldReveal.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayerModeAutoEntry
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private readonly WorldEntity _entity;
|
||||
private readonly PortalTunnelCamera _camera = new();
|
||||
private readonly Random _random;
|
||||
private readonly Action<string>? _displayNotice;
|
||||
private readonly Action<string?>? _displayNotice;
|
||||
private IDisposable? _displayNoticeLifetime;
|
||||
private readonly PortalMeshReferenceOwner _meshReferences;
|
||||
|
||||
private bool _visible;
|
||||
|
|
@ -62,6 +63,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private float _rotationStartAngle;
|
||||
private float _rotationEndAngle;
|
||||
private float _rotationCurrentAngle;
|
||||
private bool _waitCueVisible;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposing;
|
||||
private bool _disposed;
|
||||
|
|
@ -77,7 +79,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
IAnimationLoader animationLoader,
|
||||
IAnimationHookSink hookSink,
|
||||
Random random,
|
||||
Action<string>? displayNotice)
|
||||
Action<string?>? displayNotice,
|
||||
IDisposable? displayNoticeLifetime)
|
||||
{
|
||||
_gl = gl;
|
||||
_dispatcher = dispatcher;
|
||||
|
|
@ -90,6 +93,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_sequence.HookObj = _animationHooks;
|
||||
_random = random;
|
||||
_displayNotice = displayNotice;
|
||||
_displayNoticeLifetime = displayNoticeLifetime;
|
||||
|
||||
_entity = new WorldEntity
|
||||
{
|
||||
|
|
@ -119,7 +123,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
Action<string>? displayNotice = null,
|
||||
Action<string?>? displayNotice = null,
|
||||
IDisposable? displayNoticeLifetime = null,
|
||||
Random? random = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
|
|
@ -154,7 +159,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
animationLoader,
|
||||
hookSink,
|
||||
random ?? Random.Shared,
|
||||
displayNotice);
|
||||
displayNotice,
|
||||
displayNoticeLifetime);
|
||||
}
|
||||
|
||||
internal static void EnsureRequiredAssets(
|
||||
|
|
@ -211,6 +217,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_rotationEndAngle = 0f;
|
||||
_rotationCurrentAngle = 0f;
|
||||
_camera.DirectionDegrees = 0f;
|
||||
SetWaitCue(false);
|
||||
_visible = true;
|
||||
RebuildPose();
|
||||
}
|
||||
|
|
@ -221,6 +228,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
_visible = false;
|
||||
SetWaitCue(false);
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
}
|
||||
|
|
@ -236,6 +244,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
TickRotation(dt);
|
||||
}
|
||||
|
||||
public void SetWaitCue(bool visible)
|
||||
{
|
||||
if (_waitCueVisible == visible)
|
||||
return;
|
||||
|
||||
_waitCueVisible = visible;
|
||||
_displayNotice?.Invoke(
|
||||
visible ? "In Portal Space - Please Wait..." : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw retail portal space into the active viewport. The caller suppresses
|
||||
/// the normal world viewport while this scene is visible, then draws the
|
||||
|
|
@ -298,7 +316,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_rotationDuration = NextDouble(RotationDurationMin, RotationDurationMax);
|
||||
_rotationStartAngle = _rotationCurrentAngle;
|
||||
_rotationEndAngle = (float)NextDouble(0.0, 360.0);
|
||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||
if (_waitCueVisible)
|
||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -389,11 +408,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
try
|
||||
{
|
||||
_visible = false;
|
||||
SetWaitCue(false);
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
_meshReferences.Dispose();
|
||||
if (_meshReferences.IsDisposed)
|
||||
{
|
||||
_displayNoticeLifetime?.Dispose();
|
||||
_displayNoticeLifetime = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -127,8 +127,6 @@ internal interface ILocalPlayerTeleportStreamingOperations
|
|||
bool BeginRecenter(int x, int y, bool isSealedDungeon);
|
||||
bool ResetRecenter(bool sessionEnding);
|
||||
bool IsSealedDungeon(uint cellId);
|
||||
void SetPriority(uint landblockId, int radius);
|
||||
void ClearPriority();
|
||||
}
|
||||
|
||||
internal sealed class LocalPlayerTeleportStreamingOperations
|
||||
|
|
@ -165,22 +163,11 @@ internal sealed class LocalPlayerTeleportStreamingOperations
|
|||
public bool IsSealedDungeon(uint cellId) =>
|
||||
_sealedDungeonCells.IsSealedDungeon(cellId);
|
||||
|
||||
public void SetPriority(uint landblockId, int radius)
|
||||
{
|
||||
_streaming.PriorityLandblockId = landblockId;
|
||||
_streaming.PriorityRadius = radius;
|
||||
}
|
||||
|
||||
public void ClearPriority()
|
||||
{
|
||||
_streaming.PriorityLandblockId = 0u;
|
||||
_streaming.PriorityRadius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerTeleportPlacement
|
||||
{
|
||||
void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced);
|
||||
void Place(Vector3 position, uint cellId, Quaternion rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -218,7 +205,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
||||
}
|
||||
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced)
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation)
|
||||
{
|
||||
PlayerMovementController controller = _controller.Controller
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -233,13 +220,6 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
resolved.Position.Y,
|
||||
resolved.Position.Z);
|
||||
|
||||
if (forced)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: teleport HOLD gave up (impossible/timeout) - force-snapping "
|
||||
+ $"cell=0x{cellId:X8} pos={position} -> 0x{resolved.CellId:X8} {snapped}");
|
||||
}
|
||||
|
||||
uint playerGuid = _identity.ServerGuid;
|
||||
controller.SetPosition(
|
||||
snapped,
|
||||
|
|
@ -270,7 +250,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED",
|
||||
controller.CellId,
|
||||
$"forced={forced}");
|
||||
"readiness=complete");
|
||||
Console.WriteLine(
|
||||
$"live: teleport materialized - snapped to {controller.Position} "
|
||||
+ $"cell=0x{controller.CellId:X8}");
|
||||
|
|
@ -315,6 +295,7 @@ internal interface ILocalPlayerTeleportPresentation : IDisposable
|
|||
void TickTunnel(float deltaSeconds);
|
||||
void EnterTunnel();
|
||||
void ExitTunnel();
|
||||
void SetWaitCue(bool visible);
|
||||
void Reset();
|
||||
Matrix4x4 ApplyViewPlane(Matrix4x4 projection);
|
||||
ICamera ApplyViewPlane(ICamera camera);
|
||||
|
|
@ -354,6 +335,7 @@ internal sealed class LocalPlayerTeleportPresentation
|
|||
public void TickTunnel(float deltaSeconds) => _tunnel.Tick(deltaSeconds);
|
||||
public void EnterTunnel() => _tunnel.Enter();
|
||||
public void ExitTunnel() => _tunnel.Exit();
|
||||
public void SetWaitCue(bool visible) => _tunnel.SetWaitCue(visible);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
|
@ -384,10 +366,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
||||
IDisposable
|
||||
{
|
||||
internal const float MaximumWorldHoldSeconds = 10f;
|
||||
internal const int NearRingRadius =
|
||||
WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius;
|
||||
|
||||
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
||||
private readonly ILocalPlayerTeleportAuthority _authority;
|
||||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||
|
|
@ -404,7 +382,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
private bool _hasAcceptedDestination;
|
||||
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
||||
private float _holdSeconds;
|
||||
private bool _forced;
|
||||
private long _lifetimeGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -496,15 +473,13 @@ internal sealed class LocalPlayerTeleportController
|
|||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
|
||||
if (haveDestination && originReady && !ready)
|
||||
{
|
||||
if (haveDestination && !ready)
|
||||
_holdSeconds += deltaSeconds;
|
||||
if (_holdSeconds >= MaximumWorldHoldSeconds)
|
||||
{
|
||||
ready = true;
|
||||
_forced = true;
|
||||
}
|
||||
}
|
||||
_presentation.SetWaitCue(
|
||||
haveDestination
|
||||
&& !ready
|
||||
&& _worldReveal.ObserveWait(
|
||||
TimeSpan.FromSeconds(_holdSeconds)));
|
||||
|
||||
var (_, events) = _presentation.Tick(deltaSeconds, ready);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
|
|
@ -518,14 +493,10 @@ internal sealed class LocalPlayerTeleportController
|
|||
_placement.Place(
|
||||
_pendingPosition,
|
||||
_pendingCell,
|
||||
_pendingRotation,
|
||||
_forced);
|
||||
_pendingRotation);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_worldReveal.ObserveMaterialized();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_streaming.ClearPriority();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
break;
|
||||
|
|
@ -590,7 +561,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
}
|
||||
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
_presentation.Begin(_mode.Projection);
|
||||
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
||||
return;
|
||||
|
|
@ -677,7 +647,7 @@ internal sealed class LocalPlayerTeleportController
|
|||
// Retail SmartBox enters blocking_for_cells before old-world object,
|
||||
// physics, landscape, and ambient owners may advance again. Begin the
|
||||
// reveal generation before a recenter can start detaching that world.
|
||||
_worldReveal.Begin(WorldRevealKind.Portal);
|
||||
_worldReveal.Begin(WorldRevealKind.Portal, position.LandblockId);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
|
||||
|
|
@ -713,13 +683,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
_pendingPosition = worldPosition;
|
||||
_pendingCell = position.LandblockId;
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
_streaming.SetPriority(
|
||||
StreamingRegion.EncodeLandblockId(landblockX, landblockY),
|
||||
NearRingRadius);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
|
||||
PhysicsDiagnostics.LogTeleport(
|
||||
"AIM",
|
||||
position.LandblockId,
|
||||
|
|
@ -748,25 +711,19 @@ internal sealed class LocalPlayerTeleportController
|
|||
_hasAcceptedDestination = false;
|
||||
_acceptedDestination = default;
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
|
||||
_streaming.ResetRecenter(clearSession);
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
if (clearSession)
|
||||
{
|
||||
_worldReveal.Cancel();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
}
|
||||
_worldReveal.Cancel();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
_presentation.Reset();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
_streaming.ClearPriority();
|
||||
|
||||
return generation;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal readonly record struct StreamingQueuedCompletion(
|
|||
LandblockStreamResult Result,
|
||||
LandblockStreamCostEstimate Estimate,
|
||||
StreamingCompletionPriority Priority,
|
||||
long RevealGeneration,
|
||||
ulong Generation,
|
||||
long Sequence,
|
||||
long EnqueuedTimestamp);
|
||||
|
|
|
|||
|
|
@ -17,8 +17,15 @@ namespace AcDream.App.Streaming;
|
|||
/// Threading: not thread-safe. All calls must happen on the render thread.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public sealed class StreamingController : IStreamingFrameBackend
|
||||
public sealed class StreamingController
|
||||
: IStreamingFrameBackend,
|
||||
IWorldRevealStreamingScheduler
|
||||
{
|
||||
private readonly record struct DestinationReservation(
|
||||
long RevealGeneration,
|
||||
uint LandblockId,
|
||||
int Radius);
|
||||
|
||||
private sealed class OriginRecenterRetirement
|
||||
{
|
||||
public bool RadiiConverged;
|
||||
|
|
@ -61,6 +68,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
private readonly StreamingCompletionQueue _completionQueue = new();
|
||||
private long _nextCompletionSequence;
|
||||
private int _legacyCompletionProfile = 4;
|
||||
private DestinationReservation? _destinationReservation;
|
||||
|
||||
private readonly GpuWorldState _state;
|
||||
private StreamingRegion? _region;
|
||||
|
|
@ -120,24 +128,54 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #138: the teleport destination landblock id. Loaded or promoted results
|
||||
/// inside its priority ring enter the destination FIFO, ahead of ordinary
|
||||
/// Near/Far work, but still consume the same typed frame budget.
|
||||
///
|
||||
/// <para>Set by <c>GameWindow</c> when a teleport starts; reset to 0
|
||||
/// once the destination landblock has been applied (or when the
|
||||
/// teleport destination changes).</para>
|
||||
/// </summary>
|
||||
public uint PriorityLandblockId { get; set; }
|
||||
internal long ActiveRevealGeneration =>
|
||||
_destinationReservation?.RevealGeneration ?? 0L;
|
||||
|
||||
/// <summary>
|
||||
/// Radius in landblocks (Chebyshev distance) around
|
||||
/// <see cref="PriorityLandblockId"/> classified as destination work.
|
||||
/// Zero means only the center. This changes queue priority, never budget
|
||||
/// enforcement or atomic publication semantics.
|
||||
/// </summary>
|
||||
public int PriorityRadius { get; set; }
|
||||
internal uint DestinationLandblockId =>
|
||||
_destinationReservation?.LandblockId ?? 0u;
|
||||
|
||||
internal int DestinationRadius =>
|
||||
_destinationReservation?.Radius ?? 0;
|
||||
|
||||
internal void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius)
|
||||
{
|
||||
if (revealGeneration <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(revealGeneration));
|
||||
if (destinationCell == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||
if (requiredRenderRadius < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(requiredRenderRadius));
|
||||
|
||||
_destinationReservation = new DestinationReservation(
|
||||
revealGeneration,
|
||||
(destinationCell & 0xFFFF0000u) | 0xFFFFu,
|
||||
requiredRenderRadius);
|
||||
}
|
||||
|
||||
internal void EndDestinationReservation(long revealGeneration)
|
||||
{
|
||||
if (_destinationReservation is { } active
|
||||
&& active.RevealGeneration == revealGeneration)
|
||||
{
|
||||
_destinationReservation = null;
|
||||
}
|
||||
}
|
||||
|
||||
void IWorldRevealStreamingScheduler.BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius) =>
|
||||
BeginDestinationReservation(
|
||||
revealGeneration,
|
||||
destinationCell,
|
||||
requiredRenderRadius);
|
||||
|
||||
void IWorldRevealStreamingScheduler.EndDestinationReservation(
|
||||
long revealGeneration) =>
|
||||
EndDestinationReservation(revealGeneration);
|
||||
|
||||
// [FRAME-DIAG] (read by GameWindow's ACDREAM_WB_DIAG rollup): the standing
|
||||
// accepted completion backlog. A non-zero value during/after a teleport is
|
||||
|
|
@ -507,6 +545,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
for (int i = 0; i < pending.Count; i++)
|
||||
{
|
||||
LandblockStreamResult result = pending[i];
|
||||
using StreamingWorkMeter.LaneScope lane =
|
||||
_activeWorkMeter.EnterLane(
|
||||
IsDestinationWork(result.LandblockId)
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance =
|
||||
_presentation.ResumePublication(
|
||||
result,
|
||||
|
|
@ -549,7 +592,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
throw new InvalidOperationException(
|
||||
"StreamingController.Tick cannot be reentered.");
|
||||
|
||||
var meter = new StreamingWorkMeter(_workBudget);
|
||||
var meter = new StreamingWorkMeter(
|
||||
_workBudget,
|
||||
destinationReservationActive: _destinationReservation is not null);
|
||||
_activeWorkMeter = meter;
|
||||
try
|
||||
{
|
||||
|
|
@ -901,15 +946,13 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="id"/> is the priority center or within
|
||||
/// <see cref="PriorityRadius"/> landblocks of it (Chebyshev) — i.e. inside the teleport
|
||||
/// near ring that <see cref="DrainAndApply"/> prioritizes. With the default radius 0
|
||||
/// this reduces to an exact match on <see cref="PriorityLandblockId"/> (the original
|
||||
/// single-landblock priority behaviour).
|
||||
/// True when <paramref name="id"/> belongs to the canonical reveal
|
||||
/// generation's destination neighborhood.
|
||||
/// </summary>
|
||||
private bool IsWithinPriorityRing(uint id)
|
||||
=> PriorityLandblockId != 0u
|
||||
&& ChebyshevLandblocks(id, PriorityLandblockId) <= PriorityRadius;
|
||||
private bool IsDestinationWork(uint id)
|
||||
=> _destinationReservation is { } reservation
|
||||
&& ChebyshevLandblocks(id, reservation.LandblockId)
|
||||
<= reservation.Radius;
|
||||
|
||||
/// <summary>
|
||||
/// Dungeon-exit edge (portal to outdoors / teleport): rebuild the full
|
||||
|
|
@ -1498,6 +1541,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
"The completion queue returned a null head.");
|
||||
try
|
||||
{
|
||||
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||
work.Priority == StreamingCompletionPriority.Destination
|
||||
&& work.RevealGeneration == ActiveRevealGeneration
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance = ApplyResult(
|
||||
work,
|
||||
meter,
|
||||
|
|
@ -1554,6 +1602,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
"The completion source returned a null peek.");
|
||||
bool stale = IsStaleGeneration(result)
|
||||
&& !_presentation.HasPendingPublication(result);
|
||||
StreamingCompletionPriority priority = stale
|
||||
? StreamingCompletionPriority.Control
|
||||
: ClassifyCompletion(result);
|
||||
LandblockStreamCostEstimate estimate =
|
||||
LandblockStreamResultCost.Estimate(result);
|
||||
StreamingWorkCost admissionCost = stale
|
||||
|
|
@ -1563,6 +1614,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
estimate.Work.CompletionAdmissions,
|
||||
AdoptedCpuBytes:
|
||||
estimate.Work.AdoptedCpuBytes);
|
||||
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||
priority == StreamingCompletionPriority.Destination
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
StreamingWorkAdmission admission = meter.TryReserve(
|
||||
admissionCost,
|
||||
"completion-admission");
|
||||
|
|
@ -1584,7 +1639,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
_completionQueue.Enqueue(new StreamingQueuedCompletion(
|
||||
result,
|
||||
estimate,
|
||||
ClassifyCompletion(result),
|
||||
priority,
|
||||
priority == StreamingCompletionPriority.Destination
|
||||
? ActiveRevealGeneration
|
||||
: 0L,
|
||||
result.Generation,
|
||||
checked(_nextCompletionSequence++),
|
||||
Stopwatch.GetTimestamp()));
|
||||
|
|
@ -1604,7 +1662,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
{
|
||||
if (result is LandblockStreamResult.Loaded
|
||||
or LandblockStreamResult.Promoted
|
||||
&& IsWithinPriorityRing(result.LandblockId))
|
||||
&& IsDestinationWork(result.LandblockId))
|
||||
{
|
||||
return StreamingCompletionPriority.Destination;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,11 +135,19 @@ public enum StreamingWorkLimit : byte
|
|||
GlRetireOperations,
|
||||
}
|
||||
|
||||
internal enum StreamingWorkLane : byte
|
||||
{
|
||||
NonDestination,
|
||||
Destination,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable observation of one frame's streaming work.
|
||||
/// </summary>
|
||||
public readonly record struct StreamingWorkMeterSnapshot(
|
||||
StreamingWorkCost Used,
|
||||
StreamingWorkCost DestinationUsed,
|
||||
StreamingWorkCost NonDestinationUsed,
|
||||
int Operations,
|
||||
int CompletedOperations,
|
||||
int YieldCount,
|
||||
|
|
@ -177,7 +185,14 @@ public sealed class StreamingWorkMeter
|
|||
private readonly Func<long> _timestamp;
|
||||
private readonly long _frequency;
|
||||
private readonly long _start;
|
||||
private readonly bool _destinationReservationActive;
|
||||
private StreamingWorkCost _used;
|
||||
private StreamingWorkCost _destinationUsed;
|
||||
private StreamingWorkCost _nonDestinationUsed;
|
||||
private StreamingWorkLane _lane;
|
||||
private long _destinationElapsedTicks;
|
||||
private long _nonDestinationElapsedTicks;
|
||||
private long _activeOperationStart;
|
||||
private int _operations;
|
||||
private int _completed;
|
||||
private int _yields;
|
||||
|
|
@ -191,15 +206,22 @@ public sealed class StreamingWorkMeter
|
|||
private string? _lastStage;
|
||||
private StreamingWorkLimit _lastLimit;
|
||||
|
||||
public StreamingWorkMeter(StreamingWorkBudget budget)
|
||||
: this(budget, Stopwatch.GetTimestamp, Stopwatch.Frequency)
|
||||
public StreamingWorkMeter(
|
||||
StreamingWorkBudget budget,
|
||||
bool destinationReservationActive = false)
|
||||
: this(
|
||||
budget,
|
||||
Stopwatch.GetTimestamp,
|
||||
Stopwatch.Frequency,
|
||||
destinationReservationActive)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamingWorkMeter(
|
||||
StreamingWorkBudget budget,
|
||||
Func<long> timestamp,
|
||||
long timestampFrequency)
|
||||
long timestampFrequency,
|
||||
bool destinationReservationActive = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(timestamp);
|
||||
if (timestampFrequency <= 0)
|
||||
|
|
@ -210,6 +232,18 @@ public sealed class StreamingWorkMeter
|
|||
_timestamp = timestamp;
|
||||
_frequency = timestampFrequency;
|
||||
_start = timestamp();
|
||||
_destinationReservationActive = destinationReservationActive;
|
||||
}
|
||||
|
||||
internal LaneScope EnterLane(StreamingWorkLane lane)
|
||||
{
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot change streaming lane during an active operation.");
|
||||
|
||||
StreamingWorkLane previous = _lane;
|
||||
_lane = lane;
|
||||
return new LaneScope(this, previous);
|
||||
}
|
||||
|
||||
public StreamingWorkAdmission TryReserve(
|
||||
|
|
@ -237,9 +271,14 @@ public sealed class StreamingWorkMeter
|
|||
}
|
||||
|
||||
_used = _used.Add(cost);
|
||||
if (_lane == StreamingWorkLane.Destination)
|
||||
_destinationUsed = _destinationUsed.Add(cost);
|
||||
else
|
||||
_nonDestinationUsed = _nonDestinationUsed.Add(cost);
|
||||
_operations++;
|
||||
_reservationActive = true;
|
||||
_activeStage = stage;
|
||||
_activeOperationStart = _timestamp();
|
||||
if (ensureProgress)
|
||||
_ensuredProgressGranted = true;
|
||||
if (limit != StreamingWorkLimit.None)
|
||||
|
|
@ -258,31 +297,10 @@ public sealed class StreamingWorkMeter
|
|||
return StreamingWorkAdmission.Admitted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// E1 observation seam: records work the legacy scheduler already chose to
|
||||
/// execute, while naming the budget decision that would have yielded. This
|
||||
/// preserves execution until the explicit scheduler takes ownership in E2.
|
||||
/// </summary>
|
||||
public void ForceReserveObserved(
|
||||
StreamingWorkCost cost,
|
||||
string stage)
|
||||
{
|
||||
cost.Validate();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(stage);
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"The prior streaming operation has not completed.");
|
||||
|
||||
_used = _used.Add(cost);
|
||||
_operations++;
|
||||
_reservationActive = true;
|
||||
_activeStage = stage;
|
||||
_lastStage = stage;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
EnsureReservation();
|
||||
CompleteLaneTiming();
|
||||
_completed++;
|
||||
_reservationActive = false;
|
||||
_activeStage = null;
|
||||
|
|
@ -292,6 +310,7 @@ public sealed class StreamingWorkMeter
|
|||
public void Fail()
|
||||
{
|
||||
EnsureReservation();
|
||||
CompleteLaneTiming();
|
||||
_failures++;
|
||||
_reservationActive = false;
|
||||
_activeStage = null;
|
||||
|
|
@ -313,6 +332,8 @@ public sealed class StreamingWorkMeter
|
|||
long now = _timestamp();
|
||||
return new StreamingWorkMeterSnapshot(
|
||||
_used,
|
||||
_destinationUsed,
|
||||
_nonDestinationUsed,
|
||||
_operations,
|
||||
_completed,
|
||||
_yields,
|
||||
|
|
@ -348,9 +369,105 @@ public sealed class StreamingWorkMeter
|
|||
{
|
||||
return StreamingWorkLimit.GlRetireOperations;
|
||||
}
|
||||
|
||||
if (_destinationReservationActive
|
||||
&& _lane == StreamingWorkLane.NonDestination)
|
||||
{
|
||||
double unreservedFraction =
|
||||
1.0 - _budget.DestinationReserveFraction;
|
||||
long maxTimeTicks = ReservedLimit(
|
||||
_budget.MaxUpdateTime.Ticks,
|
||||
unreservedFraction);
|
||||
if (_nonDestinationElapsedTicks >= maxTimeTicks)
|
||||
return StreamingWorkLimit.Time;
|
||||
if ((long)_nonDestinationUsed.CompletionAdmissions
|
||||
+ cost.CompletionAdmissions
|
||||
> ReservedLimit(
|
||||
_budget.MaxCompletionAdmissions,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.CompletionAdmissions;
|
||||
}
|
||||
if (_nonDestinationUsed.AdoptedCpuBytes
|
||||
> ReservedLimit(
|
||||
_budget.MaxAdoptedCpuBytes,
|
||||
unreservedFraction) - cost.AdoptedCpuBytes)
|
||||
{
|
||||
return StreamingWorkLimit.AdoptedCpuBytes;
|
||||
}
|
||||
if ((long)_nonDestinationUsed.EntityOperations
|
||||
+ cost.EntityOperations
|
||||
> ReservedLimit(
|
||||
_budget.MaxEntityOperations,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.EntityOperations;
|
||||
}
|
||||
if (_nonDestinationUsed.GpuUploadBytes
|
||||
> ReservedLimit(
|
||||
_budget.MaxGpuUploadBytes,
|
||||
unreservedFraction) - cost.GpuUploadBytes)
|
||||
{
|
||||
return StreamingWorkLimit.GpuUploadBytes;
|
||||
}
|
||||
if ((long)_nonDestinationUsed.GlRetireOperations
|
||||
+ cost.GlRetireOperations
|
||||
> ReservedLimit(
|
||||
_budget.MaxGlRetireOperations,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.GlRetireOperations;
|
||||
}
|
||||
}
|
||||
return StreamingWorkLimit.None;
|
||||
}
|
||||
|
||||
private static long ReservedLimit(long total, double fraction) =>
|
||||
Math.Max(1L, (long)Math.Floor(total * fraction));
|
||||
|
||||
private void CompleteLaneTiming()
|
||||
{
|
||||
long raw = Math.Max(0L, _timestamp() - _activeOperationStart);
|
||||
long elapsed = raw > long.MaxValue / TimeSpan.TicksPerSecond
|
||||
? long.MaxValue
|
||||
: raw * TimeSpan.TicksPerSecond / _frequency;
|
||||
if (_lane == StreamingWorkLane.Destination)
|
||||
_destinationElapsedTicks = SaturatingAdd(
|
||||
_destinationElapsedTicks,
|
||||
elapsed);
|
||||
else
|
||||
_nonDestinationElapsedTicks = SaturatingAdd(
|
||||
_nonDestinationElapsedTicks,
|
||||
elapsed);
|
||||
}
|
||||
|
||||
private static long SaturatingAdd(long left, long right) =>
|
||||
left > long.MaxValue - right ? long.MaxValue : left + right;
|
||||
|
||||
private void RestoreLane(StreamingWorkLane lane)
|
||||
{
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot restore streaming lane during an active operation.");
|
||||
_lane = lane;
|
||||
}
|
||||
|
||||
internal readonly struct LaneScope : IDisposable
|
||||
{
|
||||
private readonly StreamingWorkMeter _meter;
|
||||
private readonly StreamingWorkLane _previous;
|
||||
|
||||
internal LaneScope(
|
||||
StreamingWorkMeter meter,
|
||||
StreamingWorkLane previous)
|
||||
{
|
||||
_meter = meter;
|
||||
_previous = previous;
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.RestoreLane(_previous);
|
||||
}
|
||||
|
||||
private void EnsureReservation()
|
||||
{
|
||||
if (!_reservationActive)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Generation-scoped destination-work seam. The reveal coordinator is the
|
||||
/// canonical owner of login/portal lifetime; the streaming scheduler only
|
||||
/// reserves capacity for the exact generation and destination it is given.
|
||||
/// </summary>
|
||||
internal interface IWorldRevealStreamingScheduler
|
||||
{
|
||||
void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius);
|
||||
|
||||
void EndDestinationReservation(long revealGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
||||
/// diagnostics. This keeps the destination barrier and its observations on a
|
||||
|
|
@ -11,6 +26,7 @@ internal sealed class WorldRevealCoordinator
|
|||
private readonly WorldRevealReadinessBarrier _readiness;
|
||||
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
||||
private readonly WorldGenerationQuiescence? _quiescence;
|
||||
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||
private long _activeGeneration;
|
||||
|
||||
public WorldRevealCoordinator(
|
||||
|
|
@ -22,7 +38,8 @@ internal sealed class WorldRevealCoordinator
|
|||
Action invalidateCompositeTextures,
|
||||
Func<uint, bool> isSpawnClaimUnhydratable,
|
||||
Action<string>? log = null,
|
||||
WorldGenerationQuiescence? quiescence = null)
|
||||
WorldGenerationQuiescence? quiescence = null,
|
||||
IWorldRevealStreamingScheduler? streaming = null)
|
||||
{
|
||||
_readiness = new WorldRevealReadinessBarrier(
|
||||
isRenderNeighborhoodReady,
|
||||
|
|
@ -34,17 +51,26 @@ internal sealed class WorldRevealCoordinator
|
|||
isSpawnClaimUnhydratable);
|
||||
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
||||
_quiescence = quiescence;
|
||||
_streaming = streaming;
|
||||
}
|
||||
|
||||
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
||||
public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount;
|
||||
public bool WaitCueShown => _lifecycle.WaitCueShown;
|
||||
|
||||
public long Begin(WorldRevealKind kind)
|
||||
public long Begin(WorldRevealKind kind, uint destinationCell)
|
||||
{
|
||||
if (destinationCell == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||
|
||||
_readiness.Begin();
|
||||
long generation = _lifecycle.Begin(kind);
|
||||
_activeGeneration = generation;
|
||||
_quiescence?.Begin(generation);
|
||||
_streaming?.BeginDestinationReservation(
|
||||
generation,
|
||||
destinationCell,
|
||||
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
||||
return generation;
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +96,9 @@ internal sealed class WorldRevealCoordinator
|
|||
public void ObserveWorldViewportVisible() =>
|
||||
_lifecycle.ObserveWorldViewportVisible();
|
||||
|
||||
public bool ObserveWait(TimeSpan elapsed) =>
|
||||
_lifecycle.ObserveWait(elapsed);
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_lifecycle.Complete();
|
||||
|
|
@ -89,6 +118,7 @@ internal sealed class WorldRevealCoordinator
|
|||
return;
|
||||
|
||||
_activeGeneration = 0;
|
||||
_streaming?.EndDestinationReservation(generation);
|
||||
_quiescence?.End(generation);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ internal readonly record struct WorldRevealLifecycleSnapshot(
|
|||
/// </summary>
|
||||
internal sealed class WorldRevealLifecycleTelemetry
|
||||
{
|
||||
internal static readonly TimeSpan RetailWaitCueDelay =
|
||||
TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly Action<string> _log;
|
||||
private WorldRevealLifecycleSnapshot _snapshot;
|
||||
private long _nextGeneration;
|
||||
private int _portalMaterializationCount;
|
||||
private bool _waitCueShown;
|
||||
|
||||
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
|
||||
{
|
||||
|
|
@ -42,6 +46,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
|
||||
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
|
||||
public int PortalMaterializationCount => _portalMaterializationCount;
|
||||
public bool WaitCueShown => _waitCueShown;
|
||||
|
||||
public long Begin(WorldRevealKind kind)
|
||||
{
|
||||
|
|
@ -53,6 +58,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
}
|
||||
|
||||
long generation = checked(++_nextGeneration);
|
||||
_waitCueShown = false;
|
||||
_snapshot = new WorldRevealLifecycleSnapshot(
|
||||
generation,
|
||||
kind,
|
||||
|
|
@ -66,6 +72,27 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
return generation;
|
||||
}
|
||||
|
||||
public bool ObserveWait(TimeSpan elapsed)
|
||||
{
|
||||
if (_snapshot.Generation == 0
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed
|
||||
|| elapsed < RetailWaitCueDelay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_waitCueShown)
|
||||
{
|
||||
_waitCueShown = true;
|
||||
_log(
|
||||
$"[world-reveal] event=wait-cue elapsedMs={elapsed.TotalMilliseconds:F0} "
|
||||
+ Describe(_snapshot));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ObserveReadiness(WorldRevealReadinessSnapshot readiness)
|
||||
{
|
||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
||||
|
|
@ -121,7 +148,9 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
||||
if (_snapshot.Generation == 0
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed)
|
||||
return;
|
||||
|
||||
_snapshot = _snapshot with { Cancelled = true };
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ internal sealed class WorldRevealReadinessBarrier
|
|||
/// <summary>
|
||||
/// True only when the same destination is drawable and collidable. An
|
||||
/// impossible indoor claim intentionally crosses the barrier so the
|
||||
/// existing loud forced-placement path can expose the invalid server data.
|
||||
/// existing loud unhydratable-placement path can expose invalid server
|
||||
/// data. A merely slow hydratable claim never crosses early.
|
||||
/// </summary>
|
||||
public bool IsReady(uint destinationCell)
|
||||
=> Evaluate(destinationCell).IsReady;
|
||||
|
|
@ -139,7 +140,7 @@ internal sealed class WorldRevealReadinessBarrier
|
|||
collisionReady);
|
||||
}
|
||||
|
||||
private static int RequiredRenderRadius(uint destinationCell) =>
|
||||
internal static int RequiredRenderRadius(uint destinationCell) =>
|
||||
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
|
||||
|
||||
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
|
||||
|
|
|
|||
64
src/AcDream.App/UI/PortalWaitNoticeController.cs
Normal file
64
src/AcDream.App/UI/PortalWaitNoticeController.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retained presentation of retail's ECM_UI display-string notice emitted by
|
||||
/// <c>gmSmartBoxUI::UseTime @ 0x004D6E30</c> while portal-space cell blocking
|
||||
/// continues. It is a centered overlay, not a chat message.
|
||||
/// </summary>
|
||||
internal sealed class PortalWaitNoticeController : IDisposable
|
||||
{
|
||||
private readonly UiRoot _root;
|
||||
private readonly UiText _text;
|
||||
private UiText.Line[] _lines = [];
|
||||
private bool _disposed;
|
||||
|
||||
public PortalWaitNoticeController(UiRoot root)
|
||||
{
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
_text = new UiText
|
||||
{
|
||||
Name = "PortalSpaceWaitNotice",
|
||||
Left = 0f,
|
||||
Top = 0f,
|
||||
Width = root.Width,
|
||||
Height = root.Height,
|
||||
Anchors = AnchorEdges.Left
|
||||
| AnchorEdges.Top
|
||||
| AnchorEdges.Right
|
||||
| AnchorEdges.Bottom,
|
||||
Centered = true,
|
||||
OneLine = true,
|
||||
ClickThrough = true,
|
||||
ZOrder = int.MaxValue,
|
||||
DefaultColor = Vector4.One,
|
||||
Visible = false,
|
||||
};
|
||||
_text.LinesProvider = () => _lines;
|
||||
_root.AddChild(_text);
|
||||
}
|
||||
|
||||
public void Set(string? message)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
_text.Visible = false;
|
||||
_lines = [];
|
||||
return;
|
||||
}
|
||||
|
||||
_lines = [new UiText.Line(message, Vector4.One)];
|
||||
_text.Visible = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_root.RemoveChild(_text);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +224,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
|||
lbY,
|
||||
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
||||
position.LandblockId));
|
||||
_worldReveal.Begin(WorldRevealKind.Login);
|
||||
_worldReveal.Begin(WorldRevealKind.Login, position.LandblockId);
|
||||
|
||||
return new(
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -1024,14 +1024,14 @@ public sealed class LandblockPresentationPipelineTests
|
|||
MaxGpuUploadBytes: 1_000_000,
|
||||
MaxGlRetireOperations: 1_000,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
controller.PriorityLandblockId = priorityId;
|
||||
controller.BeginDestinationReservation(1, priorityId, 0);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
||||
Assert.Empty(outbox);
|
||||
Assert.False(state.IsLoaded(laterId));
|
||||
Assert.True(state.IsLoaded(unloadId));
|
||||
|
||||
controller.PriorityLandblockId = 0;
|
||||
controller.EndDestinationReservation(1);
|
||||
controller.Tick(0x70, 0x70);
|
||||
|
||||
Assert.True(state.IsLoaded(priorityId));
|
||||
|
|
|
|||
|
|
@ -46,22 +46,23 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
harness.Controller.OfferDestination(
|
||||
destination,
|
||||
teleportTimestampAdvanced: true);
|
||||
Assert.Empty(harness.Streaming.PriorityWrites);
|
||||
Assert.Empty(harness.Streaming.Reservations);
|
||||
|
||||
harness.Controller.OnTeleportStarted(7);
|
||||
|
||||
Assert.Equal(1, harness.Input.EndCount);
|
||||
Assert.Equal(1, harness.Mode.EnterPortalCount);
|
||||
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
|
||||
Assert.Equal((0x2021FFFFu, LocalPlayerTeleportController.NearRingRadius),
|
||||
Assert.Single(harness.Streaming.PriorityWrites));
|
||||
Assert.Equal(
|
||||
(1L, 0x20210001u, WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius),
|
||||
Assert.Single(harness.Streaming.Reservations));
|
||||
Assert.True(harness.Reveal.Snapshot.IsActive);
|
||||
Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind);
|
||||
|
||||
harness.Controller.OfferDestination(
|
||||
destination,
|
||||
teleportTimestampAdvanced: false);
|
||||
Assert.Single(harness.Streaming.PriorityWrites);
|
||||
Assert.Single(harness.Streaming.Reservations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -89,7 +90,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.True(harness.Controller.IsActive);
|
||||
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
||||
Assert.NotNull(harness.Presentation.BeginProjection);
|
||||
Assert.Single(harness.Streaming.PriorityWrites);
|
||||
Assert.Single(harness.Streaming.Reservations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -105,14 +106,14 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
teleportTimestampAdvanced: true);
|
||||
|
||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||
Assert.Empty(harness.Streaming.PriorityWrites);
|
||||
Assert.Empty(harness.Streaming.Reservations);
|
||||
|
||||
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
|
||||
harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||
harness.Controller.Tick(0.016f);
|
||||
|
||||
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
||||
Assert.Single(harness.Streaming.PriorityWrites);
|
||||
Assert.Single(harness.Streaming.Reservations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -158,7 +159,6 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.Empty(harness.Streaming.Recenters);
|
||||
Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position);
|
||||
Assert.Equal(0x20210123u, harness.Placement.CellId);
|
||||
Assert.False(harness.Placement.Forced);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -188,7 +188,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadinessHold_UsesWallClockAndForcesExactlyAtTenSeconds()
|
||||
public void ReadinessHold_NeverRevealsIncompleteWorldAndShowsWaitCueAfterFiveSeconds()
|
||||
{
|
||||
var harness = new Harness(worldReady: false);
|
||||
harness.Controller.OnTeleportStarted(5);
|
||||
|
|
@ -197,15 +197,18 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
teleportTimestampAdvanced: true);
|
||||
harness.Presentation.EmitPlaceWhenReady = true;
|
||||
|
||||
harness.Controller.Tick(9.5f);
|
||||
harness.Controller.Tick(4.9f);
|
||||
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
|
||||
Assert.False(harness.Presentation.WaitCueValues[^1]);
|
||||
Assert.Equal(default, harness.Placement.Position);
|
||||
|
||||
harness.Controller.Tick(0.5f);
|
||||
harness.Controller.Tick(0.1f);
|
||||
harness.Controller.Tick(30f);
|
||||
|
||||
Assert.True(harness.Presentation.WorldReadyValues[^1]);
|
||||
Assert.True(harness.Placement.Forced);
|
||||
Assert.Equal(new Vector3(1f, 2f, 3f), harness.Placement.Position);
|
||||
Assert.All(harness.Presentation.WorldReadyValues, Assert.False);
|
||||
Assert.True(harness.Presentation.WaitCueValues[^1]);
|
||||
Assert.Equal(default, harness.Placement.Position);
|
||||
Assert.True(harness.Reveal.WaitCueShown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -223,7 +226,6 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
harness.Controller.Tick(0.016f);
|
||||
|
||||
Assert.True(Index(order, "placement") < Index(order, "[world-reveal] event=materialized"));
|
||||
Assert.True(Index(order, "[world-reveal] event=materialized") < Index(order, "priority-clear"));
|
||||
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +251,8 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.False(harness.Controller.IsActive);
|
||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||
Assert.Equal(1, harness.Session.LoginCompleteCount);
|
||||
Assert.True(harness.Reveal.Snapshot.Completed);
|
||||
Assert.False(harness.Reveal.Snapshot.Cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -261,7 +265,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
teleportTimestampAdvanced: true);
|
||||
|
||||
harness.Controller.OnTeleportStarted(11);
|
||||
harness.Controller.Tick(LocalPlayerTeleportController.MaximumWorldHoldSeconds);
|
||||
harness.Controller.Tick(30f);
|
||||
|
||||
Assert.Equal(default, harness.Placement.Position);
|
||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||
|
|
@ -318,7 +322,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
harness.Controller.OfferDestination(
|
||||
Position(0x20210001u, 20, 1f, 2f, 3f),
|
||||
teleportTimestampAdvanced: true);
|
||||
harness.Streaming.PriorityClearCount = 0;
|
||||
harness.Streaming.ReservationEnds.Clear();
|
||||
harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21);
|
||||
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
||||
|
||||
|
|
@ -327,7 +331,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
Assert.True(harness.Controller.IsActive);
|
||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
|
||||
Assert.Equal(1, harness.Streaming.PriorityClearCount);
|
||||
Assert.Single(harness.Streaming.ReservationEnds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -418,7 +422,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
var position = new Vector3(12f, 24f, 6f);
|
||||
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
|
||||
|
||||
placement.Place(position, cell, rotation, forced: false);
|
||||
placement.Place(position, cell, rotation);
|
||||
|
||||
Assert.Equal(entity.Position, controllerSlot.Controller.Position);
|
||||
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
|
||||
|
|
@ -560,7 +564,8 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
prepareCompositeTextures: (_, _) => { },
|
||||
invalidateCompositeTextures: () => { },
|
||||
isSpawnClaimUnhydratable: _ => false,
|
||||
log: order.Add);
|
||||
log: order.Add,
|
||||
streaming: Streaming);
|
||||
Controller = new LocalPlayerTeleportController(
|
||||
Authority,
|
||||
Input,
|
||||
|
|
@ -619,7 +624,9 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class FakeStreaming : ILocalPlayerTeleportStreamingOperations
|
||||
private sealed class FakeStreaming
|
||||
: ILocalPlayerTeleportStreamingOperations,
|
||||
IWorldRevealStreamingScheduler
|
||||
{
|
||||
private readonly List<string> _order;
|
||||
|
||||
|
|
@ -636,9 +643,10 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public bool RecenterPending;
|
||||
public bool ResetResult = true;
|
||||
public bool LastResetWasSessionEnding;
|
||||
public int PriorityClearCount;
|
||||
public readonly List<(int X, int Y, bool Sealed)> Recenters = new();
|
||||
public readonly List<(uint Landblock, int Radius)> PriorityWrites = new();
|
||||
public readonly List<(long Generation, uint Cell, int Radius)>
|
||||
Reservations = new();
|
||||
public readonly List<long> ReservationEnds = new();
|
||||
|
||||
public bool BeginRecenter(int x, int y, bool isSealedDungeon)
|
||||
{
|
||||
|
|
@ -655,13 +663,17 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public bool IsSealedDungeon(uint cellId) =>
|
||||
(cellId & 0xFFFFu) >= 0x0100u;
|
||||
|
||||
public void SetPriority(uint landblockId, int radius) =>
|
||||
PriorityWrites.Add((landblockId, radius));
|
||||
public void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius) =>
|
||||
Reservations.Add(
|
||||
(revealGeneration, destinationCell, requiredRenderRadius));
|
||||
|
||||
public void ClearPriority()
|
||||
public void EndDestinationReservation(long revealGeneration)
|
||||
{
|
||||
PriorityClearCount++;
|
||||
_order.Add("priority-clear");
|
||||
ReservationEnds.Add(revealGeneration);
|
||||
_order.Add("reservation-end");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -674,16 +686,14 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public Vector3 Position;
|
||||
public uint CellId;
|
||||
public Quaternion Rotation;
|
||||
public bool Forced;
|
||||
public Action? OnPlace;
|
||||
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced)
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation)
|
||||
{
|
||||
_order.Add("placement");
|
||||
Position = position;
|
||||
CellId = cellId;
|
||||
Rotation = rotation;
|
||||
Forced = forced;
|
||||
OnPlace?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
@ -731,6 +741,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public Matrix4x4? BeginProjection;
|
||||
public bool EmitPlaceWhenReady;
|
||||
public readonly List<bool> WorldReadyValues = new();
|
||||
public readonly List<bool> WaitCueValues = new();
|
||||
public int DisposeFailuresRemaining;
|
||||
public int DisposeCount;
|
||||
|
||||
|
|
@ -760,6 +771,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
|
||||
public void EnterTunnel() => IsPortalViewportVisible = true;
|
||||
public void ExitTunnel() => IsPortalViewportVisible = false;
|
||||
public void SetWaitCue(bool visible) => WaitCueValues.Add(visible);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ public sealed class StreamingCompletionQueueTests
|
|||
result,
|
||||
estimate,
|
||||
priority,
|
||||
priority == StreamingCompletionPriority.Destination ? 1L : 0L,
|
||||
result.Generation,
|
||||
sequence,
|
||||
Stopwatch.GetTimestamp());
|
||||
|
|
|
|||
|
|
@ -117,6 +117,110 @@ public sealed class StreamingWorkBudgetTests
|
|||
meter.Snapshot.LastLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveReveal_ProtectsDestinationShareAcrossEveryCountAndByteDimension()
|
||||
{
|
||||
var meter = new StreamingWorkMeter(
|
||||
Budget(
|
||||
milliseconds: 100,
|
||||
completions: 4,
|
||||
cpuBytes: 100,
|
||||
entityOperations: 8,
|
||||
gpuBytes: 100,
|
||||
retireOperations: 4),
|
||||
static () => 0,
|
||||
timestampFrequency: 1_000,
|
||||
destinationReservationActive: true);
|
||||
|
||||
StreamingWorkCost unreservedShare = new(
|
||||
CompletionAdmissions: 1,
|
||||
AdoptedCpuBytes: 25,
|
||||
EntityOperations: 2,
|
||||
GpuUploadBytes: 25,
|
||||
GlRetireOperations: 1);
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(unreservedShare, "ordinary"));
|
||||
meter.Complete();
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Yielded,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(CompletionAdmissions: 1),
|
||||
"ordinary-over-reserve"));
|
||||
|
||||
using (meter.EnterLane(StreamingWorkLane.Destination))
|
||||
{
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(
|
||||
CompletionAdmissions: 3,
|
||||
AdoptedCpuBytes: 75,
|
||||
EntityOperations: 6,
|
||||
GpuUploadBytes: 75,
|
||||
GlRetireOperations: 3),
|
||||
"destination"));
|
||||
meter.Complete();
|
||||
}
|
||||
|
||||
StreamingWorkMeterSnapshot snapshot = meter.Snapshot;
|
||||
Assert.Equal(unreservedShare, snapshot.NonDestinationUsed);
|
||||
Assert.Equal(3, snapshot.DestinationUsed.CompletionAdmissions);
|
||||
Assert.Equal(100, snapshot.Used.AdoptedCpuBytes);
|
||||
Assert.Equal(100, snapshot.Used.GpuUploadBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InactiveReveal_DoesNotApplyDestinationReservation()
|
||||
{
|
||||
var meter = new StreamingWorkMeter(
|
||||
Budget(completions: 4),
|
||||
static () => 0,
|
||||
timestampFrequency: 1_000,
|
||||
destinationReservationActive: false);
|
||||
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(
|
||||
new StreamingWorkCost(CompletionAdmissions: 4),
|
||||
"ordinary"));
|
||||
meter.Complete();
|
||||
|
||||
Assert.Equal(4, meter.Snapshot.NonDestinationUsed.CompletionAdmissions);
|
||||
Assert.Equal(0, meter.Snapshot.DestinationUsed.CompletionAdmissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveReveal_ProtectsDestinationWallClockAfterOrdinaryShareIsSpent()
|
||||
{
|
||||
long now = 0;
|
||||
var meter = new StreamingWorkMeter(
|
||||
Budget(milliseconds: 100),
|
||||
() => now,
|
||||
timestampFrequency: 1_000,
|
||||
destinationReservationActive: true);
|
||||
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(default, "ordinary"));
|
||||
now = 25;
|
||||
meter.Complete();
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Yielded,
|
||||
meter.TryReserve(default, "ordinary-after-25ms"));
|
||||
|
||||
using (meter.EnterLane(StreamingWorkLane.Destination))
|
||||
{
|
||||
Assert.Equal(
|
||||
StreamingWorkAdmission.Admitted,
|
||||
meter.TryReserve(default, "destination"));
|
||||
now = 99;
|
||||
meter.Complete();
|
||||
}
|
||||
|
||||
Assert.Equal(2, meter.Snapshot.CompletedOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure()
|
||||
{
|
||||
|
|
@ -427,21 +531,32 @@ public sealed class StreamingWorkBudgetTests
|
|||
cpuBytes: 1_000,
|
||||
gpuBytes: 44,
|
||||
retireOperations: 1));
|
||||
controller.PriorityLandblockId = center;
|
||||
controller.BeginDestinationReservation(1, center, 0);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center], applied);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.YieldCount);
|
||||
Assert.Equal(2, controller.WorkDiagnostics.LastFrame.YieldCount);
|
||||
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center, ordinary], applied);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
|
||||
for (int i = 0;
|
||||
i < 4
|
||||
&& (controller.WorkDiagnostics.DeferredCompletions != 0
|
||||
|| source.BacklogCount != 0);
|
||||
i++)
|
||||
{
|
||||
controller.Tick(32, 32);
|
||||
}
|
||||
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(0, source.BacklogCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ public sealed class WorldRevealCoordinatorTests
|
|||
public int InvalidateCount { get; private set; }
|
||||
public int PrepareCount { get; private set; }
|
||||
|
||||
public WorldRevealCoordinator Build(List<string>? logs = null) => new(
|
||||
public WorldRevealCoordinator Build(
|
||||
List<string>? logs = null,
|
||||
IWorldRevealStreamingScheduler? streaming = null) => new(
|
||||
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
||||
isSpawnCellReady: _ => CollisionReady,
|
||||
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
||||
|
|
@ -25,7 +27,8 @@ public sealed class WorldRevealCoordinatorTests
|
|||
CompositesReady = false;
|
||||
},
|
||||
isSpawnClaimUnhydratable: _ => false,
|
||||
log: logs is null ? null : new Action<string>(logs.Add));
|
||||
log: logs is null ? null : new Action<string>(logs.Add),
|
||||
streaming: streaming);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -34,7 +37,7 @@ public sealed class WorldRevealCoordinatorTests
|
|||
var state = new State { CompositesReady = true };
|
||||
WorldRevealCoordinator coordinator = state.Build();
|
||||
|
||||
long generation = coordinator.Begin(WorldRevealKind.Login);
|
||||
long generation = coordinator.Begin(WorldRevealKind.Login, 0x11340021u);
|
||||
|
||||
Assert.Equal(1, generation);
|
||||
Assert.Equal(1, state.InvalidateCount);
|
||||
|
|
@ -52,7 +55,7 @@ public sealed class WorldRevealCoordinatorTests
|
|||
CollisionReady = true,
|
||||
};
|
||||
WorldRevealCoordinator coordinator = state.Build();
|
||||
coordinator.Begin(WorldRevealKind.Login);
|
||||
coordinator.Begin(WorldRevealKind.Login, outdoorCell);
|
||||
|
||||
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
||||
state.CompositesReady = true;
|
||||
|
|
@ -76,7 +79,7 @@ public sealed class WorldRevealCoordinatorTests
|
|||
CompositesReady = true,
|
||||
};
|
||||
WorldRevealCoordinator coordinator = state.Build(logs);
|
||||
coordinator.Begin(WorldRevealKind.Portal);
|
||||
coordinator.Begin(WorldRevealKind.Portal, outdoorCell);
|
||||
state.CompositesReady = true;
|
||||
|
||||
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
||||
|
|
@ -103,7 +106,7 @@ public sealed class WorldRevealCoordinatorTests
|
|||
CompositesReady = true,
|
||||
};
|
||||
WorldRevealCoordinator coordinator = state.Build();
|
||||
coordinator.Begin(WorldRevealKind.Portal);
|
||||
coordinator.Begin(WorldRevealKind.Portal, 0x11340021u);
|
||||
coordinator.Cancel();
|
||||
|
||||
coordinator.PrepareAndEvaluate(0x11340021u);
|
||||
|
|
@ -137,8 +140,8 @@ public sealed class WorldRevealCoordinatorTests
|
|||
isSpawnClaimUnhydratable: _ => false,
|
||||
quiescence: quiescence);
|
||||
|
||||
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login));
|
||||
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal));
|
||||
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login, 0x11340021u));
|
||||
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal, 0x11340021u));
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
Assert.Equal(2, availability.QuiescedGeneration);
|
||||
Assert.Equal(1, audio.SuspendCalls);
|
||||
|
|
@ -149,6 +152,30 @@ public sealed class WorldRevealCoordinatorTests
|
|||
Assert.Equal(1, audio.ResumeCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
|
||||
{
|
||||
var state = new State();
|
||||
var scheduler = new RecordingDestinationScheduler();
|
||||
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
||||
|
||||
long first = coordinator.Begin(
|
||||
WorldRevealKind.Portal,
|
||||
0x11340021u);
|
||||
long second = coordinator.Begin(
|
||||
WorldRevealKind.Portal,
|
||||
0x20210123u);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(first, 0x11340021u, 1),
|
||||
(second, 0x20210123u, 0),
|
||||
],
|
||||
scheduler.Begins);
|
||||
coordinator.Complete();
|
||||
Assert.Equal([second], scheduler.Ends);
|
||||
}
|
||||
|
||||
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
||||
{
|
||||
public int SuspendCalls { get; private set; }
|
||||
|
|
@ -157,4 +184,21 @@ public sealed class WorldRevealCoordinatorTests
|
|||
public void SuspendWorldAudio() => SuspendCalls++;
|
||||
public void ResumeWorldAudio() => ResumeCalls++;
|
||||
}
|
||||
|
||||
private sealed class RecordingDestinationScheduler
|
||||
: IWorldRevealStreamingScheduler
|
||||
{
|
||||
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
|
||||
public List<long> Ends { get; } = [];
|
||||
|
||||
public void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius) =>
|
||||
Begins.Add(
|
||||
(revealGeneration, destinationCell, requiredRenderRadius));
|
||||
|
||||
public void EndDestinationReservation(long revealGeneration) =>
|
||||
Ends.Add(revealGeneration);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class PortalWaitNoticeControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Notice_IsCenteredOverlayAndDisposesFromRetainedRoot()
|
||||
{
|
||||
var root = new UiRoot
|
||||
{
|
||||
Width = 1280f,
|
||||
Height = 720f,
|
||||
};
|
||||
|
||||
using (var controller = new PortalWaitNoticeController(root))
|
||||
{
|
||||
UiText text = Assert.IsType<UiText>(Assert.Single(root.Children));
|
||||
Assert.False(text.Visible);
|
||||
Assert.True(text.Centered);
|
||||
Assert.True(text.ClickThrough);
|
||||
Assert.Equal(root.Width, text.Width);
|
||||
Assert.Equal(root.Height, text.Height);
|
||||
|
||||
controller.Set("In Portal Space - Please Wait...");
|
||||
|
||||
Assert.True(text.Visible);
|
||||
UiText.Line line = Assert.Single(text.LinesProvider!());
|
||||
Assert.Equal("In Portal Space - Please Wait...", line.Text);
|
||||
|
||||
controller.Set(null);
|
||||
Assert.False(text.Visible);
|
||||
}
|
||||
|
||||
Assert.Empty(root.Children);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
generation);
|
||||
|
||||
[Fact]
|
||||
public void PriorityLandblock_WinsExecutionOrderAfterBoundedAdmission()
|
||||
public void DestinationReservation_WinsExecutionOrderAfterBoundedAdmission()
|
||||
{
|
||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
||||
|
|
@ -48,7 +48,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
workBudgetOptions: GenerousBudget())
|
||||
{ MaxCompletionsPerFrame = 4 };
|
||||
|
||||
ctrl.PriorityLandblockId = priority;
|
||||
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||
ctrl.Tick(169, 180);
|
||||
|
||||
Assert.NotEmpty(applied);
|
||||
|
|
@ -87,10 +87,10 @@ public class StreamingControllerPriorityApplyTests
|
|||
nearRadius: 4,
|
||||
farRadius: 12)
|
||||
{ MaxCompletionsPerFrame = 4 };
|
||||
ctrl.PriorityLandblockId = priority;
|
||||
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||
|
||||
ctrl.Tick(169, 180); // priority + some others
|
||||
ctrl.PriorityLandblockId = 0u;
|
||||
ctrl.EndDestinationReservation(1);
|
||||
ctrl.Tick(169, 180); // drains the deferred remainder
|
||||
ctrl.Tick(169, 180);
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
[Fact]
|
||||
public void PriorityNeverArrives_noThrow_noLoss_noDoubleApply()
|
||||
{
|
||||
// While a PriorityLandblockId is set but the matching completion never
|
||||
// While a reveal reservation is active but its matching completion never
|
||||
// arrives (e.g. the load failed), ordinary results still enter the typed
|
||||
// Near/Far queues and advance under the same budget without loss.
|
||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||
|
|
@ -134,7 +134,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
{ MaxCompletionsPerFrame = 4 };
|
||||
|
||||
// Set a priority id that will NEVER appear in the outbox.
|
||||
ctrl.PriorityLandblockId = priority;
|
||||
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||
|
||||
// Tick several times — should not throw and every non-priority completion
|
||||
// must still be applied without duplication.
|
||||
|
|
@ -160,6 +160,30 @@ public class StreamingControllerPriorityApplyTests
|
|||
MaxGlRetireOperations: 10_000,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
[Fact]
|
||||
public void DestinationReservation_StaleGenerationCannotClearReplacement()
|
||||
{
|
||||
var controller = new StreamingController(
|
||||
enqueueLoad: (_, _) => { },
|
||||
enqueueUnload: _ => { },
|
||||
drainCompletions: _ => Array.Empty<LandblockStreamResult>(),
|
||||
applyTerrain: (_, _) => { },
|
||||
state: new GpuWorldState(),
|
||||
nearRadius: 1,
|
||||
farRadius: 2);
|
||||
|
||||
controller.BeginDestinationReservation(1, 0x11340021u, 1);
|
||||
controller.BeginDestinationReservation(2, 0x20210123u, 0);
|
||||
controller.EndDestinationReservation(1);
|
||||
|
||||
Assert.Equal(2, controller.ActiveRevealGeneration);
|
||||
Assert.Equal(0x2021FFFFu, controller.DestinationLandblockId);
|
||||
Assert.Equal(0, controller.DestinationRadius);
|
||||
|
||||
controller.EndDestinationReservation(2);
|
||||
Assert.Equal(0, controller.ActiveRevealGeneration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredCompaction_ApplyFailureRetainsExactResultWithoutReplayingCommittedPrefix()
|
||||
{
|
||||
|
|
@ -199,8 +223,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
farRadius: 12)
|
||||
{
|
||||
MaxCompletionsPerFrame = 3,
|
||||
PriorityLandblockId = priority,
|
||||
};
|
||||
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => ctrl.Tick(169, 180));
|
||||
Assert.Equal([first], applied);
|
||||
|
|
@ -215,7 +239,6 @@ public class StreamingControllerPriorityApplyTests
|
|||
[Fact]
|
||||
public void ForceReloadWindow_DiscardsBufferedCompletionsFromOldWindow()
|
||||
{
|
||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||
var outbox = new Queue<LandblockStreamResult>(
|
||||
Enumerable.Range(0, 6)
|
||||
.Select(i => (LandblockStreamResult)LoadedOf(
|
||||
|
|
@ -243,8 +266,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
MaxGpuUploadBytes: 1_000_000,
|
||||
MaxGlRetireOperations: 1_000,
|
||||
DestinationReserveFraction: 0.75f))
|
||||
{ MaxCompletionsPerFrame = 4, PriorityLandblockId = priority };
|
||||
|
||||
{ MaxCompletionsPerFrame = 4 };
|
||||
ctrl.Tick(169, 180);
|
||||
Assert.Equal(4, applied.Count);
|
||||
applied.Clear();
|
||||
|
|
@ -452,7 +474,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
state: state,
|
||||
nearRadius: 0,
|
||||
farRadius: 0,
|
||||
onLandblockLoaded: loadedCallbacks.Add);
|
||||
onLandblockLoaded: loadedCallbacks.Add,
|
||||
workBudgetOptions: GenerousBudget());
|
||||
|
||||
ctrl.Tick(10, 10);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue