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
|
|
@ -46,7 +46,7 @@ Copy this block when adding a new issue:
|
|||
|
||||
## #225 — Scene particles overpaint translucent world objects
|
||||
|
||||
**Status:** IN-PROGRESS — implementation complete; connected visual gate pending
|
||||
**Status:** IN-PROGRESS — implementation/reviews and connected stress gate pass; final visual gate pending
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-07-18
|
||||
**Component:** rendering / world translucency / particles
|
||||
|
|
@ -79,7 +79,7 @@ order inside one instanced draw, with only DAT blend-mode changes splitting a
|
|||
run. The identical location recovered to about 153 FPS / 6.6 ms in the
|
||||
connected Release client.
|
||||
|
||||
A second, genuinely cumulative portal regression remained: ACE retains
|
||||
A second, genuinely cumulative portal regression was first isolated: ACE retains
|
||||
`KnownObjects` across normal teleports, while acdream retained every old
|
||||
`LiveEntityRecord` indefinitely. Each destination therefore left animation,
|
||||
effect, and render owners active; after enough trips the render thread blocked
|
||||
|
|
@ -93,6 +93,30 @@ duplicating vertices per material and growing forever. A connected five-region
|
|||
round trip returned live/animation ownership to baseline, recreated C95B on
|
||||
revisit, and held its normal 60–80 FPS.
|
||||
|
||||
That shorter route did not close the process-lifetime problem. A subsequent
|
||||
multi-recall run still climbed to about 3.0 GiB working set / 3.5 GiB private
|
||||
memory and reproduced the 5–12 FPS collapse, with effect emitters, composite
|
||||
textures, decoded DAT objects, texture atlases, and physical GL stores remaining
|
||||
resident after their owners left. The final integration therefore makes the
|
||||
whole chain owner-scoped and bounded: exact-incarnation appearance replacement,
|
||||
retryable live/landblock/UI/portal teardown, emitter retirement indexes,
|
||||
bounded DAT/decoded/standalone/composite caches, reclaimable mesh/atlas storage,
|
||||
incremental arena migration, and three-frame GPU-fenced physical reuse. It also
|
||||
reuses per-frame scratch storage without clearing a legitimate large working set.
|
||||
No draw distance, texture resolution, particle range, or visual effect was
|
||||
reduced.
|
||||
|
||||
The final connected route (Caul → Sawato → Rynthid → Aerlinthe → Sawato →
|
||||
Holtburg → Caul, with 25–30 second destination dwells and 60 seconds after the
|
||||
return) passed without an exception, WER report, or AMD display-driver reset.
|
||||
Peak working set fell from 2,954.5 MiB to 1,493.4 MiB and peak private memory
|
||||
from 3,502.3 MiB to 1,969.5 MiB versus the failing build. Returned Caul settled
|
||||
at 1,030.6 MiB working set / 1,638.2 MiB private / 831.6 MiB local GPU; the final
|
||||
local-display dwell held 125–153 FPS (141.8 average). Emitter/binding ownership
|
||||
balanced at 1,715/1,715 and composite physical residency remained below its
|
||||
128 MiB ceiling. The older 32 FPS comparison run was RDP-refresh-capped and is
|
||||
used only for memory comparison. The lifestone/particle visual gate remains.
|
||||
|
||||
**Files:** `src/AcDream.App/Rendering/RetailAlphaQueue.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`;
|
||||
`src/AcDream.App/Rendering/ParticleRenderer.cs`;
|
||||
|
|
@ -100,11 +124,17 @@ revisit, and held its normal 60–80 FPS.
|
|||
`src/AcDream.App/Rendering/Shaders/particle.frag`;
|
||||
`src/AcDream.App/Rendering/RetailPViewRenderer.cs`;
|
||||
`src/AcDream.App/World/LiveEntityLivenessController.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs`.
|
||||
`src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs`;
|
||||
`src/AcDream.App/Rendering/GpuFrameFlightController.cs`;
|
||||
`src/AcDream.App/Rendering/CompositeTextureArrayCache.cs`;
|
||||
`src/AcDream.App/Rendering/StandaloneBindlessTextureCache.cs`;
|
||||
`src/AcDream.Content/BoundedDatObjectCache.cs`;
|
||||
`src/AcDream.Content/DecodedTextureCache.cs`.
|
||||
|
||||
**Research:**
|
||||
`docs/research/2026-07-18-retail-shared-alpha-list-pseudocode.md`;
|
||||
`docs/research/2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md`.
|
||||
`docs/research/2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md`;
|
||||
`docs/research/2026-07-18-retail-texture-resource-lifetime-pseudocode.md`.
|
||||
|
||||
**Acceptance:** At a translucent lifestone, smoke or flame behind the crystal
|
||||
is attenuated by it while an effect in front remains bright. The lifestone's
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **Missile/portal VFX campaign Step 8 implemented and independently reviewed 2026-07-14.** `LiveEntityRuntime` now distinguishes raw server PhysicsState from retail's side-effect-derived final state, beginning from constructor state `0x00400C08` and applying `set_state` in Lighting → NoDraw → Hidden order. `LiveEntityPresentationController` keeps Hidden objects logically alive while suppressing root mesh, collision, picking, radar, status, and new target acquisition; typed `PS_Hidden`/`PS_UnHide` plays remain DAT-driven, direct equipped children inherit NoDraw, particles/lights survive, and repeated/stale state cannot replay a transition. A normal visible CreateObject never fabricates UnHide. Effect-owner preparation is separate from pending F754/F755 replay so construction-state effects always run first. Remote fresh-teleport/cell-less placement now executes the exact ordered `teleport_hook` action bundle and a full-cell hard placement before contact/airborne routing, preventing an airborne correction from restoring the old location. Projectile Hidden state pauses the retained body without consuming its active identity or accumulating a clock backlog. TS-43 is retired; AP-69 remains for exact ObjCell-PVS/preview-retention parity after the 2026-07-18 liveness port.
|
||||
- **Missile/portal VFX campaign Step 9 automated hardening implemented and independently reviewed 2026-07-14; final two-client visual gate pending.** A deterministic 96-owner App fixture drives canonical missiles and effect owners through projectile updates, repeated DAT-effect scheduling, loaded↔pending↔loaded landblock churn, light/particle withdrawal and recovery, accepted deletes, same-GUID generation reuse, never-created F754 queues, and session reset. It asserts balanced logical render registration and zero residual records/bodies/projectiles, spatial buckets/rescues, shadows (including suspended registrations), effect profiles/packets, PhysicsScript FIFOs/anchors/delayed calls, particle bindings/logical IDs/render-pass owners, poses, light-controller/sink/manager owners, and stale record component references. A companion twelve-cycle gate drives exact recall motion `0x10000153` through AnimationSequencer/CallPES, Hidden, deferred remote placement, hydration, and UnHide; a second 96-owner `EntitySpawnAdapter` gate balances actual mesh-adapter reference counts without GL. The pass fixed undrained persistent rescue retention, delayed stale visibility edges, GUID-scoped teardown, create resurrection after a nested delete/reset, non-atomic resource registration, double teardown from a re-entrant session-clear callback, observer failure stranding later visibility edges, and stale outer projection transactions overwriting callback-created replacements. Teardown now removes exact projection references and generation/local-ID owners, uses per-GUID/session lifetime epochs plus per-record projection tokens, drains visibility fan-out before aggregating failures, and finishes or supersedes canonical commits before surfacing observer errors. Core remains GL/backend-free, panels remain on UI abstractions, and projectiles still draw through ordinary `WbDrawDispatcher` live-entity submission rather than a global projectile pass. AP-69 and TS-49 remain; AP-83/AP-91 now explicitly describe only a future mover that enables PerfectClip.
|
||||
|
||||
- **Portal lifetime/performance correction 2026-07-18; user visual gate pending.** `LiveEntityLivenessController` ports retail's 25-second leave-visibility destruction queue over canonical live records, cancels expiry for spatially resident or owned objects, and uses holtburger's conservative 384-unit envelope where ACE supplies no client-visible PVS leave event. Expiry reuses the exact generation-safe F747 teardown. The modern `GlobalMeshBuffer` now uploads vertices once per mesh and owns coalescing vertex/index ranges released by zero-reference LRU eviction. A connected five-region route returned animation ownership to baseline, reclaimed/reused GPU ranges, recreated the starting region on revisit, and prevented the prior persistent 3–12 FPS collapse. AP-69 is narrowed rather than retired because exact ObjCell PVS and explicit preview lifetimes remain.
|
||||
- **Portal/resource lifetime correction 2026-07-18; connected stress gate passed, final visual gate pending.** `LiveEntityLivenessController` ports retail's 25-second leave-visibility destruction queue over canonical live records, cancels expiry for spatially resident or owned objects, and uses holtburger's conservative 384-unit envelope where ACE supplies no client-visible PVS leave event. The broader integration balances generation-scoped appearance/live/landblock/effect/UI/portal owners, bounds DAT/decoded/standalone/composite/atlas/mesh residency, performs incremental mesh-arena migration, and defers GL deletion or slot reuse through three frames of GPU fences. Render/UI scratch storage is reused with hysteretic trimming. No visual range or quality setting was reduced. The final seven-destination connected route cut peak working/private memory from 2,954.5/3,502.3 MiB to 1,493.4/1,969.5 MiB, returned Caul to 1,030.6/1,638.2 MiB, held 125–153 FPS locally through the final dwell, and produced no WER/driver reset. AP-69 is narrowed rather than retired because exact ObjCell PVS and explicit preview lifetimes remain.
|
||||
|
||||
**Reference docs:** `docs/research/retail-ui/00-master-synthesis.md` + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.
|
||||
|
||||
|
|
@ -1311,6 +1311,10 @@ port in any phase — no separate listing here.
|
|||
> number and accepts the rewrite risk. Rust rejected. Parked (resume from the table
|
||||
> below): MP1b pak/bake (needs the 865 GB EnvCell-dedup slice; loading-speed lever),
|
||||
> MP-Alloc hard sites, MP1c, MP2. Full rationale: `project_mp_track_findings.md`.
|
||||
> The 2026-07-18 portal/resource-lifetime repair does not resume MP's ECS, pak,
|
||||
> or throughput redesign. It is corrective ownership/reclamation work for a
|
||||
> reproduced crash-class regression in the mandatory renderer, using the current
|
||||
> architecture and preserving pixels/ranges.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||
umbrella design — read it first). **Goal:** smoothness first (no frame over
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@
|
|||
**Currently working toward:** **M3 — "Cast a spell." M2 — "Kill a drudge"
|
||||
LANDED 2026-07-15.** The complete connected melee/missile, death/loot,
|
||||
inventory, and item-giving demo is user-gated. The shared projectile/effect
|
||||
foundation is hardened through Step 9 and its single-client visual gate passes.
|
||||
Next is the connected F.5 component-book/favorite-bar visual gate, followed by
|
||||
cast/enchantment presentation and the two-client portal/observer VFX gate.
|
||||
foundation is hardened through Step 9. Spellbook/component-book filtering,
|
||||
favorite spell bar, connected casting/enchantment effects, and portal-space
|
||||
presentation have passed their single-client visual gates. The corrective bounded
|
||||
render/resource-lifetime integration passed its seven-destination connected
|
||||
stress gate without a crash or cumulative collapse. The remaining M3 visual
|
||||
requirement is the final two-client portal-out/materialize observer gate.
|
||||
Carried:
|
||||
#145-residual, #116 slide-response, R6/TS-42, and Track MP0.
|
||||
|
||||
|
|
@ -285,6 +288,11 @@ as ad-hoc rework. Rule 2's freeze still binds all NON-MP work. MP runs in
|
|||
dedicated side-track sessions; the M1.5 critical path wins every conflict,
|
||||
and MP4 is hard-queued behind #137.
|
||||
|
||||
The 2026-07-18 repeated-portal lifetime repair is a freeze-allowed corrective
|
||||
change, not a resumption of Track MP. It keeps the current renderer and visual
|
||||
ranges while balancing owners, bounding residency, and fence-delaying physical
|
||||
GPU reuse; MP's pak/ECS/throughput redesign remains parked.
|
||||
|
||||
**Dungeon support (Phase G.3, issue #133) — SHIPPED.** The original
|
||||
premise here (terrain-less dungeon landblocks unsupported anywhere in the
|
||||
streaming/load/render/physics pipeline) was refuted at design time
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
# Retail texture and per-object material lifetime
|
||||
|
||||
## Scope
|
||||
|
||||
This note records the retail ownership rules needed to fix the repeated-recall
|
||||
resource buildup in acdream. It does not attempt to reproduce Direct3D 9 or
|
||||
retail's whole database cache. The modern renderer keeps shared atlas textures,
|
||||
but per-object palette/texture composites must follow the same owner lifetime as
|
||||
retail's `CSurface`/`ImgTex` pair.
|
||||
|
||||
## Retail oracle
|
||||
|
||||
Named Sept-2013 client symbols and pseudo-C:
|
||||
|
||||
- `CSurface::SetTextureAndPalette` `0x00535FB0`
|
||||
- `CSurface::Destroy` `0x005361F0`
|
||||
- `CSurface::UseTextureMap(ImgTex*, SurfaceHandlerEnum)` `0x00536350`
|
||||
- `CSurface::InitEnd` `0x00536400`
|
||||
- `CSurface::RestorePalShiftSurface` `0x00536530`
|
||||
- `ImgTex::PurgeResource` `0x0053E550`
|
||||
- `ImgTex::GetD3DTexture` `0x0053E5B0`
|
||||
- `ImgTex::CreateD3DTexture` `0x0053EDB0`
|
||||
- `DBCache::UseTime` `0x00414090`
|
||||
- `DBCache::FlushFreeObjects` `0x004141E0`
|
||||
- `GraphicsResource::PurgeOldResources` `0x00446C60`
|
||||
- `SceneTool::PurgeOldGraphicsResources` `0x0043E5C0`
|
||||
|
||||
Modern reference seam:
|
||||
|
||||
- WorldBuilder's extracted `TextureAtlasManager` reference-counts a texture
|
||||
layer and releases it when its last mesh user disappears.
|
||||
- acdream's `ObjectMeshManager.UnloadObject` already disposes an empty atlas
|
||||
and removes it from `_globalAtlases`; the missing lifetime is the separate
|
||||
`TextureCache` used by live-object composites.
|
||||
|
||||
## Pseudocode
|
||||
|
||||
### A database surface owns its current image texture
|
||||
|
||||
```text
|
||||
CSurface::UseTextureMap(newTexture):
|
||||
if current base1map exists:
|
||||
current base1map.Release()
|
||||
current base1map = null
|
||||
|
||||
current base1map = newTexture
|
||||
newTexture.AddRef()
|
||||
```
|
||||
|
||||
### Replacing a palette composite releases the previous composite
|
||||
|
||||
```text
|
||||
CSurface::SetTextureAndPalette(sourceTexture, palette):
|
||||
combined = ImgTex::CreateCombinedTexture(sourceTexture, palette, clipmap)
|
||||
if combined is null:
|
||||
fail
|
||||
|
||||
if current base1map exists:
|
||||
current base1map.Release()
|
||||
current base1map = null
|
||||
|
||||
current base1map = combined
|
||||
```
|
||||
|
||||
`RestorePalShiftSurface` follows the same order: release the old `base1map`,
|
||||
create the replacement combined texture, then install it.
|
||||
|
||||
### Object destruction releases its material resources immediately
|
||||
|
||||
```text
|
||||
CSurface::Destroy():
|
||||
if base1map exists:
|
||||
base1map.Release()
|
||||
base1map = null
|
||||
|
||||
if base1pal exists:
|
||||
Palette::releasePalette(base1pal)
|
||||
base1pal = null
|
||||
|
||||
reset surface fields
|
||||
```
|
||||
|
||||
This is the important portal/recall rule: a palette-shifted texture is not a
|
||||
session-global retain-forever entry. Its owning surface contributes a reference,
|
||||
and destroying/replacing that surface releases the reference.
|
||||
|
||||
### Shared image resources are separately purgeable
|
||||
|
||||
```text
|
||||
ImgTex::GetD3DTexture():
|
||||
if resource is lost:
|
||||
RestoreResource()
|
||||
TimeUsed = Timer.local_time
|
||||
FrameUsed = render_device.frame_stamp
|
||||
return D3D texture
|
||||
|
||||
SceneTool::PurgeOldGraphicsResources():
|
||||
every 5 seconds:
|
||||
if available video memory is low:
|
||||
GraphicsResource::PurgeOldResources(unused-age threshold)
|
||||
|
||||
GraphicsResource::PurgeOldResources(age):
|
||||
for each registered graphics resource:
|
||||
if not lost
|
||||
and not used this frame
|
||||
and it is thrashable
|
||||
and TimeUsed is older than age:
|
||||
resource.PurgeResource()
|
||||
mark resource lost
|
||||
```
|
||||
|
||||
Shared DAT image residency and owner-scoped composites are therefore two
|
||||
different lifetimes. The modern atlas is acdream's shared-image adaptation;
|
||||
palette and original-texture overrides remain owner-scoped.
|
||||
|
||||
## acdream port contract
|
||||
|
||||
```text
|
||||
ResolveTexture(entity, meshBatch):
|
||||
if no original-texture override
|
||||
and no applicable palette override:
|
||||
use meshBatch's existing shared atlas handle and layer
|
||||
|
||||
if entity has a palette override
|
||||
and the resolved RenderSurface is P8 or INDEX16:
|
||||
get/create one owner-scoped palette composite
|
||||
record (entity local id -> composite key)
|
||||
use composite handle, layer 0
|
||||
|
||||
else if entity has an original-texture override:
|
||||
get/create one owner-scoped override texture
|
||||
record (entity local id -> override key)
|
||||
use override handle, layer 0
|
||||
|
||||
else:
|
||||
use shared atlas handle and layer
|
||||
```
|
||||
|
||||
```text
|
||||
ReleaseTextureOwner(entity local id):
|
||||
for each composite key first acquired by that owner:
|
||||
decrement the key's owner count
|
||||
if count becomes zero:
|
||||
mark the cache entry unowned and therefore evictable
|
||||
|
||||
EvictUnownedTexture(key):
|
||||
remove key from every public lookup first
|
||||
enqueue its handle/resource against the current GPU frame serial
|
||||
after the retirement fence signals:
|
||||
make bindless handle non-resident
|
||||
delete GL texture or recycle the array layer
|
||||
remove physical diagnostic metadata
|
||||
```
|
||||
|
||||
The registry is idempotent per `(owner, key)`, so classifying the same animated
|
||||
object every frame does not add references. Appearance replacement resolves and
|
||||
acquires the complete desired set first, publishes that exact incarnation, then
|
||||
releases resources no longer used by the old description. Logical entity
|
||||
teardown withdraws publication and releases the same set exactly once.
|
||||
|
||||
Keeping a bounded unowned cache is the modern equivalent of retail's separately
|
||||
purgeable `GraphicsResource`: it does not change the owner contract. Current
|
||||
production budgets are 32 MiB / 256 unowned standalone textures and 64 MiB
|
||||
unowned / 128 MiB physical composite arrays. Physical destruction/reuse waits
|
||||
for the three-frame fence owner so the GPU cannot still be sampling a resource
|
||||
whose logical owner has disappeared.
|
||||
|
||||
## Captured failure evidence
|
||||
|
||||
The connected C95B -> 3032 -> F682 -> 0904 -> C95B sequence retained normal
|
||||
world ownership (`139` live network objects, `13` animated objects, `1,705`
|
||||
particle emitters) but `TextureCache` held `1,945` GL textures:
|
||||
|
||||
- `527` legacy palette composites pre-warmed by `EntitySpawnAdapter` but never
|
||||
consumed by the modern draw path;
|
||||
- `366` base bindless textures duplicating textures already resident in the
|
||||
WorldBuilder atlas;
|
||||
- `587` bindless palette composites, including stale objects from earlier
|
||||
destinations and duplicates for non-indexed surfaces.
|
||||
|
||||
The process reached about 2.7 GB private memory, about 891 MB dedicated GPU
|
||||
memory, and 5-8 FPS. A managed CPU trace spent nearly the entire render thread
|
||||
inside native rendering, while a GC snapshot reduced working set but did not
|
||||
recover frame rate. That combination isolates retained GL texture residency,
|
||||
not live-entity, particle, or managed-update counts, as the remaining
|
||||
recall-triggered buildup.
|
||||
|
||||
## Final connected conformance evidence
|
||||
|
||||
The integrated build ran Caul → Sawato → Rynthid → Aerlinthe → Sawato →
|
||||
Holtburg → Caul with 25–30 second destination dwells and a 60-second final
|
||||
reclamation dwell. Compared with the failing build on the same route:
|
||||
|
||||
| Metric | Failing build | Integrated build |
|
||||
|---|---:|---:|
|
||||
| Peak working set | 2,954.5 MiB | 1,493.4 MiB |
|
||||
| Peak private bytes | 3,502.3 MiB | 1,969.5 MiB |
|
||||
| Final Caul working set | 1,826.2 MiB | 1,030.6 MiB |
|
||||
| Final Caul private bytes | 2,433.7 MiB | 1,638.2 MiB |
|
||||
|
||||
Final local GPU residency was 831.6 MiB and stable through the dwell. The
|
||||
composite cache ended at 65,780,224 unowned bytes and 122,437,632 physical
|
||||
bytes, below its 64 MiB policy modulo one entry's admission granularity and
|
||||
below the strict 128 MiB physical ceiling. Emitter and binding indexes balanced
|
||||
at 1,715 each. Caul held 125–153 FPS (141.8 average) on the local display.
|
||||
There was no application WER event or AMD display-driver reset. The old run's
|
||||
32 FPS was imposed by the RDP monitor's 32 Hz refresh and is not a performance
|
||||
comparison; its process-memory measurements remain valid.
|
||||
Loading…
Add table
Add a link
Reference in a new issue