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,
|
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the appraisal formula-media/foreground correction passes
|
Release builds; the current performance-program checkpoint passes
|
||||||
3,557 App tests / 3 skips and 7,988 complete-solution tests / 5 skips.
|
3,674 App tests / 3 skips and 8,158 complete-solution tests / 5 skips.
|
||||||
|
|
||||||
**M4 feature work order (paused while the authorized performance program
|
**M4 feature work order (paused while the authorized performance program
|
||||||
executes):** the six-slice pre-M4 world-interaction completion
|
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
|
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
||||||
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
||||||
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
||||||
typed residency policy/accounting and bounded retained caches. Slice E0–E4 now
|
typed residency policy/accounting and bounded retained caches. Slice E0–E5 now
|
||||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||||
metered full-window/recenter retirement, and retained cursor-budgeted
|
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||||
render/physics/static/building/EnvCell publication; E5 destination reservation
|
render/physics/static/building/EnvCell publication. The canonical reveal
|
||||||
and reveal-generation joining is next. The physical console is available and
|
generation now owns one exact destination reservation across every typed
|
||||||
reserved for E6 connected gates. Slices F/G
|
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
|
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||||
gameplay-owner gate. Evidence:
|
gameplay-owner gate. Evidence:
|
||||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||||
|
|
@ -170,7 +173,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
||||||
truth remain correct.
|
truth remain correct.
|
||||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice 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.
|
lifestone/particle alpha visual gate.
|
||||||
|
|
||||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||||
|
|
|
||||||
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,
|
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the appraisal formula-media/foreground correction passes
|
Release builds; the current performance-program checkpoint passes
|
||||||
3,557 App tests / 3 skips and 7,988 complete-solution tests / 5 skips.
|
3,674 App tests / 3 skips and 8,158 complete-solution tests / 5 skips.
|
||||||
|
|
||||||
**M4 feature work order (paused while the authorized performance program
|
**M4 feature work order (paused while the authorized performance program
|
||||||
executes):** the six-slice pre-M4 world-interaction completion
|
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
|
lifetime, full-suite, and user-visual gates passed. Process allocation fell
|
||||||
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
41.8–61.9%, GC pause time fell 48.4–54.1%, uncapped CPU/GPU p99 improved
|
||||||
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
17.0%/20.6%, and invalid Setup-probe exceptions fell to zero. Slice D unified
|
||||||
typed residency policy/accounting and bounded retained caches. Slice E0–E4 now
|
typed residency policy/accounting and bounded retained caches. Slice E0–E5 now
|
||||||
enforce typed completion admission, generation-scoped old-world quiescence,
|
enforce typed completion admission, generation-scoped old-world quiescence,
|
||||||
metered full-window/recenter retirement, and retained cursor-budgeted
|
metered full-window/recenter retirement, and retained cursor-budgeted
|
||||||
render/physics/static/building/EnvCell publication; E5 destination reservation
|
render/physics/static/building/EnvCell publication. The canonical reveal
|
||||||
and reveal-generation joining is next. The physical console is available and
|
generation now owns one exact destination reservation across every typed
|
||||||
reserved for E6 connected gates. Slices F/G
|
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
|
remain behind the explicit ECS decision gate and Slice J behind its separate
|
||||||
gameplay-owner gate. Evidence:
|
gameplay-owner gate. Evidence:
|
||||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md` and
|
||||||
|
|
@ -168,7 +171,7 @@ uncapped Release presentation is smooth, while physics, collision, and wire
|
||||||
truth remain correct.
|
truth remain correct.
|
||||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||||
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||||
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice 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.
|
lifestone/particle alpha visual gate.
|
||||||
|
|
||||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,14 @@ What does NOT go here:
|
||||||
smooth and physics, collision, and wire state remain correct.
|
smooth and physics, collision, and wire state remain correct.
|
||||||
- **Build hygiene:** `#228` records 17 clean-Release test-project warnings;
|
- **Build hygiene:** `#228` records 17 clean-Release test-project warnings;
|
||||||
production compilation and all tests pass.
|
production compilation and all tests pass.
|
||||||
- **Performance side track:** Slices A–D and Slice E0–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
|
completion admission, immediate generation-scoped old-world quiescence, and
|
||||||
budgeted full-window/recenter retirement are live. Publication now advances
|
budgeted full-window/recenter retirement are live. Publication now advances
|
||||||
from retained render/physics/static/building/EnvCell cursors without replay;
|
from retained render/physics/static/building/EnvCell cursors without replay.
|
||||||
E5 destination reservation/reveal joining is next. The physical console is
|
The canonical reveal generation now owns the destination reservation across
|
||||||
available and remains reserved for the E6 connected gate. Evidence:
|
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).
|
[`research/2026-07-24-slice-d-unified-residency-report.md`](research/2026-07-24-slice-d-unified-residency-report.md).
|
||||||
|
|
||||||
The [documentation map](README.md) defines how this tactical ledger relates to
|
The [documentation map](README.md) defines how this tactical ledger relates to
|
||||||
|
|
@ -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).
|
**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).
|
[`plans/2026-07-23-world-interaction-completion.md`](plans/2026-07-23-world-interaction-completion.md).
|
||||||
The eight-slice `GameWindow` decomposition and its connected visual matrix
|
The eight-slice `GameWindow` decomposition and its connected visual matrix
|
||||||
are complete.
|
are complete.
|
||||||
- **Current active engineering program:** Slices A–D and Slice E0–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)
|
[`plans/2026-07-24-modern-runtime-architecture.md`](plans/2026-07-24-modern-runtime-architecture.md)
|
||||||
are complete. Production world-mesh streaming consumes the validated
|
are complete. Production world-mesh streaming consumes the validated
|
||||||
prepared package, typed residency is bounded/accounted, and streaming now
|
prepared package, typed residency is bounded/accounted, and streaming now
|
||||||
has typed admission plus immediate old-generation quiescence, metered
|
has typed admission plus immediate old-generation quiescence, metered
|
||||||
retirement, and retained cursor-budgeted publication across render, physics,
|
retirement, and retained cursor-budgeted publication across render, physics,
|
||||||
statics, building registries, collision/reflood, and EnvCells. E5 destination
|
statics, building registries, collision/reflood, and EnvCells. The canonical
|
||||||
reservation/reveal joining is next. The physical console is available; its
|
reveal generation now owns an exact destination reservation across every
|
||||||
connected performance and visual evidence remains the E6 gate. See
|
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).
|
the [Slice D residency report](research/2026-07-24-slice-d-unified-residency-report.md).
|
||||||
- **Completed gates:** R6 locomotion/collision/projectile/teleport/radar and
|
- **Completed gates:** R6 locomotion/collision/projectile/teleport/radar and
|
||||||
the final two-client portal-out/materialization observer comparison (`#218`).
|
the final two-client portal-out/materialization observer comparison (`#218`).
|
||||||
|
|
@ -34,7 +36,7 @@ banner cannot silently override the current program state.
|
||||||
user decision gate; Linux/headless automation (Track LH) remains future work.
|
user decision gate; Linux/headless automation (Track LH) remains future work.
|
||||||
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
|
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
|
||||||
and UN 5 — plus the retained retired TS-37 history note.
|
and UN 5 — plus the retained retired TS-37 history note.
|
||||||
- **Latest automated baseline:** Release build succeeds; 8,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
|
five are intentionally skipped. The capped, uncapped, and dense-Arwic
|
||||||
physical prepared-asset routes all completed their portal/input/lifetime
|
physical prepared-asset routes all completed their portal/input/lifetime
|
||||||
assertions and disconnected gracefully.
|
assertions and disconnected gracefully.
|
||||||
|
|
|
||||||
|
|
@ -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
|
adopting it. `StreamingController` is the sole scheduler: it admits through the
|
||||||
typed meter into reusable destination/control/unload/Near/Far FIFOs, preserves
|
typed meter into reusable destination/control/unload/Near/Far FIFOs, preserves
|
||||||
exact reference identity and retry position, rejects stale generations, and
|
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
|
budget. Lifecycle artifacts publish worker and per-class backlog, retained
|
||||||
bytes/age, yields, oversizes, overruns, and pending publication/retirement
|
bytes/age, per-lane work, yields, oversizes, overruns, and pending
|
||||||
facts.
|
publication/retirement facts.
|
||||||
|
|
||||||
At a hard login/portal reveal edge, `WorldGenerationQuiescence` makes the old
|
At a hard login/portal reveal edge, `WorldGenerationQuiescence` makes the old
|
||||||
generation unavailable through the same `WorldRevealCoordinator` lifetime.
|
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
|
`WorldRevealCoordinator` is the single App-layer owner of each login or portal
|
||||||
reveal lifetime. It composes the canonical `WorldRevealReadinessBarrier` with
|
reveal lifetime. It composes the canonical `WorldRevealReadinessBarrier` with
|
||||||
generation-scoped lifecycle observation, so begin, preparation, readiness,
|
generation-scoped lifecycle observation and the streaming destination
|
||||||
materialization, world visibility, completion, and cancellation cannot be
|
reservation, so begin, preparation, readiness, materialization, world
|
||||||
wired independently. The barrier joins `StreamingController`/`GpuWorldState`
|
visibility, completion, cancellation, and reserved capacity cannot be wired
|
||||||
|
independently. The barrier joins `StreamingController`/`GpuWorldState`
|
||||||
static-mesh publication, `WbDrawDispatcher` composite-texture warmup, and
|
static-mesh publication, `WbDrawDispatcher` composite-texture warmup, and
|
||||||
`PhysicsEngine` destination residency. Login's sky-only gate and portal-space
|
`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
|
Composite preparation advances after `WbMeshAdapter.Tick` on the render
|
||||||
thread, never from the streaming worker. This is acdream's asynchronous equivalent of retail
|
thread, never from the streaming worker. This is acdream's asynchronous equivalent of retail
|
||||||
`SmartBox::UseTime` holding position completion while
|
`SmartBox::UseTime` holding position completion while
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,9 @@ src/AcDream.App/
|
||||||
│ └── SelectionInteractionController.cs # owns selection intents, Use/PickUp, auto-walk deferral
|
│ └── SelectionInteractionController.cs # owns selection intents, Use/PickUp, auto-walk deferral
|
||||||
├── Streaming/
|
├── Streaming/
|
||||||
│ ├── LandblockPresentationPipeline.cs # DAT build/apply/retire transaction and resource publication
|
│ ├── 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)
|
├── Input/ # (already exists)
|
||||||
├── Audio/ # (already exists)
|
├── Audio/ # (already exists)
|
||||||
└── Plugins/ # (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. |
|
| 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`). |
|
| 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`). |
|
| 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. |
|
| 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. |
|
| 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`). |
|
| 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-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal `elapsed >= 1.0` state edge. Incoming fades retain the exact timer. | `src/AcDream.Core/World/TeleportAnimSequencer.cs` (`OutgoingViewportReachedTerminalProjection`) | An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | `UIGlobals::GetAnimLevel @ 0x004EE540`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; paired retail/acdream captures documented in `docs/research/2026-07-15-retail-portal-space-pseudocode.md` |
|
||||||
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
|
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
|
||||||
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. `WorldRevealCoordinator` owns this shared lifetime; diagnostic telemetry observes its generation without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **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-3 | Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data | `src/AcDream.Core/Physics/CellTransit.cs:503` | Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk | Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building | `CObjCell::find_cell_list` 0052b535-0052b56c (null-CLandCell case) |
|
||||||
| AD-4 | `point_in_cell` against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells | `src/AcDream.Core/Physics/CellTransit.cs:588` | The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice | During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root | `CEnvCell::find_visible_child_cell` :311397; cell-BSP vtable[0x84] |
|
| AD-4 | `point_in_cell` against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells | `src/AcDream.Core/Physics/CellTransit.cs:588` | The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice | During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root | `CEnvCell::find_visible_child_cell` :311397; cell-BSP vtable[0x84] |
|
||||||
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
|
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
|
||||||
|
|
@ -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-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-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-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-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` |
|
| 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
|
> generation unavailable at the hard reveal edge and freezes its world
|
||||||
> simulation/audio while full-window and shared-origin teardown advance through
|
> simulation/audio while full-window and shared-origin teardown advance through
|
||||||
> metered resident, entity, and owner-stage cursors with exact retry receipts.
|
> metered resident, entity, and owner-stage cursors with exact retry receipts.
|
||||||
> E4 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
|
> physics, static, building-registry, EnvCell, collision, and reflood
|
||||||
> publication before one observer-atomic spatial swap. E5 destination
|
> publication before one observer-atomic spatial swap. E5 now joins the
|
||||||
> reservation/reveal joining is next. F/G and J retain their explicit approval
|
> canonical reveal generation to an exact destination reservation, protects
|
||||||
> gates.
|
> 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
|
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||||
umbrella design — read it first). **Goal:** smoothness first (no frame over
|
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
|
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
||||||
production streaming, and typed unified residency. Its physical capped,
|
production streaming, and typed unified residency. Its physical capped,
|
||||||
uncapped, and dense connected gates pass without a visual-quality change.
|
uncapped, and dense connected gates pass without a visual-quality change.
|
||||||
Slice E0–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
|
stable priority queues, publish an immediate generation-scoped old-world
|
||||||
quiesce at login/portal boundaries, and advance full-window/recenter teardown
|
quiesce at login/portal boundaries, and advance full-window/recenter teardown
|
||||||
through exact metered retirement cursors. Render, physics, static, building,
|
through exact metered retirement cursors. Render, physics, static, building,
|
||||||
and EnvCell publication now resume from retained exact cursors under the same
|
and EnvCell publication now resume from retained exact cursors under the same
|
||||||
frame budget. E5 destination reservation and reveal-generation joining is
|
frame budget. The reveal coordinator now owns the exact destination
|
||||||
next. Slices F/G remain behind their explicit ECS approval gate, and Slice J
|
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.
|
remains behind its gameplay-owner approval gate.
|
||||||
|
|
||||||
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2's
|
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2's
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
**Date:** 2026-07-24
|
**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
|
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
||||||
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
||||||
|
|
@ -729,9 +729,13 @@ checkpoint retained staged or retiring bytes. Evidence:
|
||||||
**Checkpoint 2026-07-24:** E0 fixed the retail/adaptation contract and current
|
**Checkpoint 2026-07-24:** E0 fixed the retail/adaptation contract and current
|
||||||
owner inventory. E1 landed the validated work profile, deterministic meter and
|
owner inventory. E1 landed the validated work profile, deterministic meter and
|
||||||
completion charge model. E2 cut execution over to stable priority FIFOs. E3
|
completion charge model. E2 cut execution over to stable priority FIFOs. E3
|
||||||
added immediate generation quiescence and cursored retirement. E4 now advances
|
added immediate generation quiescence and cursored retirement. E4 advances
|
||||||
render/physics/static publication from retained exact receipts; E5 destination
|
render/physics/static publication from retained exact receipts. E5 replaced
|
||||||
reservation and reveal-generation joining is next.
|
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.
|
**Purpose:** Remove update-thread portal transactions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
|
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
|
||||||
|
|
||||||
**Status:** active — E0/E1/E2/E3/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`
|
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||||
**Baseline:** Slice D closeout commit `66690805`
|
**Baseline:** Slice D closeout commit `66690805`
|
||||||
**Behavior contract:** no visual-quality or retail-behavior reduction
|
**Behavior contract:** no visual-quality or retail-behavior reduction
|
||||||
|
|
@ -356,6 +356,35 @@ performance and visual evidence remains an E6 gate.
|
||||||
five seconds.
|
five seconds.
|
||||||
- Remove the last count-only execution/bypass paths.
|
- 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
|
### E6 — Gates and closeout
|
||||||
|
|
||||||
- Deterministic pressure, reentrancy, failure, cancellation, stale-generation,
|
- Deterministic pressure, reentrancy, failure, cancellation, stale-generation,
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ Required asynchronous equivalent:
|
||||||
```text
|
```text
|
||||||
WorldRevealReady(destination):
|
WorldRevealReady(destination):
|
||||||
if destination claim cannot be hydrated:
|
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:
|
if destination is indoor:
|
||||||
return render center landblock Near-tier and ready
|
return render center landblock Near-tier and ready
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
- `CObjectMaint::DestroyObjects @ 0x00508C30`
|
- `CObjectMaint::DestroyObjects @ 0x00508C30`
|
||||||
- `CObjectMaint::AddObjectToBeDestroyed @ 0x00508F70`
|
- `CObjectMaint::AddObjectToBeDestroyed @ 0x00508F70`
|
||||||
- `SmartBox::UseTime @ 0x00455410`
|
- `SmartBox::UseTime @ 0x00455410`
|
||||||
|
- `gmSmartBoxUI::UseTime @ 0x004D6E30`
|
||||||
|
|
||||||
The Sept 2013 named pseudo-C is the behavioral oracle. WorldBuilder has no
|
The Sept 2013 named pseudo-C is the behavioral oracle. WorldBuilder has no
|
||||||
equivalent live-client teardown scheduler; its extracted mesh/cache owners are
|
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
|
dispatch and the portal presentation continue. This is the retail basis for
|
||||||
Slice E's immediate old-generation quiesce.
|
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
|
## Acdream asynchronous adaptation
|
||||||
|
|
||||||
Retail performs each teardown synchronously and can synchronously block cell
|
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 Near scene ready
|
||||||
required composite textures ready
|
required composite textures ready
|
||||||
camera/player identity belongs to destination generation
|
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
|
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",
|
"portal depth mask",
|
||||||
() => new PortalDepthMaskRenderer(d.Gl),
|
() => new PortalDepthMaskRenderer(d.Gl),
|
||||||
static value => value.Dispose());
|
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;
|
PortalTunnelPresentation portalTunnel;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -840,7 +854,9 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
d.HookRouter,
|
d.HookRouter,
|
||||||
dispatcherLease.Resource,
|
dispatcherLease.Resource,
|
||||||
foundation.SceneLighting,
|
foundation.SceneLighting,
|
||||||
foundation.MeshAdapter),
|
foundation.MeshAdapter,
|
||||||
|
displayPortalWaitNotice,
|
||||||
|
portalWaitNoticeLease?.Resource),
|
||||||
static tunnel => tunnel.PrepareResources());
|
static tunnel => tunnel.PrepareResources());
|
||||||
}
|
}
|
||||||
catch (Exception acquisitionFailure)
|
catch (Exception acquisitionFailure)
|
||||||
|
|
@ -1006,6 +1022,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
clipFrameLease.Transfer();
|
clipFrameLease.Transfer();
|
||||||
portalDepthLease.Transfer();
|
portalDepthLease.Transfer();
|
||||||
portalTunnelLease.Transfer();
|
portalTunnelLease.Transfer();
|
||||||
|
portalWaitNoticeLease?.Transfer();
|
||||||
skyLease.Transfer();
|
skyLease.Transfer();
|
||||||
skyShaderLease.Transfer();
|
skyShaderLease.Transfer();
|
||||||
particleLease.Transfer();
|
particleLease.Transfer();
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,8 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
|
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
|
||||||
spawnClaimClassifier.IsUnhydratable,
|
spawnClaimClassifier.IsUnhydratable,
|
||||||
d.Log,
|
d.Log,
|
||||||
worldQuiescence);
|
worldQuiescence,
|
||||||
|
streaming);
|
||||||
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
|
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
|
||||||
|
|
||||||
return CompleteSessionPlayer(
|
return CompleteSessionPlayer(
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,11 @@ internal sealed class LivePlayerModeAutoEntryContext
|
||||||
|
|
||||||
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
||||||
|
|
||||||
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
|
public void EnterPlayerMode()
|
||||||
|
{
|
||||||
|
_playerMode.EnterFromAutoEntry();
|
||||||
|
_worldReveal.Complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerModeAutoEntry
|
public sealed class PlayerModeAutoEntry
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
private readonly WorldEntity _entity;
|
private readonly WorldEntity _entity;
|
||||||
private readonly PortalTunnelCamera _camera = new();
|
private readonly PortalTunnelCamera _camera = new();
|
||||||
private readonly Random _random;
|
private readonly Random _random;
|
||||||
private readonly Action<string>? _displayNotice;
|
private readonly Action<string?>? _displayNotice;
|
||||||
|
private IDisposable? _displayNoticeLifetime;
|
||||||
private readonly PortalMeshReferenceOwner _meshReferences;
|
private readonly PortalMeshReferenceOwner _meshReferences;
|
||||||
|
|
||||||
private bool _visible;
|
private bool _visible;
|
||||||
|
|
@ -62,6 +63,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
private float _rotationStartAngle;
|
private float _rotationStartAngle;
|
||||||
private float _rotationEndAngle;
|
private float _rotationEndAngle;
|
||||||
private float _rotationCurrentAngle;
|
private float _rotationCurrentAngle;
|
||||||
|
private bool _waitCueVisible;
|
||||||
private bool _disposeRequested;
|
private bool _disposeRequested;
|
||||||
private bool _disposing;
|
private bool _disposing;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -77,7 +79,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
IAnimationLoader animationLoader,
|
IAnimationLoader animationLoader,
|
||||||
IAnimationHookSink hookSink,
|
IAnimationHookSink hookSink,
|
||||||
Random random,
|
Random random,
|
||||||
Action<string>? displayNotice)
|
Action<string?>? displayNotice,
|
||||||
|
IDisposable? displayNoticeLifetime)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
|
|
@ -90,6 +93,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
_sequence.HookObj = _animationHooks;
|
_sequence.HookObj = _animationHooks;
|
||||||
_random = random;
|
_random = random;
|
||||||
_displayNotice = displayNotice;
|
_displayNotice = displayNotice;
|
||||||
|
_displayNoticeLifetime = displayNoticeLifetime;
|
||||||
|
|
||||||
_entity = new WorldEntity
|
_entity = new WorldEntity
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +123,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
WbDrawDispatcher dispatcher,
|
WbDrawDispatcher dispatcher,
|
||||||
SceneLightingUboBinding lightUbo,
|
SceneLightingUboBinding lightUbo,
|
||||||
IWbMeshAdapter meshAdapter,
|
IWbMeshAdapter meshAdapter,
|
||||||
Action<string>? displayNotice = null,
|
Action<string?>? displayNotice = null,
|
||||||
|
IDisposable? displayNoticeLifetime = null,
|
||||||
Random? random = null)
|
Random? random = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(gl);
|
ArgumentNullException.ThrowIfNull(gl);
|
||||||
|
|
@ -154,7 +159,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
animationLoader,
|
animationLoader,
|
||||||
hookSink,
|
hookSink,
|
||||||
random ?? Random.Shared,
|
random ?? Random.Shared,
|
||||||
displayNotice);
|
displayNotice,
|
||||||
|
displayNoticeLifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void EnsureRequiredAssets(
|
internal static void EnsureRequiredAssets(
|
||||||
|
|
@ -211,6 +217,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
_rotationEndAngle = 0f;
|
_rotationEndAngle = 0f;
|
||||||
_rotationCurrentAngle = 0f;
|
_rotationCurrentAngle = 0f;
|
||||||
_camera.DirectionDegrees = 0f;
|
_camera.DirectionDegrees = 0f;
|
||||||
|
SetWaitCue(false);
|
||||||
_visible = true;
|
_visible = true;
|
||||||
RebuildPose();
|
RebuildPose();
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +228,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_visible = false;
|
_visible = false;
|
||||||
|
SetWaitCue(false);
|
||||||
_animationHooks.Clear();
|
_animationHooks.Clear();
|
||||||
_sequence.ClearAnimations();
|
_sequence.ClearAnimations();
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +244,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
TickRotation(dt);
|
TickRotation(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetWaitCue(bool visible)
|
||||||
|
{
|
||||||
|
if (_waitCueVisible == visible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_waitCueVisible = visible;
|
||||||
|
_displayNotice?.Invoke(
|
||||||
|
visible ? "In Portal Space - Please Wait..." : null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw retail portal space into the active viewport. The caller suppresses
|
/// Draw retail portal space into the active viewport. The caller suppresses
|
||||||
/// the normal world viewport while this scene is visible, then draws the
|
/// 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);
|
_rotationDuration = NextDouble(RotationDurationMin, RotationDurationMax);
|
||||||
_rotationStartAngle = _rotationCurrentAngle;
|
_rotationStartAngle = _rotationCurrentAngle;
|
||||||
_rotationEndAngle = (float)NextDouble(0.0, 360.0);
|
_rotationEndAngle = (float)NextDouble(0.0, 360.0);
|
||||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
if (_waitCueVisible)
|
||||||
|
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -389,11 +408,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_visible = false;
|
_visible = false;
|
||||||
|
SetWaitCue(false);
|
||||||
_animationHooks.Clear();
|
_animationHooks.Clear();
|
||||||
_sequence.ClearAnimations();
|
_sequence.ClearAnimations();
|
||||||
_meshReferences.Dispose();
|
_meshReferences.Dispose();
|
||||||
if (_meshReferences.IsDisposed)
|
if (_meshReferences.IsDisposed)
|
||||||
|
{
|
||||||
|
_displayNoticeLifetime?.Dispose();
|
||||||
|
_displayNoticeLifetime = null;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,6 @@ internal interface ILocalPlayerTeleportStreamingOperations
|
||||||
bool BeginRecenter(int x, int y, bool isSealedDungeon);
|
bool BeginRecenter(int x, int y, bool isSealedDungeon);
|
||||||
bool ResetRecenter(bool sessionEnding);
|
bool ResetRecenter(bool sessionEnding);
|
||||||
bool IsSealedDungeon(uint cellId);
|
bool IsSealedDungeon(uint cellId);
|
||||||
void SetPriority(uint landblockId, int radius);
|
|
||||||
void ClearPriority();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class LocalPlayerTeleportStreamingOperations
|
internal sealed class LocalPlayerTeleportStreamingOperations
|
||||||
|
|
@ -165,22 +163,11 @@ internal sealed class LocalPlayerTeleportStreamingOperations
|
||||||
public bool IsSealedDungeon(uint cellId) =>
|
public bool IsSealedDungeon(uint cellId) =>
|
||||||
_sealedDungeonCells.IsSealedDungeon(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
|
internal interface ILocalPlayerTeleportPlacement
|
||||||
{
|
{
|
||||||
void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced);
|
void Place(Vector3 position, uint cellId, Quaternion rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -218,7 +205,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
||||||
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
_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
|
PlayerMovementController controller = _controller.Controller
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|
@ -233,13 +220,6 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
||||||
resolved.Position.Y,
|
resolved.Position.Y,
|
||||||
resolved.Position.Z);
|
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;
|
uint playerGuid = _identity.ServerGuid;
|
||||||
controller.SetPosition(
|
controller.SetPosition(
|
||||||
snapped,
|
snapped,
|
||||||
|
|
@ -270,7 +250,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
||||||
PhysicsDiagnostics.LogTeleport(
|
PhysicsDiagnostics.LogTeleport(
|
||||||
"PLACED",
|
"PLACED",
|
||||||
controller.CellId,
|
controller.CellId,
|
||||||
$"forced={forced}");
|
"readiness=complete");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"live: teleport materialized - snapped to {controller.Position} "
|
$"live: teleport materialized - snapped to {controller.Position} "
|
||||||
+ $"cell=0x{controller.CellId:X8}");
|
+ $"cell=0x{controller.CellId:X8}");
|
||||||
|
|
@ -315,6 +295,7 @@ internal interface ILocalPlayerTeleportPresentation : IDisposable
|
||||||
void TickTunnel(float deltaSeconds);
|
void TickTunnel(float deltaSeconds);
|
||||||
void EnterTunnel();
|
void EnterTunnel();
|
||||||
void ExitTunnel();
|
void ExitTunnel();
|
||||||
|
void SetWaitCue(bool visible);
|
||||||
void Reset();
|
void Reset();
|
||||||
Matrix4x4 ApplyViewPlane(Matrix4x4 projection);
|
Matrix4x4 ApplyViewPlane(Matrix4x4 projection);
|
||||||
ICamera ApplyViewPlane(ICamera camera);
|
ICamera ApplyViewPlane(ICamera camera);
|
||||||
|
|
@ -354,6 +335,7 @@ internal sealed class LocalPlayerTeleportPresentation
|
||||||
public void TickTunnel(float deltaSeconds) => _tunnel.Tick(deltaSeconds);
|
public void TickTunnel(float deltaSeconds) => _tunnel.Tick(deltaSeconds);
|
||||||
public void EnterTunnel() => _tunnel.Enter();
|
public void EnterTunnel() => _tunnel.Enter();
|
||||||
public void ExitTunnel() => _tunnel.Exit();
|
public void ExitTunnel() => _tunnel.Exit();
|
||||||
|
public void SetWaitCue(bool visible) => _tunnel.SetWaitCue(visible);
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
|
@ -384,10 +366,6 @@ internal sealed class LocalPlayerTeleportController
|
||||||
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
internal const float MaximumWorldHoldSeconds = 10f;
|
|
||||||
internal const int NearRingRadius =
|
|
||||||
WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius;
|
|
||||||
|
|
||||||
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
||||||
private readonly ILocalPlayerTeleportAuthority _authority;
|
private readonly ILocalPlayerTeleportAuthority _authority;
|
||||||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||||
|
|
@ -404,7 +382,6 @@ internal sealed class LocalPlayerTeleportController
|
||||||
private bool _hasAcceptedDestination;
|
private bool _hasAcceptedDestination;
|
||||||
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
||||||
private float _holdSeconds;
|
private float _holdSeconds;
|
||||||
private bool _forced;
|
|
||||||
private long _lifetimeGeneration;
|
private long _lifetimeGeneration;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -496,15 +473,13 @@ internal sealed class LocalPlayerTeleportController
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (haveDestination && originReady && !ready)
|
if (haveDestination && !ready)
|
||||||
{
|
|
||||||
_holdSeconds += deltaSeconds;
|
_holdSeconds += deltaSeconds;
|
||||||
if (_holdSeconds >= MaximumWorldHoldSeconds)
|
_presentation.SetWaitCue(
|
||||||
{
|
haveDestination
|
||||||
ready = true;
|
&& !ready
|
||||||
_forced = true;
|
&& _worldReveal.ObserveWait(
|
||||||
}
|
TimeSpan.FromSeconds(_holdSeconds)));
|
||||||
}
|
|
||||||
|
|
||||||
var (_, events) = _presentation.Tick(deltaSeconds, ready);
|
var (_, events) = _presentation.Tick(deltaSeconds, ready);
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
|
|
@ -518,14 +493,10 @@ internal sealed class LocalPlayerTeleportController
|
||||||
_placement.Place(
|
_placement.Place(
|
||||||
_pendingPosition,
|
_pendingPosition,
|
||||||
_pendingCell,
|
_pendingCell,
|
||||||
_pendingRotation,
|
_pendingRotation);
|
||||||
_forced);
|
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return;
|
return;
|
||||||
_worldReveal.ObserveMaterialized();
|
_worldReveal.ObserveMaterialized();
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
|
||||||
return;
|
|
||||||
_streaming.ClearPriority();
|
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
|
|
@ -590,7 +561,6 @@ internal sealed class LocalPlayerTeleportController
|
||||||
}
|
}
|
||||||
|
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
_forced = false;
|
|
||||||
_presentation.Begin(_mode.Projection);
|
_presentation.Begin(_mode.Projection);
|
||||||
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
||||||
return;
|
return;
|
||||||
|
|
@ -677,7 +647,7 @@ internal sealed class LocalPlayerTeleportController
|
||||||
// Retail SmartBox enters blocking_for_cells before old-world object,
|
// Retail SmartBox enters blocking_for_cells before old-world object,
|
||||||
// physics, landscape, and ambient owners may advance again. Begin the
|
// physics, landscape, and ambient owners may advance again. Begin the
|
||||||
// reveal generation before a recenter can start detaching that world.
|
// reveal generation before a recenter can start detaching that world.
|
||||||
_worldReveal.Begin(WorldRevealKind.Portal);
|
_worldReveal.Begin(WorldRevealKind.Portal, position.LandblockId);
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -713,13 +683,6 @@ internal sealed class LocalPlayerTeleportController
|
||||||
_pendingPosition = worldPosition;
|
_pendingPosition = worldPosition;
|
||||||
_pendingCell = position.LandblockId;
|
_pendingCell = position.LandblockId;
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
_forced = false;
|
|
||||||
_streaming.SetPriority(
|
|
||||||
StreamingRegion.EncodeLandblockId(landblockX, landblockY),
|
|
||||||
NearRingRadius);
|
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
PhysicsDiagnostics.LogTeleport(
|
PhysicsDiagnostics.LogTeleport(
|
||||||
"AIM",
|
"AIM",
|
||||||
position.LandblockId,
|
position.LandblockId,
|
||||||
|
|
@ -748,25 +711,19 @@ internal sealed class LocalPlayerTeleportController
|
||||||
_hasAcceptedDestination = false;
|
_hasAcceptedDestination = false;
|
||||||
_acceptedDestination = default;
|
_acceptedDestination = default;
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
_forced = false;
|
|
||||||
|
|
||||||
_streaming.ResetRecenter(clearSession);
|
_streaming.ResetRecenter(clearSession);
|
||||||
if (_lifetimeGeneration != generation)
|
if (_lifetimeGeneration != generation)
|
||||||
return generation;
|
return generation;
|
||||||
|
|
||||||
if (clearSession)
|
_worldReveal.Cancel();
|
||||||
{
|
if (_lifetimeGeneration != generation)
|
||||||
_worldReveal.Cancel();
|
return generation;
|
||||||
if (_lifetimeGeneration != generation)
|
|
||||||
return generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
_presentation.Reset();
|
_presentation.Reset();
|
||||||
if (_lifetimeGeneration != generation)
|
if (_lifetimeGeneration != generation)
|
||||||
return generation;
|
return generation;
|
||||||
|
|
||||||
_streaming.ClearPriority();
|
|
||||||
|
|
||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ internal readonly record struct StreamingQueuedCompletion(
|
||||||
LandblockStreamResult Result,
|
LandblockStreamResult Result,
|
||||||
LandblockStreamCostEstimate Estimate,
|
LandblockStreamCostEstimate Estimate,
|
||||||
StreamingCompletionPriority Priority,
|
StreamingCompletionPriority Priority,
|
||||||
|
long RevealGeneration,
|
||||||
ulong Generation,
|
ulong Generation,
|
||||||
long Sequence,
|
long Sequence,
|
||||||
long EnqueuedTimestamp);
|
long EnqueuedTimestamp);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,15 @@ namespace AcDream.App.Streaming;
|
||||||
/// Threading: not thread-safe. All calls must happen on the render thread.
|
/// Threading: not thread-safe. All calls must happen on the render thread.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// </summary>
|
/// </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
|
private sealed class OriginRecenterRetirement
|
||||||
{
|
{
|
||||||
public bool RadiiConverged;
|
public bool RadiiConverged;
|
||||||
|
|
@ -61,6 +68,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
private readonly StreamingCompletionQueue _completionQueue = new();
|
private readonly StreamingCompletionQueue _completionQueue = new();
|
||||||
private long _nextCompletionSequence;
|
private long _nextCompletionSequence;
|
||||||
private int _legacyCompletionProfile = 4;
|
private int _legacyCompletionProfile = 4;
|
||||||
|
private DestinationReservation? _destinationReservation;
|
||||||
|
|
||||||
private readonly GpuWorldState _state;
|
private readonly GpuWorldState _state;
|
||||||
private StreamingRegion? _region;
|
private StreamingRegion? _region;
|
||||||
|
|
@ -120,24 +128,54 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
internal long ActiveRevealGeneration =>
|
||||||
/// #138: the teleport destination landblock id. Loaded or promoted results
|
_destinationReservation?.RevealGeneration ?? 0L;
|
||||||
/// 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; }
|
|
||||||
|
|
||||||
/// <summary>
|
internal uint DestinationLandblockId =>
|
||||||
/// Radius in landblocks (Chebyshev distance) around
|
_destinationReservation?.LandblockId ?? 0u;
|
||||||
/// <see cref="PriorityLandblockId"/> classified as destination work.
|
|
||||||
/// Zero means only the center. This changes queue priority, never budget
|
internal int DestinationRadius =>
|
||||||
/// enforcement or atomic publication semantics.
|
_destinationReservation?.Radius ?? 0;
|
||||||
/// </summary>
|
|
||||||
public int PriorityRadius { get; set; }
|
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
|
// [FRAME-DIAG] (read by GameWindow's ACDREAM_WB_DIAG rollup): the standing
|
||||||
// accepted completion backlog. A non-zero value during/after a teleport is
|
// 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++)
|
for (int i = 0; i < pending.Count; i++)
|
||||||
{
|
{
|
||||||
LandblockStreamResult result = pending[i];
|
LandblockStreamResult result = pending[i];
|
||||||
|
using StreamingWorkMeter.LaneScope lane =
|
||||||
|
_activeWorkMeter.EnterLane(
|
||||||
|
IsDestinationWork(result.LandblockId)
|
||||||
|
? StreamingWorkLane.Destination
|
||||||
|
: StreamingWorkLane.NonDestination);
|
||||||
LandblockPublicationAdvance advance =
|
LandblockPublicationAdvance advance =
|
||||||
_presentation.ResumePublication(
|
_presentation.ResumePublication(
|
||||||
result,
|
result,
|
||||||
|
|
@ -549,7 +592,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"StreamingController.Tick cannot be reentered.");
|
"StreamingController.Tick cannot be reentered.");
|
||||||
|
|
||||||
var meter = new StreamingWorkMeter(_workBudget);
|
var meter = new StreamingWorkMeter(
|
||||||
|
_workBudget,
|
||||||
|
destinationReservationActive: _destinationReservation is not null);
|
||||||
_activeWorkMeter = meter;
|
_activeWorkMeter = meter;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -901,15 +946,13 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when <paramref name="id"/> is the priority center or within
|
/// True when <paramref name="id"/> belongs to the canonical reveal
|
||||||
/// <see cref="PriorityRadius"/> landblocks of it (Chebyshev) — i.e. inside the teleport
|
/// generation's destination neighborhood.
|
||||||
/// 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).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool IsWithinPriorityRing(uint id)
|
private bool IsDestinationWork(uint id)
|
||||||
=> PriorityLandblockId != 0u
|
=> _destinationReservation is { } reservation
|
||||||
&& ChebyshevLandblocks(id, PriorityLandblockId) <= PriorityRadius;
|
&& ChebyshevLandblocks(id, reservation.LandblockId)
|
||||||
|
<= reservation.Radius;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dungeon-exit edge (portal to outdoors / teleport): rebuild the full
|
/// 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.");
|
"The completion queue returned a null head.");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||||
|
work.Priority == StreamingCompletionPriority.Destination
|
||||||
|
&& work.RevealGeneration == ActiveRevealGeneration
|
||||||
|
? StreamingWorkLane.Destination
|
||||||
|
: StreamingWorkLane.NonDestination);
|
||||||
LandblockPublicationAdvance advance = ApplyResult(
|
LandblockPublicationAdvance advance = ApplyResult(
|
||||||
work,
|
work,
|
||||||
meter,
|
meter,
|
||||||
|
|
@ -1554,6 +1602,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
"The completion source returned a null peek.");
|
"The completion source returned a null peek.");
|
||||||
bool stale = IsStaleGeneration(result)
|
bool stale = IsStaleGeneration(result)
|
||||||
&& !_presentation.HasPendingPublication(result);
|
&& !_presentation.HasPendingPublication(result);
|
||||||
|
StreamingCompletionPriority priority = stale
|
||||||
|
? StreamingCompletionPriority.Control
|
||||||
|
: ClassifyCompletion(result);
|
||||||
LandblockStreamCostEstimate estimate =
|
LandblockStreamCostEstimate estimate =
|
||||||
LandblockStreamResultCost.Estimate(result);
|
LandblockStreamResultCost.Estimate(result);
|
||||||
StreamingWorkCost admissionCost = stale
|
StreamingWorkCost admissionCost = stale
|
||||||
|
|
@ -1563,6 +1614,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
estimate.Work.CompletionAdmissions,
|
estimate.Work.CompletionAdmissions,
|
||||||
AdoptedCpuBytes:
|
AdoptedCpuBytes:
|
||||||
estimate.Work.AdoptedCpuBytes);
|
estimate.Work.AdoptedCpuBytes);
|
||||||
|
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||||
|
priority == StreamingCompletionPriority.Destination
|
||||||
|
? StreamingWorkLane.Destination
|
||||||
|
: StreamingWorkLane.NonDestination);
|
||||||
StreamingWorkAdmission admission = meter.TryReserve(
|
StreamingWorkAdmission admission = meter.TryReserve(
|
||||||
admissionCost,
|
admissionCost,
|
||||||
"completion-admission");
|
"completion-admission");
|
||||||
|
|
@ -1584,7 +1639,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
_completionQueue.Enqueue(new StreamingQueuedCompletion(
|
_completionQueue.Enqueue(new StreamingQueuedCompletion(
|
||||||
result,
|
result,
|
||||||
estimate,
|
estimate,
|
||||||
ClassifyCompletion(result),
|
priority,
|
||||||
|
priority == StreamingCompletionPriority.Destination
|
||||||
|
? ActiveRevealGeneration
|
||||||
|
: 0L,
|
||||||
result.Generation,
|
result.Generation,
|
||||||
checked(_nextCompletionSequence++),
|
checked(_nextCompletionSequence++),
|
||||||
Stopwatch.GetTimestamp()));
|
Stopwatch.GetTimestamp()));
|
||||||
|
|
@ -1604,7 +1662,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
||||||
{
|
{
|
||||||
if (result is LandblockStreamResult.Loaded
|
if (result is LandblockStreamResult.Loaded
|
||||||
or LandblockStreamResult.Promoted
|
or LandblockStreamResult.Promoted
|
||||||
&& IsWithinPriorityRing(result.LandblockId))
|
&& IsDestinationWork(result.LandblockId))
|
||||||
{
|
{
|
||||||
return StreamingCompletionPriority.Destination;
|
return StreamingCompletionPriority.Destination;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,11 +135,19 @@ public enum StreamingWorkLimit : byte
|
||||||
GlRetireOperations,
|
GlRetireOperations,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum StreamingWorkLane : byte
|
||||||
|
{
|
||||||
|
NonDestination,
|
||||||
|
Destination,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable observation of one frame's streaming work.
|
/// Immutable observation of one frame's streaming work.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct StreamingWorkMeterSnapshot(
|
public readonly record struct StreamingWorkMeterSnapshot(
|
||||||
StreamingWorkCost Used,
|
StreamingWorkCost Used,
|
||||||
|
StreamingWorkCost DestinationUsed,
|
||||||
|
StreamingWorkCost NonDestinationUsed,
|
||||||
int Operations,
|
int Operations,
|
||||||
int CompletedOperations,
|
int CompletedOperations,
|
||||||
int YieldCount,
|
int YieldCount,
|
||||||
|
|
@ -177,7 +185,14 @@ public sealed class StreamingWorkMeter
|
||||||
private readonly Func<long> _timestamp;
|
private readonly Func<long> _timestamp;
|
||||||
private readonly long _frequency;
|
private readonly long _frequency;
|
||||||
private readonly long _start;
|
private readonly long _start;
|
||||||
|
private readonly bool _destinationReservationActive;
|
||||||
private StreamingWorkCost _used;
|
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 _operations;
|
||||||
private int _completed;
|
private int _completed;
|
||||||
private int _yields;
|
private int _yields;
|
||||||
|
|
@ -191,15 +206,22 @@ public sealed class StreamingWorkMeter
|
||||||
private string? _lastStage;
|
private string? _lastStage;
|
||||||
private StreamingWorkLimit _lastLimit;
|
private StreamingWorkLimit _lastLimit;
|
||||||
|
|
||||||
public StreamingWorkMeter(StreamingWorkBudget budget)
|
public StreamingWorkMeter(
|
||||||
: this(budget, Stopwatch.GetTimestamp, Stopwatch.Frequency)
|
StreamingWorkBudget budget,
|
||||||
|
bool destinationReservationActive = false)
|
||||||
|
: this(
|
||||||
|
budget,
|
||||||
|
Stopwatch.GetTimestamp,
|
||||||
|
Stopwatch.Frequency,
|
||||||
|
destinationReservationActive)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamingWorkMeter(
|
public StreamingWorkMeter(
|
||||||
StreamingWorkBudget budget,
|
StreamingWorkBudget budget,
|
||||||
Func<long> timestamp,
|
Func<long> timestamp,
|
||||||
long timestampFrequency)
|
long timestampFrequency,
|
||||||
|
bool destinationReservationActive = false)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(timestamp);
|
ArgumentNullException.ThrowIfNull(timestamp);
|
||||||
if (timestampFrequency <= 0)
|
if (timestampFrequency <= 0)
|
||||||
|
|
@ -210,6 +232,18 @@ public sealed class StreamingWorkMeter
|
||||||
_timestamp = timestamp;
|
_timestamp = timestamp;
|
||||||
_frequency = timestampFrequency;
|
_frequency = timestampFrequency;
|
||||||
_start = timestamp();
|
_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(
|
public StreamingWorkAdmission TryReserve(
|
||||||
|
|
@ -237,9 +271,14 @@ public sealed class StreamingWorkMeter
|
||||||
}
|
}
|
||||||
|
|
||||||
_used = _used.Add(cost);
|
_used = _used.Add(cost);
|
||||||
|
if (_lane == StreamingWorkLane.Destination)
|
||||||
|
_destinationUsed = _destinationUsed.Add(cost);
|
||||||
|
else
|
||||||
|
_nonDestinationUsed = _nonDestinationUsed.Add(cost);
|
||||||
_operations++;
|
_operations++;
|
||||||
_reservationActive = true;
|
_reservationActive = true;
|
||||||
_activeStage = stage;
|
_activeStage = stage;
|
||||||
|
_activeOperationStart = _timestamp();
|
||||||
if (ensureProgress)
|
if (ensureProgress)
|
||||||
_ensuredProgressGranted = true;
|
_ensuredProgressGranted = true;
|
||||||
if (limit != StreamingWorkLimit.None)
|
if (limit != StreamingWorkLimit.None)
|
||||||
|
|
@ -258,31 +297,10 @@ public sealed class StreamingWorkMeter
|
||||||
return StreamingWorkAdmission.Admitted;
|
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()
|
public void Complete()
|
||||||
{
|
{
|
||||||
EnsureReservation();
|
EnsureReservation();
|
||||||
|
CompleteLaneTiming();
|
||||||
_completed++;
|
_completed++;
|
||||||
_reservationActive = false;
|
_reservationActive = false;
|
||||||
_activeStage = null;
|
_activeStage = null;
|
||||||
|
|
@ -292,6 +310,7 @@ public sealed class StreamingWorkMeter
|
||||||
public void Fail()
|
public void Fail()
|
||||||
{
|
{
|
||||||
EnsureReservation();
|
EnsureReservation();
|
||||||
|
CompleteLaneTiming();
|
||||||
_failures++;
|
_failures++;
|
||||||
_reservationActive = false;
|
_reservationActive = false;
|
||||||
_activeStage = null;
|
_activeStage = null;
|
||||||
|
|
@ -313,6 +332,8 @@ public sealed class StreamingWorkMeter
|
||||||
long now = _timestamp();
|
long now = _timestamp();
|
||||||
return new StreamingWorkMeterSnapshot(
|
return new StreamingWorkMeterSnapshot(
|
||||||
_used,
|
_used,
|
||||||
|
_destinationUsed,
|
||||||
|
_nonDestinationUsed,
|
||||||
_operations,
|
_operations,
|
||||||
_completed,
|
_completed,
|
||||||
_yields,
|
_yields,
|
||||||
|
|
@ -348,9 +369,105 @@ public sealed class StreamingWorkMeter
|
||||||
{
|
{
|
||||||
return StreamingWorkLimit.GlRetireOperations;
|
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;
|
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()
|
private void EnsureReservation()
|
||||||
{
|
{
|
||||||
if (!_reservationActive)
|
if (!_reservationActive)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
namespace AcDream.App.Streaming;
|
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>
|
/// <summary>
|
||||||
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
||||||
/// diagnostics. This keeps the destination barrier and its observations on a
|
/// 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 WorldRevealReadinessBarrier _readiness;
|
||||||
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
||||||
private readonly WorldGenerationQuiescence? _quiescence;
|
private readonly WorldGenerationQuiescence? _quiescence;
|
||||||
|
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||||
private long _activeGeneration;
|
private long _activeGeneration;
|
||||||
|
|
||||||
public WorldRevealCoordinator(
|
public WorldRevealCoordinator(
|
||||||
|
|
@ -22,7 +38,8 @@ internal sealed class WorldRevealCoordinator
|
||||||
Action invalidateCompositeTextures,
|
Action invalidateCompositeTextures,
|
||||||
Func<uint, bool> isSpawnClaimUnhydratable,
|
Func<uint, bool> isSpawnClaimUnhydratable,
|
||||||
Action<string>? log = null,
|
Action<string>? log = null,
|
||||||
WorldGenerationQuiescence? quiescence = null)
|
WorldGenerationQuiescence? quiescence = null,
|
||||||
|
IWorldRevealStreamingScheduler? streaming = null)
|
||||||
{
|
{
|
||||||
_readiness = new WorldRevealReadinessBarrier(
|
_readiness = new WorldRevealReadinessBarrier(
|
||||||
isRenderNeighborhoodReady,
|
isRenderNeighborhoodReady,
|
||||||
|
|
@ -34,17 +51,26 @@ internal sealed class WorldRevealCoordinator
|
||||||
isSpawnClaimUnhydratable);
|
isSpawnClaimUnhydratable);
|
||||||
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
||||||
_quiescence = quiescence;
|
_quiescence = quiescence;
|
||||||
|
_streaming = streaming;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
||||||
public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount;
|
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();
|
_readiness.Begin();
|
||||||
long generation = _lifecycle.Begin(kind);
|
long generation = _lifecycle.Begin(kind);
|
||||||
_activeGeneration = generation;
|
_activeGeneration = generation;
|
||||||
_quiescence?.Begin(generation);
|
_quiescence?.Begin(generation);
|
||||||
|
_streaming?.BeginDestinationReservation(
|
||||||
|
generation,
|
||||||
|
destinationCell,
|
||||||
|
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +96,9 @@ internal sealed class WorldRevealCoordinator
|
||||||
public void ObserveWorldViewportVisible() =>
|
public void ObserveWorldViewportVisible() =>
|
||||||
_lifecycle.ObserveWorldViewportVisible();
|
_lifecycle.ObserveWorldViewportVisible();
|
||||||
|
|
||||||
|
public bool ObserveWait(TimeSpan elapsed) =>
|
||||||
|
_lifecycle.ObserveWait(elapsed);
|
||||||
|
|
||||||
public void Complete()
|
public void Complete()
|
||||||
{
|
{
|
||||||
_lifecycle.Complete();
|
_lifecycle.Complete();
|
||||||
|
|
@ -89,6 +118,7 @@ internal sealed class WorldRevealCoordinator
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_activeGeneration = 0;
|
_activeGeneration = 0;
|
||||||
|
_streaming?.EndDestinationReservation(generation);
|
||||||
_quiescence?.End(generation);
|
_quiescence?.End(generation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,14 @@ internal readonly record struct WorldRevealLifecycleSnapshot(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class WorldRevealLifecycleTelemetry
|
internal sealed class WorldRevealLifecycleTelemetry
|
||||||
{
|
{
|
||||||
|
internal static readonly TimeSpan RetailWaitCueDelay =
|
||||||
|
TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
private readonly Action<string> _log;
|
private readonly Action<string> _log;
|
||||||
private WorldRevealLifecycleSnapshot _snapshot;
|
private WorldRevealLifecycleSnapshot _snapshot;
|
||||||
private long _nextGeneration;
|
private long _nextGeneration;
|
||||||
private int _portalMaterializationCount;
|
private int _portalMaterializationCount;
|
||||||
|
private bool _waitCueShown;
|
||||||
|
|
||||||
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
|
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
|
||||||
{
|
{
|
||||||
|
|
@ -42,6 +46,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
||||||
|
|
||||||
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
|
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
|
||||||
public int PortalMaterializationCount => _portalMaterializationCount;
|
public int PortalMaterializationCount => _portalMaterializationCount;
|
||||||
|
public bool WaitCueShown => _waitCueShown;
|
||||||
|
|
||||||
public long Begin(WorldRevealKind kind)
|
public long Begin(WorldRevealKind kind)
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +58,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
||||||
}
|
}
|
||||||
|
|
||||||
long generation = checked(++_nextGeneration);
|
long generation = checked(++_nextGeneration);
|
||||||
|
_waitCueShown = false;
|
||||||
_snapshot = new WorldRevealLifecycleSnapshot(
|
_snapshot = new WorldRevealLifecycleSnapshot(
|
||||||
generation,
|
generation,
|
||||||
kind,
|
kind,
|
||||||
|
|
@ -66,6 +72,27 @@ internal sealed class WorldRevealLifecycleTelemetry
|
||||||
return generation;
|
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)
|
public void ObserveReadiness(WorldRevealReadinessSnapshot readiness)
|
||||||
{
|
{
|
||||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
||||||
|
|
@ -121,7 +148,9 @@ internal sealed class WorldRevealLifecycleTelemetry
|
||||||
|
|
||||||
public void Cancel()
|
public void Cancel()
|
||||||
{
|
{
|
||||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
if (_snapshot.Generation == 0
|
||||||
|
|| _snapshot.Cancelled
|
||||||
|
|| _snapshot.Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_snapshot = _snapshot with { Cancelled = true };
|
_snapshot = _snapshot with { Cancelled = true };
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ internal sealed class WorldRevealReadinessBarrier
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True only when the same destination is drawable and collidable. An
|
/// True only when the same destination is drawable and collidable. An
|
||||||
/// impossible indoor claim intentionally crosses the barrier so the
|
/// 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>
|
/// </summary>
|
||||||
public bool IsReady(uint destinationCell)
|
public bool IsReady(uint destinationCell)
|
||||||
=> Evaluate(destinationCell).IsReady;
|
=> Evaluate(destinationCell).IsReady;
|
||||||
|
|
@ -139,7 +140,7 @@ internal sealed class WorldRevealReadinessBarrier
|
||||||
collisionReady);
|
collisionReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RequiredRenderRadius(uint destinationCell) =>
|
internal static int RequiredRenderRadius(uint destinationCell) =>
|
||||||
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
|
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
|
||||||
|
|
||||||
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
|
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,
|
lbY,
|
||||||
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
||||||
position.LandblockId));
|
position.LandblockId));
|
||||||
_worldReveal.Begin(WorldRevealKind.Login);
|
_worldReveal.Begin(WorldRevealKind.Login, position.LandblockId);
|
||||||
|
|
||||||
return new(
|
return new(
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
|
|
@ -1024,14 +1024,14 @@ public sealed class LandblockPresentationPipelineTests
|
||||||
MaxGpuUploadBytes: 1_000_000,
|
MaxGpuUploadBytes: 1_000_000,
|
||||||
MaxGlRetireOperations: 1_000,
|
MaxGlRetireOperations: 1_000,
|
||||||
DestinationReserveFraction: 0.75f));
|
DestinationReserveFraction: 0.75f));
|
||||||
controller.PriorityLandblockId = priorityId;
|
controller.BeginDestinationReservation(1, priorityId, 0);
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
||||||
Assert.Empty(outbox);
|
Assert.Empty(outbox);
|
||||||
Assert.False(state.IsLoaded(laterId));
|
Assert.False(state.IsLoaded(laterId));
|
||||||
Assert.True(state.IsLoaded(unloadId));
|
Assert.True(state.IsLoaded(unloadId));
|
||||||
|
|
||||||
controller.PriorityLandblockId = 0;
|
controller.EndDestinationReservation(1);
|
||||||
controller.Tick(0x70, 0x70);
|
controller.Tick(0x70, 0x70);
|
||||||
|
|
||||||
Assert.True(state.IsLoaded(priorityId));
|
Assert.True(state.IsLoaded(priorityId));
|
||||||
|
|
|
||||||
|
|
@ -46,22 +46,23 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
harness.Controller.OfferDestination(
|
harness.Controller.OfferDestination(
|
||||||
destination,
|
destination,
|
||||||
teleportTimestampAdvanced: true);
|
teleportTimestampAdvanced: true);
|
||||||
Assert.Empty(harness.Streaming.PriorityWrites);
|
Assert.Empty(harness.Streaming.Reservations);
|
||||||
|
|
||||||
harness.Controller.OnTeleportStarted(7);
|
harness.Controller.OnTeleportStarted(7);
|
||||||
|
|
||||||
Assert.Equal(1, harness.Input.EndCount);
|
Assert.Equal(1, harness.Input.EndCount);
|
||||||
Assert.Equal(1, harness.Mode.EnterPortalCount);
|
Assert.Equal(1, harness.Mode.EnterPortalCount);
|
||||||
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
|
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
|
||||||
Assert.Equal((0x2021FFFFu, LocalPlayerTeleportController.NearRingRadius),
|
Assert.Equal(
|
||||||
Assert.Single(harness.Streaming.PriorityWrites));
|
(1L, 0x20210001u, WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius),
|
||||||
|
Assert.Single(harness.Streaming.Reservations));
|
||||||
Assert.True(harness.Reveal.Snapshot.IsActive);
|
Assert.True(harness.Reveal.Snapshot.IsActive);
|
||||||
Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind);
|
Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind);
|
||||||
|
|
||||||
harness.Controller.OfferDestination(
|
harness.Controller.OfferDestination(
|
||||||
destination,
|
destination,
|
||||||
teleportTimestampAdvanced: false);
|
teleportTimestampAdvanced: false);
|
||||||
Assert.Single(harness.Streaming.PriorityWrites);
|
Assert.Single(harness.Streaming.Reservations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -89,7 +90,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Assert.True(harness.Controller.IsActive);
|
Assert.True(harness.Controller.IsActive);
|
||||||
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
||||||
Assert.NotNull(harness.Presentation.BeginProjection);
|
Assert.NotNull(harness.Presentation.BeginProjection);
|
||||||
Assert.Single(harness.Streaming.PriorityWrites);
|
Assert.Single(harness.Streaming.Reservations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -105,14 +106,14 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
teleportTimestampAdvanced: true);
|
teleportTimestampAdvanced: true);
|
||||||
|
|
||||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
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 = new PlayerMovementController(new PhysicsEngine());
|
||||||
harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
||||||
harness.Controller.Tick(0.016f);
|
harness.Controller.Tick(0.016f);
|
||||||
|
|
||||||
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
||||||
Assert.Single(harness.Streaming.PriorityWrites);
|
Assert.Single(harness.Streaming.Reservations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -158,7 +159,6 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Assert.Empty(harness.Streaming.Recenters);
|
Assert.Empty(harness.Streaming.Recenters);
|
||||||
Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position);
|
Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position);
|
||||||
Assert.Equal(0x20210123u, harness.Placement.CellId);
|
Assert.Equal(0x20210123u, harness.Placement.CellId);
|
||||||
Assert.False(harness.Placement.Forced);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -188,7 +188,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReadinessHold_UsesWallClockAndForcesExactlyAtTenSeconds()
|
public void ReadinessHold_NeverRevealsIncompleteWorldAndShowsWaitCueAfterFiveSeconds()
|
||||||
{
|
{
|
||||||
var harness = new Harness(worldReady: false);
|
var harness = new Harness(worldReady: false);
|
||||||
harness.Controller.OnTeleportStarted(5);
|
harness.Controller.OnTeleportStarted(5);
|
||||||
|
|
@ -197,15 +197,18 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
teleportTimestampAdvanced: true);
|
teleportTimestampAdvanced: true);
|
||||||
harness.Presentation.EmitPlaceWhenReady = true;
|
harness.Presentation.EmitPlaceWhenReady = true;
|
||||||
|
|
||||||
harness.Controller.Tick(9.5f);
|
harness.Controller.Tick(4.9f);
|
||||||
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
|
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
|
||||||
|
Assert.False(harness.Presentation.WaitCueValues[^1]);
|
||||||
Assert.Equal(default, harness.Placement.Position);
|
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.All(harness.Presentation.WorldReadyValues, Assert.False);
|
||||||
Assert.True(harness.Placement.Forced);
|
Assert.True(harness.Presentation.WaitCueValues[^1]);
|
||||||
Assert.Equal(new Vector3(1f, 2f, 3f), harness.Placement.Position);
|
Assert.Equal(default, harness.Placement.Position);
|
||||||
|
Assert.True(harness.Reveal.WaitCueShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -223,7 +226,6 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
harness.Controller.Tick(0.016f);
|
harness.Controller.Tick(0.016f);
|
||||||
|
|
||||||
Assert.True(Index(order, "placement") < Index(order, "[world-reveal] event=materialized"));
|
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);
|
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,6 +251,8 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Assert.False(harness.Controller.IsActive);
|
Assert.False(harness.Controller.IsActive);
|
||||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||||
Assert.Equal(1, harness.Session.LoginCompleteCount);
|
Assert.Equal(1, harness.Session.LoginCompleteCount);
|
||||||
|
Assert.True(harness.Reveal.Snapshot.Completed);
|
||||||
|
Assert.False(harness.Reveal.Snapshot.Cancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -261,7 +265,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
teleportTimestampAdvanced: true);
|
teleportTimestampAdvanced: true);
|
||||||
|
|
||||||
harness.Controller.OnTeleportStarted(11);
|
harness.Controller.OnTeleportStarted(11);
|
||||||
harness.Controller.Tick(LocalPlayerTeleportController.MaximumWorldHoldSeconds);
|
harness.Controller.Tick(30f);
|
||||||
|
|
||||||
Assert.Equal(default, harness.Placement.Position);
|
Assert.Equal(default, harness.Placement.Position);
|
||||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||||
|
|
@ -318,7 +322,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
harness.Controller.OfferDestination(
|
harness.Controller.OfferDestination(
|
||||||
Position(0x20210001u, 20, 1f, 2f, 3f),
|
Position(0x20210001u, 20, 1f, 2f, 3f),
|
||||||
teleportTimestampAdvanced: true);
|
teleportTimestampAdvanced: true);
|
||||||
harness.Streaming.PriorityClearCount = 0;
|
harness.Streaming.ReservationEnds.Clear();
|
||||||
harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21);
|
harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21);
|
||||||
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
||||||
|
|
||||||
|
|
@ -327,7 +331,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Assert.True(harness.Controller.IsActive);
|
Assert.True(harness.Controller.IsActive);
|
||||||
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
||||||
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
|
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
|
||||||
Assert.Equal(1, harness.Streaming.PriorityClearCount);
|
Assert.Single(harness.Streaming.ReservationEnds);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -418,7 +422,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
var position = new Vector3(12f, 24f, 6f);
|
var position = new Vector3(12f, 24f, 6f);
|
||||||
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
|
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.Position, controllerSlot.Controller.Position);
|
||||||
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
|
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
|
||||||
|
|
@ -560,7 +564,8 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
prepareCompositeTextures: (_, _) => { },
|
prepareCompositeTextures: (_, _) => { },
|
||||||
invalidateCompositeTextures: () => { },
|
invalidateCompositeTextures: () => { },
|
||||||
isSpawnClaimUnhydratable: _ => false,
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
log: order.Add);
|
log: order.Add,
|
||||||
|
streaming: Streaming);
|
||||||
Controller = new LocalPlayerTeleportController(
|
Controller = new LocalPlayerTeleportController(
|
||||||
Authority,
|
Authority,
|
||||||
Input,
|
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;
|
private readonly List<string> _order;
|
||||||
|
|
||||||
|
|
@ -636,9 +643,10 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public bool RecenterPending;
|
public bool RecenterPending;
|
||||||
public bool ResetResult = true;
|
public bool ResetResult = true;
|
||||||
public bool LastResetWasSessionEnding;
|
public bool LastResetWasSessionEnding;
|
||||||
public int PriorityClearCount;
|
|
||||||
public readonly List<(int X, int Y, bool Sealed)> Recenters = new();
|
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)
|
public bool BeginRecenter(int x, int y, bool isSealedDungeon)
|
||||||
{
|
{
|
||||||
|
|
@ -655,13 +663,17 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public bool IsSealedDungeon(uint cellId) =>
|
public bool IsSealedDungeon(uint cellId) =>
|
||||||
(cellId & 0xFFFFu) >= 0x0100u;
|
(cellId & 0xFFFFu) >= 0x0100u;
|
||||||
|
|
||||||
public void SetPriority(uint landblockId, int radius) =>
|
public void BeginDestinationReservation(
|
||||||
PriorityWrites.Add((landblockId, radius));
|
long revealGeneration,
|
||||||
|
uint destinationCell,
|
||||||
|
int requiredRenderRadius) =>
|
||||||
|
Reservations.Add(
|
||||||
|
(revealGeneration, destinationCell, requiredRenderRadius));
|
||||||
|
|
||||||
public void ClearPriority()
|
public void EndDestinationReservation(long revealGeneration)
|
||||||
{
|
{
|
||||||
PriorityClearCount++;
|
ReservationEnds.Add(revealGeneration);
|
||||||
_order.Add("priority-clear");
|
_order.Add("reservation-end");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -674,16 +686,14 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public uint CellId;
|
public uint CellId;
|
||||||
public Quaternion Rotation;
|
public Quaternion Rotation;
|
||||||
public bool Forced;
|
|
||||||
public Action? OnPlace;
|
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");
|
_order.Add("placement");
|
||||||
Position = position;
|
Position = position;
|
||||||
CellId = cellId;
|
CellId = cellId;
|
||||||
Rotation = rotation;
|
Rotation = rotation;
|
||||||
Forced = forced;
|
|
||||||
OnPlace?.Invoke();
|
OnPlace?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -731,6 +741,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public Matrix4x4? BeginProjection;
|
public Matrix4x4? BeginProjection;
|
||||||
public bool EmitPlaceWhenReady;
|
public bool EmitPlaceWhenReady;
|
||||||
public readonly List<bool> WorldReadyValues = new();
|
public readonly List<bool> WorldReadyValues = new();
|
||||||
|
public readonly List<bool> WaitCueValues = new();
|
||||||
public int DisposeFailuresRemaining;
|
public int DisposeFailuresRemaining;
|
||||||
public int DisposeCount;
|
public int DisposeCount;
|
||||||
|
|
||||||
|
|
@ -760,6 +771,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
|
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
|
||||||
public void EnterTunnel() => IsPortalViewportVisible = true;
|
public void EnterTunnel() => IsPortalViewportVisible = true;
|
||||||
public void ExitTunnel() => IsPortalViewportVisible = false;
|
public void ExitTunnel() => IsPortalViewportVisible = false;
|
||||||
|
public void SetWaitCue(bool visible) => WaitCueValues.Add(visible);
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ public sealed class StreamingCompletionQueueTests
|
||||||
result,
|
result,
|
||||||
estimate,
|
estimate,
|
||||||
priority,
|
priority,
|
||||||
|
priority == StreamingCompletionPriority.Destination ? 1L : 0L,
|
||||||
result.Generation,
|
result.Generation,
|
||||||
sequence,
|
sequence,
|
||||||
Stopwatch.GetTimestamp());
|
Stopwatch.GetTimestamp());
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,110 @@ public sealed class StreamingWorkBudgetTests
|
||||||
meter.Snapshot.LastLimit);
|
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]
|
[Fact]
|
||||||
public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure()
|
public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure()
|
||||||
{
|
{
|
||||||
|
|
@ -427,21 +531,32 @@ public sealed class StreamingWorkBudgetTests
|
||||||
cpuBytes: 1_000,
|
cpuBytes: 1_000,
|
||||||
gpuBytes: 44,
|
gpuBytes: 44,
|
||||||
retireOperations: 1));
|
retireOperations: 1));
|
||||||
controller.PriorityLandblockId = center;
|
controller.BeginDestinationReservation(1, center, 0);
|
||||||
|
|
||||||
controller.Tick(32, 32);
|
controller.Tick(32, 32);
|
||||||
|
|
||||||
Assert.Equal([center], applied);
|
Assert.Equal([center], applied);
|
||||||
Assert.Equal(1, controller.WorkDiagnostics.DeferredCompletions);
|
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(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
||||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||||
|
|
||||||
controller.Tick(32, 32);
|
controller.Tick(32, 32);
|
||||||
|
|
||||||
Assert.Equal([center, ordinary], applied);
|
Assert.Equal([center, ordinary], applied);
|
||||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
|
||||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
public int InvalidateCount { get; private set; }
|
public int InvalidateCount { get; private set; }
|
||||||
public int PrepareCount { 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,
|
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
||||||
isSpawnCellReady: _ => CollisionReady,
|
isSpawnCellReady: _ => CollisionReady,
|
||||||
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
||||||
|
|
@ -25,7 +27,8 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CompositesReady = false;
|
CompositesReady = false;
|
||||||
},
|
},
|
||||||
isSpawnClaimUnhydratable: _ => 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]
|
[Fact]
|
||||||
|
|
@ -34,7 +37,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
var state = new State { CompositesReady = true };
|
var state = new State { CompositesReady = true };
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
|
|
||||||
long generation = coordinator.Begin(WorldRevealKind.Login);
|
long generation = coordinator.Begin(WorldRevealKind.Login, 0x11340021u);
|
||||||
|
|
||||||
Assert.Equal(1, generation);
|
Assert.Equal(1, generation);
|
||||||
Assert.Equal(1, state.InvalidateCount);
|
Assert.Equal(1, state.InvalidateCount);
|
||||||
|
|
@ -52,7 +55,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CollisionReady = true,
|
CollisionReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
coordinator.Begin(WorldRevealKind.Login);
|
coordinator.Begin(WorldRevealKind.Login, outdoorCell);
|
||||||
|
|
||||||
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
||||||
state.CompositesReady = true;
|
state.CompositesReady = true;
|
||||||
|
|
@ -76,7 +79,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CompositesReady = true,
|
CompositesReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build(logs);
|
WorldRevealCoordinator coordinator = state.Build(logs);
|
||||||
coordinator.Begin(WorldRevealKind.Portal);
|
coordinator.Begin(WorldRevealKind.Portal, outdoorCell);
|
||||||
state.CompositesReady = true;
|
state.CompositesReady = true;
|
||||||
|
|
||||||
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
||||||
|
|
@ -103,7 +106,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CompositesReady = true,
|
CompositesReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
coordinator.Begin(WorldRevealKind.Portal);
|
coordinator.Begin(WorldRevealKind.Portal, 0x11340021u);
|
||||||
coordinator.Cancel();
|
coordinator.Cancel();
|
||||||
|
|
||||||
coordinator.PrepareAndEvaluate(0x11340021u);
|
coordinator.PrepareAndEvaluate(0x11340021u);
|
||||||
|
|
@ -137,8 +140,8 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
isSpawnClaimUnhydratable: _ => false,
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
quiescence: quiescence);
|
quiescence: quiescence);
|
||||||
|
|
||||||
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login));
|
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login, 0x11340021u));
|
||||||
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal));
|
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal, 0x11340021u));
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.Equal(2, availability.QuiescedGeneration);
|
Assert.Equal(2, availability.QuiescedGeneration);
|
||||||
Assert.Equal(1, audio.SuspendCalls);
|
Assert.Equal(1, audio.SuspendCalls);
|
||||||
|
|
@ -149,6 +152,30 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
Assert.Equal(1, audio.ResumeCalls);
|
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
|
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
||||||
{
|
{
|
||||||
public int SuspendCalls { get; private set; }
|
public int SuspendCalls { get; private set; }
|
||||||
|
|
@ -157,4 +184,21 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
public void SuspendWorldAudio() => SuspendCalls++;
|
public void SuspendWorldAudio() => SuspendCalls++;
|
||||||
public void ResumeWorldAudio() => ResumeCalls++;
|
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);
|
generation);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PriorityLandblock_WinsExecutionOrderAfterBoundedAdmission()
|
public void DestinationReservation_WinsExecutionOrderAfterBoundedAdmission()
|
||||||
{
|
{
|
||||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||||
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
var outbox = new Queue<LandblockStreamResult>(new LandblockStreamResult[]
|
||||||
|
|
@ -48,7 +48,7 @@ public class StreamingControllerPriorityApplyTests
|
||||||
workBudgetOptions: GenerousBudget())
|
workBudgetOptions: GenerousBudget())
|
||||||
{ MaxCompletionsPerFrame = 4 };
|
{ MaxCompletionsPerFrame = 4 };
|
||||||
|
|
||||||
ctrl.PriorityLandblockId = priority;
|
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||||
ctrl.Tick(169, 180);
|
ctrl.Tick(169, 180);
|
||||||
|
|
||||||
Assert.NotEmpty(applied);
|
Assert.NotEmpty(applied);
|
||||||
|
|
@ -87,10 +87,10 @@ public class StreamingControllerPriorityApplyTests
|
||||||
nearRadius: 4,
|
nearRadius: 4,
|
||||||
farRadius: 12)
|
farRadius: 12)
|
||||||
{ MaxCompletionsPerFrame = 4 };
|
{ MaxCompletionsPerFrame = 4 };
|
||||||
ctrl.PriorityLandblockId = priority;
|
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||||
|
|
||||||
ctrl.Tick(169, 180); // priority + some others
|
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); // drains the deferred remainder
|
||||||
ctrl.Tick(169, 180);
|
ctrl.Tick(169, 180);
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ public class StreamingControllerPriorityApplyTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PriorityNeverArrives_noThrow_noLoss_noDoubleApply()
|
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
|
// arrives (e.g. the load failed), ordinary results still enter the typed
|
||||||
// Near/Far queues and advance under the same budget without loss.
|
// Near/Far queues and advance under the same budget without loss.
|
||||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
||||||
|
|
@ -134,7 +134,7 @@ public class StreamingControllerPriorityApplyTests
|
||||||
{ MaxCompletionsPerFrame = 4 };
|
{ MaxCompletionsPerFrame = 4 };
|
||||||
|
|
||||||
// Set a priority id that will NEVER appear in the outbox.
|
// 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
|
// Tick several times — should not throw and every non-priority completion
|
||||||
// must still be applied without duplication.
|
// must still be applied without duplication.
|
||||||
|
|
@ -160,6 +160,30 @@ public class StreamingControllerPriorityApplyTests
|
||||||
MaxGlRetireOperations: 10_000,
|
MaxGlRetireOperations: 10_000,
|
||||||
DestinationReserveFraction: 0.75f);
|
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]
|
[Fact]
|
||||||
public void DeferredCompaction_ApplyFailureRetainsExactResultWithoutReplayingCommittedPrefix()
|
public void DeferredCompaction_ApplyFailureRetainsExactResultWithoutReplayingCommittedPrefix()
|
||||||
{
|
{
|
||||||
|
|
@ -199,8 +223,8 @@ public class StreamingControllerPriorityApplyTests
|
||||||
farRadius: 12)
|
farRadius: 12)
|
||||||
{
|
{
|
||||||
MaxCompletionsPerFrame = 3,
|
MaxCompletionsPerFrame = 3,
|
||||||
PriorityLandblockId = priority,
|
|
||||||
};
|
};
|
||||||
|
ctrl.BeginDestinationReservation(1, priority, 0);
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() => ctrl.Tick(169, 180));
|
Assert.Throws<InvalidOperationException>(() => ctrl.Tick(169, 180));
|
||||||
Assert.Equal([first], applied);
|
Assert.Equal([first], applied);
|
||||||
|
|
@ -215,7 +239,6 @@ public class StreamingControllerPriorityApplyTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ForceReloadWindow_DiscardsBufferedCompletionsFromOldWindow()
|
public void ForceReloadWindow_DiscardsBufferedCompletionsFromOldWindow()
|
||||||
{
|
{
|
||||||
uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180);
|
|
||||||
var outbox = new Queue<LandblockStreamResult>(
|
var outbox = new Queue<LandblockStreamResult>(
|
||||||
Enumerable.Range(0, 6)
|
Enumerable.Range(0, 6)
|
||||||
.Select(i => (LandblockStreamResult)LoadedOf(
|
.Select(i => (LandblockStreamResult)LoadedOf(
|
||||||
|
|
@ -243,8 +266,7 @@ public class StreamingControllerPriorityApplyTests
|
||||||
MaxGpuUploadBytes: 1_000_000,
|
MaxGpuUploadBytes: 1_000_000,
|
||||||
MaxGlRetireOperations: 1_000,
|
MaxGlRetireOperations: 1_000,
|
||||||
DestinationReserveFraction: 0.75f))
|
DestinationReserveFraction: 0.75f))
|
||||||
{ MaxCompletionsPerFrame = 4, PriorityLandblockId = priority };
|
{ MaxCompletionsPerFrame = 4 };
|
||||||
|
|
||||||
ctrl.Tick(169, 180);
|
ctrl.Tick(169, 180);
|
||||||
Assert.Equal(4, applied.Count);
|
Assert.Equal(4, applied.Count);
|
||||||
applied.Clear();
|
applied.Clear();
|
||||||
|
|
@ -452,7 +474,8 @@ public class StreamingControllerPriorityApplyTests
|
||||||
state: state,
|
state: state,
|
||||||
nearRadius: 0,
|
nearRadius: 0,
|
||||||
farRadius: 0,
|
farRadius: 0,
|
||||||
onLandblockLoaded: loadedCallbacks.Add);
|
onLandblockLoaded: loadedCallbacks.Add,
|
||||||
|
workBudgetOptions: GenerousBudget());
|
||||||
|
|
||||||
ctrl.Tick(10, 10);
|
ctrl.Tick(10, 10);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue