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:
Erik 2026-07-24 19:39:23 +02:00
parent 98f1ac8934
commit 2ff8f844b0
33 changed files with 870 additions and 230 deletions

View file

@ -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.861.9%, GC pause time fell 48.454.1%, uncapped CPU/GPU p99 improved 41.861.9%, GC pause time fell 48.454.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 E0E4 now typed residency policy/accounting and bounded retained caches. Slice E0E5 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

View file

@ -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.861.9%, GC pause time fell 48.454.1%, uncapped CPU/GPU p99 improved 41.861.9%, GC pause time fell 48.454.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 E0E4 now typed residency policy/accounting and bounded retained caches. Slice E0E5 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

View file

@ -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 AD and Slice E0E4 are complete. Typed - **Performance side track:** Slices AD and Slice E0E5 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**.
--- ---

View file

@ -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 AD and Slice E0E4 of - **Current active engineering program:** Slices AD and Slice E0E5 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.

View file

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

View file

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

View file

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

View file

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

View file

@ -93,13 +93,17 @@ The separately authorized modern-runtime performance program has completed
Slices AD: corrected measurement, prepared-package bake/dedup, package-only Slices AD: 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 E0E4 now define and enforce typed completion admission through explicit Slice E0E5 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

View file

@ -2,7 +2,7 @@
**Date:** 2026-07-24 **Date:** 2026-07-24
**Status:** Slices AD complete. Slice E is active (E0E4 complete). **Status:** Slices AD complete. Slice E is active (E0E5 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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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