260 lines
14 KiB
Markdown
260 lines
14 KiB
Markdown
# Modern Runtime Slice D — Typed Asset Handles and Unified Residency
|
|
|
|
**Status:** active — D1 and D2 complete
|
|
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
|
**Baseline:** Slice C closeout commit `a564c4b7`
|
|
**Behavior contract:** no visual-quality or retail-behavior change
|
|
|
|
## 1. Outcome
|
|
|
|
Slice D makes runtime memory finite, attributable, and generation-safe without
|
|
replacing the specialized caches that already work.
|
|
|
|
One `ResidencyManager` owns policy, logical asset identity, generations,
|
|
leases, budgets, and the aggregate diagnostic snapshot. Existing renderer and
|
|
content owners continue to own their physical storage:
|
|
|
|
- `ObjectMeshManager` owns object render data, mesh ranges, texture atlases,
|
|
prepared CPU mesh entries, and staged uploads.
|
|
- `GlobalMeshBuffer` owns vertex/index arenas and fence-delayed range reuse.
|
|
- `CompositeTextureArrayCache` owns pooled composite-array layers and arrays.
|
|
- `StandaloneBindlessTextureCache` owns standalone particle arrays.
|
|
- `GpuFrameFlightController` and `GpuRetirementLedger` own fence-delayed
|
|
physical release.
|
|
- `RetailAnimationLoader` owns parsed animation residence.
|
|
- Deferred-alpha producers own their reusable CPU scratch arrays.
|
|
|
|
The manager never stores or deletes an OpenGL name. It observes immutable
|
|
facts, makes bounded policy decisions during the update/resource-maintenance
|
|
phase, and issues logical trim requests. The physical owner executes any GL
|
|
release during its existing render-thread maintenance phase.
|
|
|
|
## 2. Current-owner inventory
|
|
|
|
| Domain | Existing policy | Physical owner | Slice D action |
|
|
|---|---|---|---|
|
|
| Object render data | 1 GiB estimated GPU / 50 unowned objects, per-frame bounded reclamation | `ObjectMeshManager` | Preserve; source limits from typed budgets; report logical/non-arena/arena/retiring bytes separately |
|
|
| Prepared mesh CPU cache | 100 entries / 128 MiB LRU | `CpuMeshUploadCache` | Preserve; source limits from typed budgets; report hits, misses, evictions, count, bytes |
|
|
| Mesh staging | 256 claims / 128 MiB, generation checked | `MeshUploadStagingQueue` | Preserve; source limits from typed budgets; report queued and claimed bytes separately |
|
|
| Global mesh arenas | 384 MiB vertex, 128 MiB index, 896 MiB maximum physical overlap; shrink hysteresis | `GlobalMeshBuffer` | Preserve; expose live, free, largest-free, staging, retired, and fragmentation facts |
|
|
| Composite texture arrays | 64 MiB unowned / 128 MiB physical, throttled logical eviction and fence release | `CompositeTextureArrayCache` | Preserve; source limits from typed budgets; report resident/unowned/retiring/fragmentation |
|
|
| Standalone particle arrays | 32 MiB / 256 unowned, one eviction per frame | `StandaloneBindlessTextureCache` | Preserve; source limits from typed budgets; report owned/unowned/retiring bytes |
|
|
| Prepared package | one immutable memory map; payload arrays are charged to CPU cache/staging | `PakPreparedAssetSource` | Report mapped virtual bytes separately; do not mislabel the 27.5 GiB address map as committed residence |
|
|
| Parsed animations | unbounded dictionary | `RetailAnimationLoader` | Add byte-and-count bounded concurrent LRU; a live sequencer keeps its own reference after cache eviction |
|
|
| Decoded audio waves | 32 MiB LRU | `DatSoundCache` | Preserve and report; sound-table metadata is finite DAT identity metadata |
|
|
| Deferred-alpha scratch | grow-only lists/arrays at three submission seams | producer owners | Add capacity-budgeted retention with immediate correctness-preserving growth and policy-driven shrink |
|
|
| Mesh bounds memo | not present in current source | none | Close stale issue #243; do not add a replacement |
|
|
|
|
The process-wide `GpuMemoryTracker` remains an independent physical-allocation
|
|
cross-check. It is not a policy owner because it intentionally covers shaders,
|
|
framebuffers, UI textures, and other allocations outside Slice D's cache set.
|
|
|
|
## 3. Typed identity
|
|
|
|
```csharp
|
|
readonly record struct AssetHandle<TAsset>(uint Index, ushort Generation);
|
|
readonly record struct OwnerToken(uint Index, ushort Generation);
|
|
readonly record struct AssetLease<TAsset>(
|
|
AssetHandle<TAsset> Handle,
|
|
OwnerToken Owner);
|
|
```
|
|
|
|
Rules:
|
|
|
|
1. Index zero and generation zero are invalid.
|
|
2. A slot generation increments before an index is reused.
|
|
3. A stale handle cannot touch, transition, lease, release, or retire its
|
|
replacement generation.
|
|
4. Owner tokens are allocated and retired independently from assets. Retiring
|
|
an owner releases exactly that owner's leases.
|
|
5. Repeated acquisition of the same `(asset, owner)` is idempotent.
|
|
6. Callers never derive a handle, owner, or generation through an integer cast.
|
|
7. Physical cache keys remain domain-specific (`ulong` mesh id,
|
|
`CompositeTextureKey`, surface DID). An adapter maps those keys to typed
|
|
handles; the manager does not replace their key or storage types.
|
|
|
|
## 4. State machine
|
|
|
|
The logical state machine is single-writer. Terminal failure states preserve
|
|
diagnostic identity until a new generation is requested.
|
|
|
|
| Current | Event | Next | Required accounting/action |
|
|
|---|---|---|---|
|
|
| `Absent` | request | `Requested` | allocate/current generation; record priority and request frame |
|
|
| `Requested` | prepare succeeds | `Prepared` | publish actual CPU prepared/decoded bytes |
|
|
| `Requested` | cancel | `Cancelled` | clear transient bytes; never publish a completion |
|
|
| `Requested` | missing | `Missing` | retain negative result only |
|
|
| `Requested` | corrupt | `Corrupt` | retain diagnostic result; do not retry silently |
|
|
| `Requested` | failure | `Failed` | retain typed failure; do not swallow |
|
|
| `Prepared` | queue upload | `UploadPending` | move or add staging/GPU-requested charges |
|
|
| `Prepared` | release with no owners | `Retiring` | enqueue logical eviction |
|
|
| `UploadPending` | upload succeeds | `Resident` | clear requested/staging charge; publish exact resident charge |
|
|
| `UploadPending` | retryable rollback completes | `Prepared` | clear GPU-requested charge; retain CPU data |
|
|
| `UploadPending` | cancel/no owners | `Retiring` | invalidate generation; stale completion is ignored |
|
|
| `UploadPending` | corrupt/failure | `Corrupt`/`Failed` | clear transient charges; retain diagnostic state |
|
|
| `Resident` | acquire/touch | `Resident` | update owner set, priority, frame, and rebuild cost |
|
|
| `Resident` | last owner releases | `Resident` | becomes eviction-eligible; physical cache may retain it |
|
|
| `Resident` | evict | `Retiring` | invalidate logical availability before physical release |
|
|
| `Retiring` | fence release completes | `Absent` | clear retiring bytes and recycle slot with next generation |
|
|
| any non-retiring state | session/world generation invalidates | `Retiring` or terminal cancel | reject later stale work |
|
|
| terminal failure state | explicit new request | `Requested` on a new generation | prior completion cannot revive the new request |
|
|
|
|
Illegal transitions throw in tests and emit a release diagnostic in
|
|
production composition; they are never silently coerced.
|
|
|
|
## 5. Threading and phase ownership
|
|
|
|
The graphical client currently advances update and render on one native window
|
|
thread. Slice D nevertheless fixes the contract at the future host boundary:
|
|
|
|
### Shared/worker side
|
|
|
|
- Decode and preparation workers may only enqueue immutable
|
|
`ResidencyObservation` records.
|
|
- Workers never mutate the manager ledger, budgets, owners, GL resources, or
|
|
render cache collections.
|
|
- The observation queue is bounded by active asset generations. Repeated
|
|
observations for one `(handle, kind)` coalesce last-writer before drain.
|
|
- Cancellation and corrupt/failure completion carry the exact asset generation.
|
|
|
|
### Update/resource-maintenance phase
|
|
|
|
- `ResidencyManager` is single-writer.
|
|
- At most one manager drain and policy pass runs per host tick.
|
|
- It applies observations, updates the lease/state ledger, captures domain
|
|
facts, and emits bounded `ResidencyTrimRequest` commands.
|
|
- Eviction order is: zero owners first, lowest priority first, oldest use
|
|
generation/frame first, lowest rebuild cost first, then stable handle index.
|
|
- Manager code never waits on decode, file I/O, a GPU fence, or a lock acquired
|
|
by the render path.
|
|
|
|
### Render-resource phase
|
|
|
|
- Specialized owners consume trim requests without calling back into the
|
|
manager while holding their internal mutation locks.
|
|
- Logical removal occurs before publication of a physical-release request.
|
|
- OpenGL non-residency/deletion and arena range return remain render-thread
|
|
only and fence delayed through the existing retirement owners.
|
|
- Physical completion is observed on the following manager drain. A stale
|
|
completion with the wrong handle generation is rejected.
|
|
|
|
No render-thread code blocks waiting for the manager, and the manager never
|
|
sees a raw GL name or bindless handle.
|
|
|
|
## 6. Accounting model
|
|
|
|
Every domain reports the following independent values:
|
|
|
|
- `LogicalBytes`: immutable metadata and logical entry overhead where measured.
|
|
- `CpuPreparedBytes`: parsed/prepared payload retained for reuse.
|
|
- `DecodedBytes`: canonical decoded pixels/audio/animation graphs.
|
|
- `PinnedBytes`: CPU data that cannot currently be evicted due to a live lease.
|
|
- `StagingBytes`: queued or claimed upload payload.
|
|
- `GpuRequestedBytes`: physical allocation requested but not yet published.
|
|
- `GpuResidentBytes`: currently drawable physical allocation.
|
|
- `RetiringBytes`: logically dead physical allocation awaiting a fence/retry.
|
|
- `MappedVirtualBytes`: immutable package address space, not committed RAM.
|
|
- `BudgetBytes`: the policy ceiling for the relevant charge.
|
|
- `CapacityBytes`, `UsedBytes`, `LargestFreeBytes`: allocator facts used to
|
|
calculate fragmentation without pretending free capacity is a leak.
|
|
|
|
Aggregate totals never add `MappedVirtualBytes` to committed CPU residence and
|
|
never add an arena's logical allocation bytes to its physical capacity twice.
|
|
|
|
## 7. Budgets and defaults
|
|
|
|
`RuntimeOptions` owns environment parsing once. `ResidencyBudgetOptions`
|
|
contains byte/count ceilings with the current production values as defaults:
|
|
|
|
- object mesh physical: 1 GiB;
|
|
- object mesh unowned entries: 50;
|
|
- prepared mesh CPU: 128 MiB / 100;
|
|
- staging: 128 MiB / 256;
|
|
- composite arrays: 128 MiB physical / 64 MiB unowned;
|
|
- standalone particle arrays: 32 MiB / 256;
|
|
- decoded animations: 64 MiB / 512;
|
|
- deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors.
|
|
|
|
Environment overrides are diagnostic/startup inputs and reject zero, negative,
|
|
overflowing, or malformed values by falling back to the documented default.
|
|
The settings quality preset may select a complete budget profile later; Slice D
|
|
first exposes the typed runtime target so a settings change is atomic rather
|
|
than mutating individual caches independently. Defaults preserve today's
|
|
visual radius and cache behavior.
|
|
|
|
Multi-session retuning is evidence driven. This slice records actual working
|
|
sets under normal, dense, and forced-pressure routes before changing production
|
|
defaults below their current values.
|
|
|
|
## 8. Delivery checkpoints
|
|
|
|
### D1 — Contract and logical ledger
|
|
|
|
- Add typed handles, owner tokens, leases, priorities, states, observations,
|
|
trim requests, budgets, and aggregate snapshots.
|
|
- Implement the transition table and generation-safe owner release.
|
|
- Test stale completions, duplicate acquire, owner reuse, asset-slot reuse,
|
|
cancellation, corrupt/failure states, and illegal transitions.
|
|
|
|
**Complete 2026-07-24.** The typed ledger, generation checks, bounded
|
|
worker-observation journal, startup budget profile, and transition conformance
|
|
tests landed without moving physical GL ownership into the policy layer.
|
|
|
|
### D2 — Existing-owner adapters
|
|
|
|
- Parameterize current mesh, staging, composite, and standalone budgets.
|
|
- Add exact snapshot facts to each owner.
|
|
- Register them with one manager without changing their physical storage or
|
|
fence protocol.
|
|
- Cross-check manager GPU totals with `GpuMemoryTracker`, allowing explicitly
|
|
enumerated non-cache resources.
|
|
|
|
**Complete 2026-07-24.** Production composition passes one immutable budget
|
|
profile into the existing mesh and texture owners and registers exact snapshot
|
|
sources for object geometry/atlases, prepared meshes, staging, the global mesh
|
|
arena, composite arrays, standalone particle arrays, and the prepared package
|
|
mapping. Removed object atlases and standalone textures retain retiring-byte
|
|
ownership until their accepted frame-fence release actually completes.
|
|
Lifecycle artifacts now publish both cache-attributed GPU bytes and the signed
|
|
`GpuTrackerMinusResidencyBytes` remainder; that remainder deliberately covers
|
|
enumerated non-cache allocations such as terrain, shaders, UI textures,
|
|
framebuffers, and dynamic draw buffers rather than being mislabeled as cache
|
|
residence.
|
|
|
|
### D3 — Missing policies
|
|
|
|
- Bound `RetailAnimationLoader` by count and estimated retained bytes.
|
|
- Bring deferred-alpha retained scratch behind a bounded capacity owner.
|
|
- Close stale #243 after proving no current bounds cache exists.
|
|
|
|
### D4 — Diagnostics and forced pressure
|
|
|
|
- Publish aggregate and per-domain budgets/occupancy/fragmentation through the
|
|
lifecycle diagnostic artifact.
|
|
- Add deterministic forced-pressure tests that exceed every existing ceiling,
|
|
drain fence release, and prove convergence.
|
|
- Assert manager and physical-owner accounting return to zero at teardown.
|
|
|
|
### D5 — Connected gate
|
|
|
|
- Run capped, uncapped, and dense connected routes against Slice C.
|
|
- Run repeated same-location portal loops and compare first/second/third visit.
|
|
- Require no missing texture/mesh, no stale-generation release, no monotonic
|
|
owner/resource growth, and graceful shutdown.
|
|
- Preserve fixed-camera screenshots and obtain the user's visual gate if the
|
|
connected route changes any visible frame.
|
|
|
|
## 9. Acceptance
|
|
|
|
Slice D closes only when:
|
|
|
|
1. All state/ownership/generation tests pass.
|
|
2. Forced-pressure tests prove the configured ceilings, not merely a route
|
|
that happens to stay below them.
|
|
3. The third same-location visit plateaus within the documented tolerance.
|
|
4. Repeated portal loops do not grow logical owners, staged work, resident GPU
|
|
resources, retiring resources, or parsed animation residence.
|
|
5. The complete Release build/test suite is green.
|
|
6. The architecture, roadmap, issues, inventory, and durable memory agree with
|
|
the implementation.
|
|
7. Connected screenshots retain Slice C's visual output.
|