docs(streaming): define cost-budgeted lifecycle

This commit is contained in:
Erik 2026-07-24 17:11:54 +02:00
parent 66690805c5
commit 2945896a6f
3 changed files with 479 additions and 0 deletions

View file

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

View file

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

View file

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