acdream/docs/plans/2026-07-24-modern-runtime-slice-d-unified-residency.md
Erik 1853a57c12 feat(diagnostics): complete residency pressure ledger
Complete Slice D4 by adding aggregate lifecycle occupancy and traffic facts, validating physical source reports, and including decoded audio under the typed startup budget. Exercise every domain under forced pressure and retain the real cache/fence convergence gates.
2026-07-24 16:40:17 +02:00

16 KiB

Modern Runtime Slice D — Typed Asset Handles and Unified Residency

Status: active — D1 through D4 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

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;
  • decoded audio: 32 MiB;
  • 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.

Complete 2026-07-24. RetailAnimationLoader now uses a concurrent in-flight load gate feeding a byte-and-count bounded LRU. Cache eviction drops only the cache reference, so active sequencers retain valid immutable animations; duplicate DID requests coalesce while unrelated reads remain parallel. The shared retail alpha path now partitions its 16 MiB startup budget between the queue, object dispatcher, and particle renderer. Each physical owner grows immediately when a frame requires it, reports exact retained backing capacity, and shrinks only after repeated severe under-use. This bounds one-frame density spikes without changing draw order, particle content, or visible quality. Issue #240 is closed. Issue #243 is closed as a stale audit finding: no _boundsCache, bounds memo, or cited insertion site exists in the current source, so no replacement cache was invented.

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.

Complete 2026-07-24. The lifecycle artifact now serializes validated per-domain rows plus aggregate entry/owner, budget, allocator capacity/usage/fragmentation, traffic, committed CPU, mapped-address-space, and physical GPU totals. The final omitted bounded owner, decoded audio, now receives its startup budget through RuntimeOptions and reports decoded bytes and hit/miss/eviction traffic beside animations and the render caches. A deterministic all-domain pressure matrix exceeds every reported ceiling and proves zero-charge convergence. The focused physical-owner suites separately exercise prepared-mesh/staging rejection, composite and standalone texture eviction, fence-delayed release, global-arena range retirement, animation/audio LRU pressure, and alpha-scratch convergence. Release build and the complete 8,111-test / 5-skip solution gate are green.

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.