diff --git a/docs/plans/2026-07-24-modern-runtime-architecture.md b/docs/plans/2026-07-24-modern-runtime-architecture.md index 08ed3239..e9dfccdd 100644 --- a/docs/plans/2026-07-24-modern-runtime-architecture.md +++ b/docs/plans/2026-07-24-modern-runtime-architecture.md @@ -724,6 +724,8 @@ checkpoint retained staged or retiring bytes. Evidence: ### Slice E — Cost-budgeted streaming and retirement +**Active plan:** [`2026-07-24-modern-runtime-slice-e-cost-budgeted-streaming.md`](2026-07-24-modern-runtime-slice-e-cost-budgeted-streaming.md). + **Purpose:** Remove update-thread portal transactions. - Introduce explicit stage queues and `StreamingWorkBudget`. diff --git a/docs/plans/2026-07-24-modern-runtime-slice-e-cost-budgeted-streaming.md b/docs/plans/2026-07-24-modern-runtime-slice-e-cost-budgeted-streaming.md new file mode 100644 index 00000000..526aa414 --- /dev/null +++ b/docs/plans/2026-07-24-modern-runtime-slice-e-cost-budgeted-streaming.md @@ -0,0 +1,300 @@ +# Modern Runtime Slice E — Cost-budgeted streaming and retirement + +**Status:** active — E0 contract/inventory complete +**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md` +**Baseline:** Slice D closeout commit `66690805` +**Behavior contract:** no visual-quality or retail-behavior reduction + +## 1. Outcome + +Slice E removes whole-window portal publication and retirement transactions +from a single update frame. It preserves the existing worker, canonical +`GpuWorldState`, presentation owners, retry receipts, world-reveal barrier, +and renderer resource owners. + +The change is scheduling and lifecycle architecture: + +- one immutable `StreamingWorkBudget` defines real per-frame limits; +- one single-writer scheduler admits and advances explicit work stages; +- destination-critical work receives reserved capacity across portal frames; +- old generations become unavailable and quiescent immediately; +- expensive owner/resource teardown resumes from exact cursors; +- publication advances through exact owner receipts under time, byte, entity, + upload, and retirement-operation limits; +- the existing `WorldRevealCoordinator` remains the only reveal predicate. + +No distance, texture, particle, cell, collision, or presentation quality is +reduced. + +## 2. Retail boundary + +Retail teardown is synchronous: + +- `CLandBlock::destroy_static_objects @ 0x0052FA50`; +- `CLandBlock::Destroy @ 0x0052FAA0`; +- `CObjectMaint::DestroyObjects @ 0x00508C30`. + +Retail portal/cell blocking freezes object maintenance, physics, landscape, +game time, and ambient sound while continuing scene/UI/event work: +`SmartBox::UseTime @ 0x00455410`. + +Acdream's async adaptation may defer physical cleanup, but may not leave a +retired owner active. Full translation: +[`../research/2026-07-24-retail-streaming-retirement-pseudocode.md`](../research/2026-07-24-retail-streaming-retirement-pseudocode.md). + +## 3. Existing-owner inventory + +| Concern | Current owner | Current behavior | Slice E action | +|---|---|---|---| +| Worker inbox/outbox | `LandblockStreamer` | unbounded channels; near jobs prioritized | retain worker; expose bounded, allocation-free single-consumer drain and backlog facts | +| Completion apply | `StreamingController._deferredApply` | retained `List`; count-budgeted loads; unload and priority ring bypass | replace with explicit generation/priority FIFO stage queues and exact retained-byte accounting | +| Full reload | `BeginFullWindowRetirement` | snapshots and begins every resident retirement synchronously | retain stable snapshot/cursor and advance admissions under budget | +| Origin recenter | `OriginRecenterRetirement` | has a cursor but drains it to completion in one call | make the cursor the canonical budgeted admission receipt | +| Logical/spatial detach | `GpuWorldState.DetachLandblock/DetachNearLayer` | exact immediate bucket withdrawal; captures entity receipt | preserve; add destination-generation quiesce before cursored detaches | +| Retirement ledger | `LandblockRetirementCoordinator` | retryable per-stage/per-entity state, but advances a complete ticket on begin/advance | expose one bounded atomic step at a time; stable FIFO | +| Retirement effects | `LandblockPresentationRetirementOwner` | lights, translucency, plugin, terrain, physics, cells/buildings | preserve order; each entity/stage consumes measured budget | +| Publication ledger | `LandblockPresentationPipeline` | retryable stage booleans, but `Advance` runs the complete transaction | convert booleans/cursors into resumable atomic publication steps | +| Render publication | `LandblockRenderPublisher` | terrain/visibility/AABB/buildings/EnvCells whole stages | retain receipts; meter GPU bytes and split iterable registries where needed | +| Physics publication | `LandblockPhysicsPublisher` | rebuilds terrain/cells/buildings/statics/reflood in one call | cursor cell/building/static preparation and commit at safe mutation boundaries | +| Static presentation | `LandblockStaticPresentationPublisher` | allocates dictionaries/sorts arrays, then loops prior/plugin entities | prepare stable ordered arrays once; cursor cleanup/light/plugin work | +| Mesh upload | `ObjectMeshManager`/`WbMeshAdapter` | existing per-frame renderer owner/budgets | report exact upload bytes/operations to the streaming meter; never move GL ownership | +| Reveal | `WorldRevealCoordinator` + `WorldRevealReadinessBarrier` | one correct login/portal predicate | add destination generation/readiness stage facts; never duplicate predicate | +| Portal wait cue | `PortalTunnelPresentation` | authored tunnel and centered wait notice port exists | hold tunnel past five seconds and publish diagnostic; never early reveal | +| Audio | `OpenAlAudioEngine`/`AudioHookSink` | one-shot owner is not retained in source slot | tag 3-D voices by owner; stop old-generation owners at quiesce | + +## 4. Work model + +### 4.1 Immutable budget + +The App-layer type is equivalent to: + +```csharp +public readonly record struct StreamingWorkBudget( + TimeSpan MaxUpdateTime, + int MaxCompletionAdmissions, + long MaxAdoptedCpuBytes, + int MaxEntityOperations, + long MaxGpuUploadBytes, + int MaxGlRetireOperations, + float DestinationReserveFraction); +``` + +All fields are positive and validated as one profile. The initial High/default +profile is evidence-tuned, not a reduction in quality: + +- 2.0 ms update-thread streaming work; +- 64 completion admissions; +- 8 MiB retained CPU payload adoption; +- 256 entity operations; +- 8 MiB requested GPU upload; +- 64 logical GL-retirement admissions; +- 75% reserved for destination-critical work while a reveal is pending. + +These are initial scheduling ceilings, not content ceilings. Work continues on +later frames until exact convergence. Connected E gates may retune the profile +as one atomic change. + +The compatibility `MaxCompletionsPerFrame` setting maps to complete Low/Medium/ +High/Ultra work profiles during migration. It does not remain an independent +hidden second throttle. + +### 4.2 Meter + +`StreamingWorkMeter` is stack/frame scoped and single-thread owned. It records: + +- start/deadline timestamp; +- admitted results and retained CPU bytes; +- entity operations; +- GPU bytes requested; +- GL retirement operations; +- stage completions, yields, overruns, failures, and oldest-work age. + +Reservation is checked before an atomic operation using its conservative known +cost. Elapsed time is checked after every atomic operation. A first operation +may run when a dimension is otherwise empty so progress cannot deadlock; any +single-operation overrun is named in diagnostics and becomes a required split +site. + +### 4.3 Cost estimation + +`LandblockStreamResultCost` is computed once from immutable payloads: + +- terrain vertex/index bytes; +- entity count; +- mesh-reference count; +- EnvCell visibility/shell count; +- physics cell/building/GfxObj counts; +- prepared payload arrays retained by the completion. + +It never uses process working set as a scheduling unit and never counts the +memory-mapped package's address space as adopted bytes. + +## 5. Queue and ordering contract + +One scheduler owns queues by `(generation, priority, stage)`. + +Priorities: + +1. destination collision/required Near scene; +2. visible Near; +3. ordinary Near; +4. Far/speculative; +5. retirement cleanup after immediate quiesce. + +Rules: + +- FIFO is exact within one generation/priority. +- Generation supersession cancels stale unpublished work and releases its + retained-byte charge. +- Destination reserve applies only while the canonical reveal generation is + pending. +- Unreserved capacity is work-conserving: any queue may use it. +- At least one noncritical/retirement operation is admitted when capacity + remains, preventing starvation during a slow destination. +- Failed stages retain their exact receipt and queue position. +- Callback reentrancy appends work; it never mutates the active cursor. +- `_deferredApply`, `MaxDrainIterations`, the eager priority-radius bypass, and + count-only `MaxCompletionsPerFrame` execution disappear at cutover. + +## 6. Immediate quiesce versus deferred release + +At a hard destination-generation edge: + +1. The old world generation is marked unavailable to normal world drawing, + picking, radar, status targeting, and collision queries. +2. Object/static animation, PhysicsScript, particle emission, and ambient/ + owner-tagged audio for that generation are frozen or silenced. +3. The portal tunnel/UI/event path continues, matching retail's + `blocking_for_cells` branch. +4. Stable resident landblock IDs are captured once. +5. Per-landblock spatial detach and owner/resource release advance from cursors. + +Quiesce is idempotent and generation-scoped. It does not destroy live server +identity, replay Hidden/UnHide, or clear the replacement generation. + +For ordinary Near→Far demotion outside a hard portal edge, the exact landblock +detach remains the immediate visibility/collision boundary; only its expensive +owner cleanup is deferred. + +## 7. Publication contract + +One accepted completion progresses through: + +```text +Admitted + -> PreparedReceipt + -> RenderPrefix + -> PhysicsBase + -> RenderCellsBuildings + -> StaticEntityPresentationAndCollision + -> SpatialCommit + -> RenderPinsAndEnvCellReplay + -> LiveProjectionRecovery + -> Complete +``` + +Each transition owns a retry receipt and known cost. Mutation order remains the +existing retail-rooted order. A stage cannot publish partial externally visible +state unless its receipt describes and resumes that exact partial prefix. + +Destination reveal consumes only `Complete`/renderer-upload facts belonging to +the active destination generation. Far-ring work may continue after reveal. + +## 8. Threading + +- Worker threads build immutable `LandblockBuild`/prepared payloads and enqueue + immutable completions only. +- The update/render thread is the sole scheduler, meter, world-state, + publication, and retirement writer. +- GL upload/non-residency/deletion remains with renderer resource owners. +- No update operation waits for the worker, a GPU fence, file I/O, or a lock + held by render work. +- Back-pressure never blocks the update thread. Worker-side bounded queues may + wait/cancel; update-side admission is non-blocking. + +## 9. Delivery checkpoints + +### E0 — Oracle, inventory, and contract + +- Record named-retail teardown/blocking pseudocode. +- Inventory every current publication/retirement owner and bypass. +- Define budget dimensions, atomic work, queue ordering, quiesce, retries, + threading, diagnostics, and acceptance. + +**Complete 2026-07-24.** + +### E1 — Typed budgets, meter, cost model, and diagnostics + +- Add validated `StreamingWorkBudgetOptions` to `RuntimeOptions`. +- Add pure `StreamingWorkMeter` and deterministic injected-clock tests. +- Add exact immutable completion-cost estimation. +- Publish per-stage backlog, bytes, oldest age, work/yield/overrun facts through + lifecycle artifacts. +- Preserve current execution while measuring the would-yield decisions. + +### E2 — Explicit admission queues + +- Replace `_deferredApply` with stable generation/priority queues. +- Bound result admission by count and CPU bytes. +- Remove `MaxDrainIterations`. +- Preserve exact retry position and stale-generation rejection. +- Keep current publication atomic until E4, but execute only work admitted by + the typed meter. + +### E3 — Quiesce and budgeted retirement + +- Add destination-generation world quiesce. +- Tag/stop owner audio at the immediate edge. +- Make full-window and recenter retirement admission cursor/time/entity bounded. +- Make each retirement ticket advance by bounded atomic entity/stage work. +- Prove no old generation ticks, emits, collides, targets, or renders while its + retained memory converges. + +### E4 — Cursor-budgeted publication + +- Move stable ordered arrays/cost facts into retained receipts. +- Cursor static cleanup, static light/collision/plugin publication, physics + cell/building work, and environment-cell publication at safe boundaries. +- Feed GPU requested-byte and retirement-operation facts from renderer owners. +- Prove every failure resumes the exact unfinished operation without replay. + +### E5 — Destination reservation and reveal generation + +- Replace eager `PriorityRadius` execution with destination reservations. +- Join scheduler destination generation to the existing reveal coordinator. +- Keep the tunnel animating and surface the retail wait cue/diagnostic beyond + five seconds. +- Remove the last count-only execution/bypass paths. + +### E6 — Gates and closeout + +- Deterministic pressure, reentrancy, failure, cancellation, stale-generation, + GUID reuse, dungeon, outdoor, and session-reset suites. +- Release build and full solution tests. +- Physical-local capped/uncapped nine-stop and pinned dense-Arwic routes. +- Compare portal p99 and maximum single-frame allocation to Slice A and Slice C. +- Require zero `viewport-before-ready`, zero stranded old generation, staged + upload, collision, effect, audio, or GPU owner. +- Fixed-camera screenshots and subjective visual gate only if pixels change. +- Update architecture, inventory, issues, roadmap, divergence register, and + durable memory. + +## 10. Acceptance + +Slice E closes only when: + +1. No priority or unload path bypasses all work budgets. +2. No frame exceeds any configured count/byte/entity dimension. +3. Every time overrun names one indivisible operation; none remain above the + configured time ceiling at closeout. +4. Old generations become unavailable, tick-frozen, and audio-silent at the + hard transition edge. +5. Retirement/publication retries resume exact cursors without replay. +6. Deferred retained CPU bytes stay within their queue budget. +7. Reveal is possible only for the active destination generation and the + existing readiness predicate. +8. Portal-window p99 and largest frame allocation materially improve over the + post-Slice-A physical baseline. +9. Complete connected routes end with zero pending publication, retirement, + upload, effect, collision, audio, and stale-generation owners. +10. Visual output/range/quality and retail gameplay behavior are unchanged. diff --git a/docs/research/2026-07-24-retail-streaming-retirement-pseudocode.md b/docs/research/2026-07-24-retail-streaming-retirement-pseudocode.md new file mode 100644 index 00000000..7b9a4c7b --- /dev/null +++ b/docs/research/2026-07-24-retail-streaming-retirement-pseudocode.md @@ -0,0 +1,177 @@ +# Retail landblock/object retirement and portal blocking — pseudocode + +**Purpose:** Oracle boundary for Modern Runtime Slice E. + +## Named-retail sources + +- `CLandBlock::destroy_static_objects @ 0x0052FA50` +- `CLandBlock::Destroy @ 0x0052FAA0` +- `CObjectMaint::DestroyObjects @ 0x00508C30` +- `CObjectMaint::AddObjectToBeDestroyed @ 0x00508F70` +- `SmartBox::UseTime @ 0x00455410` + +The Sept 2013 named pseudo-C is the behavioral oracle. WorldBuilder has no +equivalent live-client teardown scheduler; its extracted mesh/cache owners are +implementation references only. + +## `CLandBlock::destroy_static_objects` + +```text +for i = 0 .. num_static_objects - 1: + object = static_objects[i] + if object != null: + object.leave_world() + delete object + +num_static_objects = 0 +``` + +The operation is synchronous in retail. Each object leaves the world before +its memory is destroyed. + +## `CLandBlock::Destroy` + +```text +destroy_static_objects() +destroy_buildings() + +if landblock_info != null: + landblock_info.Release() + landblock_info = null + +closest = invalid +lbi_exists = false +direction = unknown + +if draw_array != null: + delete[] draw_array + draw_array = null + +draw_array_size = 0 +``` + +Retail does not expose a partially active destroyed landblock. The visible, +spatial, ticking, and memory lifetimes end in one call. + +## `CObjectMaint::DestroyObjects` + +```text +for every physics object: + object.exit_world() + object.leave_world() + remove object from object_table + remove object from null_object_table + remove object from destruction schedule + object.unset_parent() + object.unparent_children() + delete object + +for every weenie object: + remove from weenie tables + delete object + +destroy all object/null/weenie/inventory tables +destroy lost-cell table +``` + +Again, execution is complete once begun. Parent/child and spatial withdrawal +precede physical deletion. + +## `CObjectMaint::AddObjectToBeDestroyed` + +```text +remove any prior destruction schedule for object_id +deadline = Timer.cur_time + 25 seconds +destruction_table[object_id] = deadline +destruction_priority_queue.Insert(deadline, object_id) +``` + +This queue delays *when* complete object destruction begins. It is not +precedent for leaving an object active while destroying it one field at a +time. Acdream's 25-second nonresident live-object lifetime already ports this +separate behavior. + +## `SmartBox::UseTime` + +```text +if cell_manager.blocking_for_cells == false: + if not all_cells_available and CheckPrefetchStatus(): + CellManager.UpdateLoadPoint() + + if player exists and player has a cell: + CellManager.ChangePosition(player.position) + + if player exists and not waiting_for_teleport + and not position_update_complete: + position_update_complete = true + has_been_teleported = true + + ObjectMaint.UseTime() + Physics.UseTime() + GameTime.UseTime() + LScape.UseTime() + Ambient.UseTime() +else: + CellManager.CheckPrefetchStatus() + +SceneTool.Think() +drain inbound SmartBox event queue +CommandInterpreter.UseTime() +Render.CalcDegLevel() +``` + +While retail is blocked for destination cells, the old world does not advance +object maintenance, physics, landscape, game time, or ambient sound. UI/event +dispatch and the portal presentation continue. This is the retail basis for +Slice E's immediate old-generation quiesce. + +## Acdream asynchronous adaptation + +Retail performs each teardown synchronously and can synchronously block cell +loading. Acdream prepares content on a worker and must not spend an unbounded +render/update frame reproducing that transaction. The faithful observable +contract is therefore split at a different boundary: + +```text +begin destination generation: + mark prior world generation unavailable + replace normal world viewport with portal presentation + freeze old-generation object/static/effect simulation + silence old-generation audio + reject old generation from collision, picking, radar, and targeting + +over later update/resource-maintenance frames: + detach old landblocks through exact retry receipts + release scripts/effects/lights/plugin projections + release CPU owners + admit renderer retirement; physical GL release remains fence delayed + +in parallel, under reserved destination budgets: + admit destination completions + publish collision and required Near render state + request/upload required assets + +reveal only when the one destination-generation readiness state proves: + collision root ready + required Near scene ready + required composite textures ready + camera/player identity belongs to destination generation +``` + +Release-later never means run-later: an old owner may retain memory until its +cursor runs, but it cannot keep simulating, colliding, rendering, targeting, or +emitting audible work after the generation-quiesce edge. + +## Port constraints + +1. Preserve existing exact retry receipts; successful stages never replay. +2. FIFO is stable within priority and generation. +3. A time limit is checked between atomic owner operations. If one atomic + operation itself exceeds its ceiling, diagnostics name that operation; it + is then split at its natural cursor rather than interrupted mid-mutation. +4. No deferred callback may mutate GL from a worker. +5. Destination-critical work receives a reserved share of each frame; it does + not bypass all budgets. +6. A reveal delay beyond retail's five-second fade ceiling keeps the portal + tunnel active and displays the retail wait cue. It never reveals an + incomplete world.