docs(streaming): define cost-budgeted lifecycle
This commit is contained in:
parent
66690805c5
commit
2945896a6f
3 changed files with 479 additions and 0 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue