fix(streaming): preserve portal destination ownership
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
parent
2c848d4167
commit
823936ec31
57 changed files with 2551 additions and 324 deletions
|
|
@ -55,6 +55,15 @@ The portal viewport is a replacement for the world viewport. It is not a
|
|||
full-screen UI overlay: normal retained UI continues to update and draw above
|
||||
it.
|
||||
|
||||
Private retained-UI viewports also keep their own objects. In particular,
|
||||
`gmPaperDollUI::RedressCreature @ 0x004A3BC0` creates
|
||||
`m_pInventoryObject` only when it is absent and the SmartBox player is
|
||||
available; a temporarily unavailable world player does not clear an already
|
||||
constructed inventory object. `gmPaperDollUI::~gmPaperDollUI @ 0x004A3590`
|
||||
removes that private object at the UI/session lifetime boundary. Therefore a
|
||||
portal-space world quiescence may defer a paperdoll redress, but it must not
|
||||
empty and later reconstruct the visible private paperdoll.
|
||||
|
||||
## Begin/end and scene visibility
|
||||
|
||||
```text
|
||||
|
|
@ -96,6 +105,36 @@ distance, hides the portal viewport, shows the world viewport, clears the
|
|||
teleport object's sequence animations, plays `Sound_UI_ExitPortal`, and enters
|
||||
`WorldFadeIn`. That following state restores the ordinary game view distance.
|
||||
|
||||
The viewport swap is also the end of the destination-cell blocking edge, not
|
||||
the end of the complete teleport protocol:
|
||||
|
||||
```text
|
||||
TunnelFadeOut completes:
|
||||
destination cells are already available
|
||||
release SmartBox world blocking
|
||||
hide portal viewport
|
||||
show world viewport
|
||||
enter WorldFadeIn
|
||||
|
||||
WorldFadeIn completes one second later:
|
||||
send LoginComplete
|
||||
clear teleport-in-progress
|
||||
enter Off
|
||||
```
|
||||
|
||||
The two edges must remain distinct in an asynchronous client. Holding world
|
||||
blocking until `LoginComplete` creates a one-second clear-color/void interval
|
||||
after the tunnel viewport has already been hidden. Conversely, marking the
|
||||
whole teleport complete at the viewport swap loses retail's final
|
||||
`WorldFadeIn` protocol state.
|
||||
|
||||
The asynchronous renderer may temporarily spend otherwise-idle resource
|
||||
capacity on the withheld destination, but that policy belongs to the exact
|
||||
reveal generation. Acdream raises only the count of small mesh/composite
|
||||
uploads while keeping the ordinary byte, array, buffer, and mipmap ceilings.
|
||||
The profile ends at the viewport swap above. It never changes the required
|
||||
destination radius, the readiness predicate, or the resulting pixels.
|
||||
|
||||
## Camera motion
|
||||
|
||||
The camera rolls around its own AC `+Y` forward axis. It does not yaw around
|
||||
|
|
|
|||
|
|
@ -59,12 +59,35 @@ preview-owned objects. Sources:
|
|||
- [ACE Player teleport tracking](https://github.com/ACEmulator/ACE/blob/650c5b75ae909957feaf58db320e46be16502653/Source/ACE.Server/WorldObjects/Player_Tracking.cs)
|
||||
- [Holtburger liveness](https://github.com/merklejerk/holtburger/blob/main/crates/holtburger-world/src/state/liveness.rs)
|
||||
|
||||
acdream uses the same compatibility boundary: currently spatially resident
|
||||
acdream uses the same visibility boundary: currently spatially resident
|
||||
records are visible; otherwise a 384-unit 3-D global-landblock distance is the
|
||||
conservative fallback. The distance is an adaptation, not a claimed retail
|
||||
constant, and remains documented in divergence AP-69 until exact ObjCell/PVS
|
||||
membership drives the deadline.
|
||||
|
||||
There is an additional ACE compatibility boundary. Retail can destroy the
|
||||
complete client object because its server/client visibility protocol later
|
||||
recreates that object. ACE may retain the GUID in `KnownObjects` across a
|
||||
normal teleport and consequently omit the later `CreateObject`. Deleting
|
||||
acdream's only accepted `EntitySpawn` at the 25-second deadline therefore
|
||||
made doors, signs, portals, NPCs, and other server objects permanently absent
|
||||
when a destination was revisited.
|
||||
|
||||
The modern client separates those two costs:
|
||||
|
||||
- the 25-second deadline destroys every active projection, collision,
|
||||
animation, physics, script, light, particle, and render owner;
|
||||
- a cold `EntitySpawn` snapshot remains in a data-only dormant table;
|
||||
- reloading its authoritative landblock routes that snapshot through the
|
||||
ordinary generation-safe CreateObject hydration transaction;
|
||||
- an explicit F747, a newer INSTANCE timestamp, or session reset removes the
|
||||
dormant snapshot.
|
||||
|
||||
This is deliberately not active-object retention: dormant entries execute no
|
||||
per-frame work and own no GPU resources. It restores the former retained
|
||||
spawn-table contract required by ACE while preserving the resource reclamation
|
||||
that fixed the portal-run FPS collapse.
|
||||
|
||||
## Port pseudocode
|
||||
|
||||
```text
|
||||
|
|
@ -93,14 +116,30 @@ once per second:
|
|||
for each due candidate:
|
||||
re-read canonical record
|
||||
if GUID and generation still match:
|
||||
route through the normal accepted DeleteObject lifecycle
|
||||
capture its latest accepted EntitySpawn
|
||||
run the normal generation-safe active teardown
|
||||
retain only the cold EntitySpawn
|
||||
|
||||
when a landblock becomes resident:
|
||||
for each dormant spawn in that canonical landblock:
|
||||
route it through ordinary CreateObject hydration
|
||||
remove the dormant entry only after the generation is accepted
|
||||
|
||||
on explicit DeleteObject:
|
||||
delete the exact active or dormant generation
|
||||
remove its ClientObject qualities
|
||||
|
||||
on session reset:
|
||||
clear active runtime and dormant snapshots
|
||||
```
|
||||
|
||||
Routing expiry through `LiveEntityRuntime.UnregisterLiveEntity` is important:
|
||||
it performs the same generation gate, object-table removal, equipped-child
|
||||
cleanup, spatial withdrawal, animation/physics/effect teardown, and render
|
||||
resource release as an authoritative F747. A second teardown path would drift
|
||||
and recreate the original leak.
|
||||
it performs the same generation gate, equipped-child cleanup, spatial
|
||||
withdrawal, animation/physics/effect teardown, and render resource release as
|
||||
an authoritative F747. Unlike F747, local visibility expiry preserves the
|
||||
small object-quality record and cold spawn because ACE may never resend them.
|
||||
An actual F747 removes both. A second active-resource teardown path would
|
||||
drift and recreate the original leak.
|
||||
|
||||
## Reclaimable shared-buffer contract
|
||||
|
||||
|
|
|
|||
|
|
@ -172,8 +172,11 @@ begin destination generation:
|
|||
silence old-generation audio
|
||||
reject old generation from collision, picking, radar, and targeting
|
||||
|
||||
at the shared-origin transaction boundary:
|
||||
atomically swap the complete old spatial generation out of reach
|
||||
retain one exact teardown receipt per old landblock
|
||||
|
||||
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
|
||||
|
|
@ -200,6 +203,48 @@ 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.
|
||||
|
||||
The shared-origin commit barrier is the completion of **spatial detachment**,
|
||||
not completion of every deferred resource release:
|
||||
|
||||
```text
|
||||
capture exact old landblock/entity teardown receipts
|
||||
clear the old loaded/pending/traversal/collision-visible spatial indexes
|
||||
repark nonpersistent live projections by their canonical landblock
|
||||
rescue the persistent local-player projection
|
||||
publish the resulting live visibility edges once
|
||||
|
||||
once that single spatial-generation swap commits:
|
||||
recenter the shared coordinate origin
|
||||
configure and enqueue the destination window
|
||||
|
||||
on later frames:
|
||||
destination publication uses its reserved work lane
|
||||
old retirement receipts use remaining cleanup capacity
|
||||
a publication for landblock X waits only while X has an older receipt
|
||||
the exact receipt for destination X may advance ahead of unrelated FIFO
|
||||
ordinary publication continues when no destination result is available
|
||||
```
|
||||
|
||||
The per-landblock publication fence makes key-based terrain, physics,
|
||||
EnvCell, building, mesh-reference, and static-presentation cleanup safe: a
|
||||
late old receipt cannot remove a replacement owner with the same landblock
|
||||
key. Live objects are different. Their logical owner survives spatial
|
||||
withdrawal and may be reprojected immediately at the destination, so deferred
|
||||
landblock retirement must never clear a live object's lighting or
|
||||
translucency by reusable local ID. Live cell-scoped lighting is withdrawn by
|
||||
the incarnation-aware projection-visibility edge; logical translucency stays
|
||||
with the live record until its actual teardown. Landblock receipts clean only
|
||||
DAT-static entity presentation.
|
||||
|
||||
This swap is deliberately one **atomic oversized progress operation** in the
|
||||
modern frame meter. Splitting it into 625 budgeted landblock detach admissions
|
||||
created a five-second portal stall even though the old generation was already
|
||||
quiesced and no intermediate subset was observable. The atomic work scans the
|
||||
small live-projection index, clears the old spatial maps, and transfers their
|
||||
existing entity lists into receipts; it does not synchronously destroy every
|
||||
static resource. Expensive script, renderer, physics, light, plugin, and GL
|
||||
cleanup still advances through the ordinary retryable receipt cursors.
|
||||
|
||||
## Port constraints
|
||||
|
||||
1. Preserve existing exact retry receipts; successful stages never replay.
|
||||
|
|
|
|||
105
docs/research/2026-07-25-portal-regression-closeout.md
Normal file
105
docs/research/2026-07-25-portal-regression-closeout.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Portal regression closeout — 2026-07-25
|
||||
|
||||
## Scope
|
||||
|
||||
The first connected retained-scene visual pass reported:
|
||||
|
||||
1. portal transit remained in the tunnel too long;
|
||||
2. a clear/void interval appeared between tunnel exit and destination world;
|
||||
3. the inventory paperdoll disappeared and rebuilt during transit;
|
||||
4. doors, signs, portals, NPCs, and other server-spawned objects disappeared
|
||||
after repeated portal travel.
|
||||
|
||||
This note records the root causes and the accepted correction. It does not
|
||||
reduce world radius, texture quality, particles, readiness, or retail
|
||||
presentation.
|
||||
|
||||
## Retail and protocol anchors
|
||||
|
||||
- `gmSmartBoxUI::UseTime @ 0x004D6E30`: the
|
||||
`TunnelFadeOut -> WorldFadeIn` edge hides the portal viewport, shows the
|
||||
world viewport, releases cell blocking, and starts the final one-second
|
||||
WorldFadeIn state.
|
||||
- The later WorldFadeIn completion sends LoginComplete and ends logical
|
||||
teleport state. Viewport release and protocol completion are distinct.
|
||||
- `gmPaperDollUI::RedressCreature @ 0x004A3BC0` creates the private inventory
|
||||
object only when absent and a SmartBox player is available; temporary player
|
||||
absence does not delete an existing private object.
|
||||
- `gmPaperDollUI::~gmPaperDollUI @ 0x004A3590` owns final private-object
|
||||
destruction.
|
||||
- `CPhysicsObj::prepare_to_leave_visibility @ 0x00511F40`,
|
||||
`CObjectMaint::AddObjectToBeDestroyed @ 0x00508F70`, and
|
||||
`CObjectMaint::UseTime @ 0x005089B0` define retail's delayed active-object
|
||||
destruction.
|
||||
- `CPhysicsObj::set_hidden @ 0x00514C60` plays typed UnHide at intensity 1
|
||||
when materializing. The destination purple/pink effect is DAT-driven retail
|
||||
behavior, not a generic exit effect to suppress.
|
||||
|
||||
## Root causes and corrections
|
||||
|
||||
### Shared-origin retirement head-of-line blocking
|
||||
|
||||
The 25x25 old spatial window was detached as hundreds of independently metered
|
||||
operations. No intermediate subset was observable because the complete old
|
||||
generation was already quiesced, but the shared origin could not change until
|
||||
all admissions finished.
|
||||
|
||||
`GpuWorldState.DetachAllForOriginRecenter` now performs one atomic spatial
|
||||
generation swap and returns exact per-landblock teardown receipts. Deferred
|
||||
resource release remains budgeted. A destination publication waits only on an
|
||||
older receipt for that same landblock; that exact receipt may advance ahead of
|
||||
unrelated cleanup while the remaining retirement FIFO order stays unchanged.
|
||||
|
||||
### Destination scheduling and upload utilization
|
||||
|
||||
Near worker jobs, retained publication receipts, mesh uploads, and composite
|
||||
uploads could each serialize destination work behind unrelated work. The
|
||||
destination neighborhood is now ordered first at worker and publication
|
||||
boundaries. When a destination completion actually exists, ordinary
|
||||
publication is temporarily preempted; when it does not exist or failed,
|
||||
ordinary Near/Far work continues under the same budget.
|
||||
|
||||
`WorldRevealRenderResourceScheduler` owns an exact reveal-generation profile:
|
||||
|
||||
- mesh item count: 8 -> 64;
|
||||
- composite item count: 16 -> 64;
|
||||
- upload bytes: unchanged at 8 MiB;
|
||||
- array allocation, mipmap, buffer upload/allocation/copy, and new-resource
|
||||
ceilings: unchanged.
|
||||
|
||||
The profile ends at the portal/world viewport swap. The complete canonical
|
||||
render/composite/collision readiness predicate remains mandatory.
|
||||
|
||||
### Private retained-UI ownership
|
||||
|
||||
The paperdoll and creature appraisal used shared mesh data without independent
|
||||
references, so old-world retirement could evict their meshes. Both now use
|
||||
`SyntheticEntityMeshReferenceOwner`, with transactional acquisition and
|
||||
retryable release. `PaperdollFramePresenter` retains the last successful
|
||||
private object across temporary SmartBox unavailability and equivalent
|
||||
appearance refreshes; character-session reset is the explicit clear edge.
|
||||
|
||||
### ACE KnownObjects revisit
|
||||
|
||||
The active 25-second liveness teardown was correct for resource reclamation,
|
||||
but ACE may keep the GUID in `KnownObjects` and omit CreateObject when the
|
||||
client revisits that landblock. Deleting acdream's final accepted spawn
|
||||
snapshot therefore made the object permanently absent.
|
||||
|
||||
`DormantLiveEntityStore` retains only the accepted data snapshot after every
|
||||
active owner is destroyed. Landblock residency re-enters ordinary hydration
|
||||
with the retained timestamp gate. Exact F747, a newer instance generation, and
|
||||
session reset remove the dormant snapshot. Dormant objects own no physics,
|
||||
animation, script, effect, light, audio, render, collision, or per-frame work.
|
||||
This ACE adaptation remains explicit in divergence AP-69.
|
||||
|
||||
## Gates
|
||||
|
||||
- Focused portal/retirement/reveal/private-viewport/dormant tests: green.
|
||||
- Complete App Release suite: 3,813 passed / 3 skipped.
|
||||
- Release solution build: green with zero warnings.
|
||||
- Complete Release solution suite: 8,322 passed / 5 skipped.
|
||||
- User connected visual gate:
|
||||
- paperdoll remains visible;
|
||||
- portal transit is materially faster;
|
||||
- server-spawned objects remain after repeated travel.
|
||||
Loading…
Add table
Add a link
Reference in a new issue