fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -488,6 +488,44 @@ identity plus animation, motion, physics, collision, selection, and
|
|||
dead-reckoning owners. Only a real delete/despawn tears those owners down. This
|
||||
matches retail's `CPhysicsObj::DoObjDescChangesFromDefault` behavior.
|
||||
|
||||
### Runtime resource ownership and bounded residency
|
||||
|
||||
Logical lifetime, spatial residency, and physical GPU lifetime are separate
|
||||
contracts. A live entity or landblock owns stable logical references; moving it
|
||||
between buckets does not reacquire resources. Appearance replacement acquires
|
||||
the complete new mesh/texture set before publication, then releases the old set.
|
||||
Despawn and landblock demotion withdraw every public render reference before
|
||||
their physical resources become reclaimable. All of these transactions are
|
||||
retryable and generation-scoped, so a failed release cannot silently strand a
|
||||
half-retired owner or affect a reused server GUID.
|
||||
|
||||
Runtime content residency is deliberately bounded rather than proportional to
|
||||
every region visited:
|
||||
|
||||
- `RuntimeDatCollectionFactory` keeps DAT indexes on demand but uses
|
||||
`FileCachingStrategy.Never`; `DatCollection` remains the sole raw reader.
|
||||
- Each typed DAT facade has a 256-entry / 64 MiB estimated LRU. Unknown object
|
||||
graphs are conservatively charged at least 128 KiB. Canonical decoded texture
|
||||
pixels use a separate 128-entry / 64 MiB cache.
|
||||
- Standalone bindless textures retain at most 256 unowned entries / 32 MiB and
|
||||
retire at most one per frame. Owner-scoped composite textures use a 64 MiB
|
||||
unowned budget and 128 MiB physical budget, admitting at most 16 uploads or
|
||||
8 MiB per frame.
|
||||
- `ObjectMeshManager` may retain at most 32 empty texture atlases / 64 MiB.
|
||||
`GlobalMeshBuffer` owns reclaimable vertex/index ranges capped at 384 MiB and
|
||||
128 MiB respectively, with an 896 MiB physical ceiling that includes an
|
||||
in-progress migration and its retired predecessor.
|
||||
|
||||
OpenGL deletion and range/slot reuse are not synonymous with logical release.
|
||||
`GpuFrameFlightController` fences three frames in flight. Mesh-buffer stores,
|
||||
texture handles, atlas layers, terrain slots, and landblock render records enter
|
||||
retirement only after they are no longer publishable, and their physical ids are
|
||||
recycled only after the corresponding fence signals. Shutdown follows the same
|
||||
dependency order and remains retryable: UI/controllers and render registrations
|
||||
withdraw first, then owner leases and caches, then GL backing stores. This keeps
|
||||
drivers from reading freed memory without adding a portal-specific purge or a
|
||||
visual-distance reduction.
|
||||
|
||||
---
|
||||
|
||||
## Per-Frame Update Order (current runtime)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Code Structure Rules" section in `CLAUDE.md`.
|
||||
**Purpose:** Describe the desired structural state of the App layer,
|
||||
explain the rules we've adopted, and lay out the safe extraction
|
||||
sequence from today's reality (one 10,304-line `GameWindow.cs`) to the
|
||||
sequence from today's reality (one 15,288-line `GameWindow.cs`) to the
|
||||
target (thin `GameWindow`, small focused collaborators).
|
||||
**Companion to:** [`acdream-architecture.md`](acdream-architecture.md)
|
||||
(the layered architecture) and
|
||||
|
|
@ -20,7 +20,7 @@ layer is wire-compatible, the UI has a stable contract, plugins load.
|
|||
The structural debt is concentrated in **one file**:
|
||||
|
||||
```
|
||||
src/AcDream.App/Rendering/GameWindow.cs 10,304 lines
|
||||
src/AcDream.App/Rendering/GameWindow.cs 15,288 lines
|
||||
```
|
||||
|
||||
`GameWindow` is the single object that:
|
||||
|
|
@ -75,28 +75,13 @@ delegate to a collaborator for the substance.
|
|||
a GL or windowing namespace, we've lost the ability to test it without
|
||||
a graphics context, and the layer split becomes fiction.
|
||||
|
||||
**How to apply:** The only currently-allowed seams from Core into the
|
||||
WB / GL world are:
|
||||
|
||||
- `WorldBuilder.Shared` — stateless helpers (`TerrainUtils`,
|
||||
`TerrainEntry`, `RegionInfo`).
|
||||
- `Chorizite.OpenGLSDLBackend.Lib` — stateless helpers
|
||||
(`SceneryHelpers`, `TextureHelpers`).
|
||||
|
||||
Both are leaf namespaces with no GL state. If you need a new seam (e.g.
|
||||
WB's `ObjectMeshManager` needs to be visible from Core), the change
|
||||
**must** come with an inventory-doc update justifying it and ideally a
|
||||
slim interface in Core that the App layer implements.
|
||||
|
||||
**Current reality:** `src/AcDream.Core/AcDream.Core.csproj` references
|
||||
`Chorizite.OpenGLSDLBackend` (not just `OpenGLSDLBackend.Lib`). This is
|
||||
historical. Two Core files import from it:
|
||||
- `World/SceneryGenerator.cs` — `Chorizite.OpenGLSDLBackend.Lib`
|
||||
- `Textures/SurfaceDecoder.cs` — `Chorizite.OpenGLSDLBackend.Lib`
|
||||
|
||||
Both use the stateless `Lib` namespace only. The full project reference
|
||||
is wider than it needs to be; tightening it to just `WorldBuilder.Shared`
|
||||
+ a stateless-helpers shim is a candidate future cut, but not urgent.
|
||||
**How to apply:** Phase O removed both external WorldBuilder/backend project
|
||||
references. The only currently allowed seams are the GL-free helpers owned in
|
||||
our tree under `src/AcDream.Core/Rendering/Wb/`: `TerrainUtils`,
|
||||
`TerrainEntry`, `RegionInfo`, `SceneryHelpers`, and `TextureHelpers`.
|
||||
`ObjectMeshManager` and every GL resource owner remain in App. If Core needs a
|
||||
new capability, define a narrow Core interface and implement it in App; adding
|
||||
a new project reference requires an inventory-doc update explaining why.
|
||||
|
||||
### Rule 3: UI panels target `AcDream.UI.Abstractions` only
|
||||
|
||||
|
|
@ -159,11 +144,12 @@ Today:
|
|||
- `tests/AcDream.Core.Tests/` ← `src/AcDream.Core/`
|
||||
- `tests/AcDream.Core.Net.Tests/` ← `src/AcDream.Core.Net/`
|
||||
- `tests/AcDream.UI.Abstractions.Tests/` ← `src/AcDream.UI.Abstractions/`
|
||||
- `tests/AcDream.App.Tests/` ← `src/AcDream.App/`
|
||||
|
||||
`AcDream.App` does **not** yet have a test project. The RuntimeOptions
|
||||
extraction is the trigger to create `tests/AcDream.App.Tests/`. Future
|
||||
App-layer tests (LiveSessionController, SelectionInteractionController,
|
||||
etc.) go there.
|
||||
`tests/AcDream.App.Tests/` now exists and owns App-layer controller, streaming,
|
||||
render-resource lifetime, retained-UI, and `RuntimeOptions` tests. New App tests
|
||||
belong there; do not place GL-free Core behavior in that project merely because
|
||||
App currently wires it.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-19 | Under outdoor roots, ALL dynamics draw in one z-buffered final pass; retail draws objects painter-ordered per landcell inside the landscape pass (interior roots route per **#118**) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs:126` | The dynamics-drawn-LAST invariant is what makes the aperture depth punch safe (first BR-2 attempt punched after dynamics and erased the player, reverted `88be519`); z-buffer substitutes for painter's order on opaque geometry | Punch/seal correctness hinges on an ordering invariant — any pass added after DrawDynamicsLast, or alpha content needing painter order, gets erased inside apertures or composites wrong | `LScape::draw` → `DrawBlock` 0x005a17c0 → DrawSortCell pc:430124; `PView::DrawCells` 0x005a4840 |
|
||||
| AD-20 | Camera sweep fallback seeds the eye's `AdjustPosition` from the PLAYER's cell; retail re-seats at the sought eye's own tracked cell (rest of function is a verbatim `update_viewer` port) | `src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:97` | acdream's camera doesn't track the sought-eye's cell separately; the eye is near the player so the player-cell stab list is assumed to cover it | An eye outside the player cell's stab-list coverage (boundary corners, cross-landblock pull-back) seats in the wrong cell — and the viewer cell roots the whole render: one-frame wrong root (flap-class flash) | `SmartBox::update_viewer` 0x00453ce0, pc:92878-92883 |
|
||||
| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for pre-spawn / login / legacy cameras; in-world retail always has a viewer_cell root | `src/AcDream.App/Rendering/GameWindow.cs:7671` | Result is null ONLY when neither an interior root nor the synthetic outdoor node exists; kept so the login screen shows the live sky | If viewer-root resolution ever returns null in-world (membership bug, fly-camera edge), the frame silently degrades — interiors stop drawing through doorways; the old two-branch FLAP reappears for those frames | `SmartBox::RenderNormalMode` decomp:92635 |
|
||||
| AD-22 | Async streamed mesh loading with point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's per-frame meshMissing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs:211` | Documented convergence argument: the self-heal makes absence transient, converging the async pipeline to retail's never-absent guarantee | A missing mesh referenced OUTSIDE the dispatcher's walk (a future consumer not touching meshMissing) stays permanently invisible — the #119/#128 broken-stairs class; best case, late pop-in | retail synchronous content load (note at WbMeshAdapter.cs:211) |
|
||||
| AD-22 | Async streamed mesh loading with bounded CPU replay residency, per-frame upload budgets, and point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's mesh-missing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadCaches.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadFrameBudget.cs` | Immutable preparation descriptors and the bounded CPU cache can re-stage an evicted mesh; dispatcher self-heal makes absence transient while upload budgets prevent a portal arrival from monopolizing a frame | A future consumer that neither retains an owner nor reaches the self-heal/replay path can remain invisible; under heavy admission pressure a valid mesh can pop in later than retail's synchronous path | retail synchronous content load; `docs/architecture/worldbuilder-inventory.md` portal-readiness and bounded-residency seams |
|
||||
| AD-23 | Live entities with `ServerGuid != 0` and null `ParentCellId` are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal |
|
||||
| AD-24 | EnvCell shell geometry hash-deduplicated ((environmentId, structure, surface overrides) → 31-multiplier hash) and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs:276` | Verbatim WB EnvCellRenderManager port (Phase A8); dedup is what makes the single-VAO MDI cell pipeline cheap; intended visuals identical | A hash collision between distinct tuples renders the wrong interior shell in some room with NO diagnostic firing — wrong walls/floor in a dungeon room | retail `PView::DrawCells` → per-cell drawing_bsp (cited at :319) |
|
||||
| AD-25 | **REMOTE-DR sweep only** (the player half retired 2026-07-07 by the #182 verbatim rebuild): the remote dead-reckoning post-resolve still reflects velocity with the airborne-before-AND-after suppression; retail bounces unless grounded→grounded-and-not-sledding. The PLAYER path now runs the ported `handle_all_collisions` (`PhysicsObjUpdate`) with retail's `should_reflect` rule — the micro-bounce spiral it guarded is gone (contact is committed BEFORE the reflect and the small-velocity-zero is ungated) | `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep post-resolve, #173 block) | The remote DR sweep hasn't been rebuilt yet (it has no fsf/SetPositionInternal chain); the old airborne-only suppression keeps remote landings from micro-bouncing on the remote landing-snap gate | Remote landing-reflection behavior (slope-landing momentum) won't reproduce; retire when the remote-DR sweep gets the same UpdateObjectInternal rebuild as the player | `handle_all_collisions` pc:282699-282715; ACE PhysicsObj.cs:2656-2721 |
|
||||
|
|
|
|||
|
|
@ -186,6 +186,20 @@ rendering and landblock mesh pins while retaining the terrain slot. Core's
|
|||
matching physics demotion preserves the terrain surface but removes indoor
|
||||
cells, portals, buildings, and static shadow registrations.
|
||||
|
||||
**Bounded residency and GPU retirement seam (2026-07-18).** Runtime DAT access
|
||||
keeps raw file payload caching disabled and layers bounded typed-object and
|
||||
decoded-pixel LRUs above the single `DatCollection`. `ObjectMeshManager`, the
|
||||
standalone bindless texture cache, and the owner-scoped composite texture-array
|
||||
cache all distinguish an active owner from an evictable unowned entry. Appearance
|
||||
changes and landblock demotion acquire-before-publish and withdraw-before-release;
|
||||
rebucketing never creates a second owner. `GlobalMeshBuffer` uses reclaimable,
|
||||
coalescing vertex/index ranges and migrates incrementally within explicit physical
|
||||
ceilings. Texture layers, terrain slots, mesh ranges, and old backing stores are
|
||||
returned only after `GpuFrameFlightController` observes the frame fence that can
|
||||
no longer reference them. This lifetime machinery is acdream-owned integration
|
||||
around the extracted WB mesh pipeline; it does not add a second DAT decoder or a
|
||||
reduced-distance rendering path.
|
||||
|
||||
**Workflow:** Before re-implementing any AC-specific rendering or dat-handling
|
||||
algorithm, **check this inventory first**. If we already extracted it (🟢
|
||||
sections), it's in `src/AcDream.App/Rendering/Wb/` — use our copy. If WB has
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue