docs(teleport): spec — retail residency + fade-cover (refutes _datLock starvation)
Live tp-probe capture refuted the handoff's _datLock-starvation hypothesis: worker BUILD is fast + uncontended (waited=0ms); the 10-14s 'long transition' is render-thread APPLY latency, and 'dropped at wrong position' is the per-frame resolve corrupting the outbound cell frame (lbX zeroed) while the player sits on an empty world. Design: priority-apply the player's dest landblock + hold-until-resident behind a retail fade cover (reuse the dormant TeleportAnimSequencer) + cell-march landblock-id hardening. Foundation-first, not a hold over slow streaming. Flood-timeslicing + 3D swirl deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8cda7e86d
commit
31602c6a24
1 changed files with 227 additions and 0 deletions
|
|
@ -0,0 +1,227 @@
|
|||
# Retail-faithful teleport — priority residency + fade cover — design (2026-06-22)
|
||||
|
||||
**Status:** approved design, pre-implementation.
|
||||
**Branch:** `claude/thirsty-goldberg-51bb9b`. **Baseline:** `dd2eb8b` (+ the uncommitted `tp-probe` diagnostic).
|
||||
**Supersedes the framing of:** `docs/research/2026-06-21-teleport-foundation-handoff.md` (the `_datLock`-starvation
|
||||
hypothesis is **refuted** below by direct measurement).
|
||||
|
||||
> The fix makes the teleport **foundation** fast and correct first, then covers the short transit with a
|
||||
> retail-style fade. This is explicitly NOT the reverted Slice-2 hold: that band-aided slow streaming;
|
||||
> this fixes the streaming-apply so the cover is polish over a fast load, not a crutch over a slow one.
|
||||
|
||||
---
|
||||
|
||||
## 1. The problem, as measured (not hypothesized)
|
||||
|
||||
A live capture (`ACDREAM_PROBE_TELEPORT=1`, four teleports as `+Je`) plus the ACE server log produced
|
||||
hard numbers that overturn the handoff's central hypothesis.
|
||||
|
||||
**The `tp-probe` timeline for one outdoor teleport (dest landblock `0xA9B4`):**
|
||||
|
||||
| Event | Timestamp (ms) | Δ from placement | Note |
|
||||
|---|---|---|---|
|
||||
| `PLACED` | 961809984 | 0 | placed the SAME tick as arrival — on an **empty** world (`lbs=0`) |
|
||||
| `BUILD 0xA9B4FFFF` | 961810156 | **+172 ms** | worker built it almost immediately; **`waited=0ms`** (no lock contention) |
|
||||
| `APPLY 0xA9B4FFFF` | 961824328 | **+14,344 ms** | render thread took **14.3 s** to apply it |
|
||||
|
||||
The second outdoor teleport (`0xC6A9`) showed the same shape: `BUILD +250 ms`, `APPLY +10.7 s`. Across the
|
||||
whole session the worst `_datLock` `waited` was **74 ms** (one login landblock); every teleport-destination
|
||||
build logged `waited=0ms`.
|
||||
|
||||
**Three distinct root causes (NOT one):**
|
||||
|
||||
1. **`_datLock` starvation — REFUTED.** The worker is never meaningfully blocked (`waited≈0`). The bottleneck
|
||||
is the render-thread **APPLY**, not the worker BUILD. `BuildLandblockForStreaming`
|
||||
(`src/AcDream.App/Rendering/GameWindow.cs`, the `lock(_datLock)` at the build site) is fast and uncontended.
|
||||
|
||||
2. **APPLY throughput — the "long transition."** A built landblock sits in the streamer outbox while the
|
||||
render thread, saturated by the synchronous unbounded CreateObject flood (`WorldSession.Tick`,
|
||||
`src/AcDream.Core.Net/WorldSession.cs:598-607`, drains the entire inbound queue per frame) and metered to
|
||||
`MaxCompletionsPerFrame = 4` (`src/AcDream.App/Streaming/StreamingController.cs:90`, applied in
|
||||
`DrainAndApply`), crawls through it. That is the 10–14 s the user feels.
|
||||
|
||||
3. **Resolve-against-empty-world — the "dropped at the wrong position."** Outdoor teleports place
|
||||
immediately on an unloaded world (`TeleportArrivalRules.Decide` returns `Ready` for outdoor;
|
||||
`src/AcDream.App/World/TeleportArrivalController.cs:134-142`), so for the whole 10–14 s window the
|
||||
per-frame swept-transition cell-march runs against nothing and emits a `result.CellId` with the
|
||||
landblock-X byte **zeroed** (`0x00B40039` instead of `0xA9B40039`). The outbound encoder trusts that cell
|
||||
id verbatim (the #107 fix, `src/AcDream.App/Rendering/GameWindow.cs:7794-7807`), so ACE receives a move
|
||||
naming the wrong landblock and rejects **every** one (`MOVEMENT SPEED` → `failed transition` in the ACE
|
||||
log). The player is desynced for the entire window.
|
||||
|
||||
4. **`PortalSpace` input-freeze — the "stops at the portal" (separate).** `OnTeleportStarted` sets
|
||||
`PlayerState.PortalSpace` (`GameWindow.cs:5582`); `PlayerMovementController.Update` early-returns a
|
||||
zero-movement result (`src/AcDream.App/Input/PlayerMovementController.cs:862-876`), freezing both local
|
||||
prediction and the outbound send. Independent of streaming.
|
||||
|
||||
**What retail actually does (verified — ACE `Player_Location.cs:679-760`):** place the position immediately,
|
||||
set `Hidden = true; IgnoreCollisions = true`, poll every 0.1 s until `CurrentLandblock.CreateWorldObjectsCompleted`,
|
||||
then clear both. No input freeze, no position hold — collision/visibility are suppressed until the
|
||||
destination is resident, then the player materializes. The retail *client* covers the same interval with the
|
||||
portal-tunnel animation while physics keeps ticking.
|
||||
|
||||
**Out of scope here (FPS):** the user observed 30–70 fps outdoors. This was a **Debug** build, which is not
|
||||
representative. The general FPS question is deferred to its own measure-in-Release pass (see §6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Target experience (approved)
|
||||
|
||||
A **retail tunnel cover**: starting a teleport plays a cover animation that hides the (now-short) load; the
|
||||
player is "in transit" — not moving in the world — and pops out at the destination once it is resident.
|
||||
Fidelity for this spec is a **fade cover** (fade-out → hold → fade-in), reusing the dormant
|
||||
`TeleportAnimSequencer`. The authentic 3D portal-swirl is deferred to a later polish pass.
|
||||
|
||||
This single model resolves all three teleport symptoms at once:
|
||||
- The transit state means **no movement resolve runs against an empty world** → the cell frame cannot corrupt
|
||||
(fixes the wrong-position desync).
|
||||
- The fade **covers** the transit → the awkward freeze-in-place is gone (fixes "stops at the portal").
|
||||
- Materialization is gated on the destination being **resident**, which the foundation fix makes fast
|
||||
(fixes the "long transition").
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture & new sequence
|
||||
|
||||
Replaces "place-immediately + `PortalSpace` freeze." Phases:
|
||||
|
||||
1. **TRANSIT-IN** — `OnTeleportStarted` (`0xF751`) drives `TeleportAnimSequencer` into fade-out → cover.
|
||||
Input and outbound movement are suppressed *for the duration of transit*, but the fade covers it.
|
||||
No movement resolve runs.
|
||||
2. **AIM** — `OnLivePositionUpdated` (destination `UpdatePosition`) drops the stale source landblock,
|
||||
recenters streaming on the destination, pre-collapses if the destination is a sealed dungeon, and begins
|
||||
the arrival hold. The destination load is enqueued by the normal streaming `Tick` (unchanged; the probe
|
||||
confirmed `ENQ` fires correctly during the hold).
|
||||
3. **PRIORITY-APPLY** — when the destination landblock's completion lands in the streamer outbox, it is
|
||||
applied immediately (out-of-band of the 4/frame budget), so the player's cell becomes resident in
|
||||
~hundreds of ms instead of 10–14 s.
|
||||
4. **READY** — the arrival readiness flips when the player's **own** destination landblock terrain (+ the
|
||||
EnvCell struct for an indoor destination) is resident. Outdoor changes from "place immediately" to
|
||||
"hold-until-terrain-resident."
|
||||
5. **MATERIALIZE** — `PlaceTeleportArrival` resolves against the now-resident destination (grounds
|
||||
correctly; never takes the `NO-LANDBLOCK` verbatim branch), seeds the body's (cell, local) frame,
|
||||
fades in, exits transit → `InWorld`, sends `LoginComplete`.
|
||||
|
||||
### Components
|
||||
|
||||
- **A — Priority-apply** *(`src/AcDream.App/Streaming/StreamingController.cs`, small/surgical)*
|
||||
`StreamingController` gains a settable "priority landblock id" (the active teleport target). In
|
||||
`DrainAndApply`, when a priority id is set, its completion is located and applied first/out-of-band so
|
||||
the player materializes as soon as the worker finishes it (already ~170 ms). The id is cleared on
|
||||
materialize. No change to the inbound network drain. Apply remains render-thread-only.
|
||||
|
||||
- **B — Arrival controller** *(`src/AcDream.App/World/TeleportArrivalController.cs` + `TeleportArrivalRules`,
|
||||
exist)* Outdoor readiness changes from unconditional `Ready` to **hold-until-terrain-resident**, using a
|
||||
**correctly-keyed** residency check (the `EncodeLandblockId` 0xFFFF form — the bug fixed at `c880973` and
|
||||
then reverted with Slice 2; do NOT reintroduce the `& 0xFFFF0000` raw-key mismatch). The controller drives
|
||||
the TAS transit and materializes on `Ready`, or loud-fails on `Impossible`/timeout. The ~10 s frame-count
|
||||
timeout is **retained as a loud safety net** (a real failure signal — worker crash / corrupt dat / OOB
|
||||
coords) but should now rarely fire because residency is fast.
|
||||
|
||||
- **C — Fade cover** *(`src/AcDream.Core/World/TeleportAnimSequencer.cs`, exists, 29 tests + a render overlay)*
|
||||
Wire the dormant 7-state TAS to drive a full-screen fade overlay during transit, using its existing
|
||||
`ComputeFadeAlpha`. The overlay is a render-layer addition (a screen-space quad whose alpha the TAS
|
||||
drives). The authentic 3D swirl is explicitly NOT built here.
|
||||
|
||||
- **D — Cell-march hardening** *(`src/AcDream.Core/Physics/` — the swept-transition cell-march)*
|
||||
The swept-transition path must **preserve the body's landblock id** (never emit `lbX=0`) when the swept
|
||||
position isn't covered by a resident landblock — return the body's seeded (last-known) cell rather than a
|
||||
partially-derived one. `ResolveCellId` (`PhysicsEngine.cs:330-351`) already preserves its fallback; the
|
||||
zeroing is in the swept `Transition`/`SpherePath` cell update, whose exact site is identified and fixed
|
||||
during implementation. Defense-in-depth: with the transit model the player never moves against an empty
|
||||
world on the normal path, but this also clips the #145 edge-arrival residual.
|
||||
|
||||
---
|
||||
|
||||
## 4. Why this is not the reverted band-aid
|
||||
|
||||
The reverted Slice-2 hold (`docs/research/2026-06-21-teleport-foundation-handoff.md`) was a band-aid because
|
||||
(a) the foundation was slow, so it waited ~10 s, and (b) it force-placed on `NO-LANDBLOCK` after the timeout.
|
||||
This design is different on both counts:
|
||||
|
||||
- **(a) The foundation is fixed.** Priority-apply makes the player's destination resident in ~hundreds of ms
|
||||
(the worker build is already ~170 ms; the only thing that was slow was the metered render-thread apply,
|
||||
which we now bypass for the player's own landblock).
|
||||
- **(b) Placement is on a real, grounded landblock.** Materialization resolves against a resident
|
||||
destination — never the `NO-LANDBLOCK` verbatim branch — so the cell frame is correct and outbound
|
||||
movement is accepted by ACE.
|
||||
|
||||
The fade is the retail cover over a now-fast load, which is exactly what the handoff said is legitimate
|
||||
"(and only then) a retail-style visual cover is polish, not a crutch."
|
||||
|
||||
---
|
||||
|
||||
## 5. Error handling
|
||||
|
||||
- **Destination never becomes resident** (worker crash / corrupt dat / OOB coords): the frame-count timeout
|
||||
fires, force-materializes, and logs loudly (it is a genuine failure, not normal flow). With component D,
|
||||
even a forced placement does not corrupt the outbound frame.
|
||||
- **Impossible indoor claim** (cell id outside `LandBlockInfo.NumCells`): the existing
|
||||
`IsSpawnClaimUnhydratable` → `ArrivalReadiness.Impossible` short-circuit is retained.
|
||||
- **Re-sent server position mid-transit**: `BeginArrival` is server-authoritative and resets the hold
|
||||
(existing behavior).
|
||||
|
||||
---
|
||||
|
||||
## 6. Scope — what this spec does NOT do
|
||||
|
||||
- **CreateObject-flood timeslicing (the deferred "#2").** Even after the player materializes onto correct
|
||||
terrain, the town's *objects* (buildings, NPCs) keep flooding in synchronously, causing post-materialization
|
||||
pop-in and an FPS sag for a few seconds. Bounding `WorldSession.Tick` per frame is a real improvement that
|
||||
also helps general FPS, but it touches the inbound-drain path and is not strictly a *teleport* bug. Filed
|
||||
as a follow-up; **measure in Release first.**
|
||||
- **The authentic 3D portal-swirl** (fade cover ships now).
|
||||
- **General outdoor FPS** (30–70 in Debug — re-measure in Release before treating it as a regression/leak;
|
||||
the AP-48 unbounded-entity-map leak is a separate, already-registered concern).
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing & acceptance
|
||||
|
||||
**Unit (per layer):**
|
||||
- Priority-apply ordering — a priority id's completion is applied before the per-frame budget is spent
|
||||
(`tests/AcDream.Core.Tests/Streaming/`).
|
||||
- Outdoor + indoor readiness decision — outdoor now holds until terrain-resident; correctly-keyed lookup
|
||||
(`tests/AcDream.App.Tests/World/` + `tests/AcDream.Core.Tests/`).
|
||||
- Cell-march landblock preservation — a swept step whose position is over no resident landblock returns the
|
||||
seeded cell (correct `lbX`), never `lbX=0` (`tests/AcDream.Core.Tests/Physics/`).
|
||||
- The `TeleportAnimSequencer` keeps its 29 existing tests green.
|
||||
|
||||
**Live acceptance (re-run the `tp-probe`):**
|
||||
- The destination-LB `APPLY` lands within ~1–2 frames of its `BUILD` (was +10–14 s).
|
||||
- The ACE log shows **no** `00B4…` / `MOVEMENT SPEED` / `failed transition` lines after a teleport.
|
||||
- Visual: a covered (faded) transition that pops out onto real, grounded terrain — no run-in-place stutter,
|
||||
no desync.
|
||||
- The death→lifestone building keeps its collision (component A makes the dat-static building's landblock
|
||||
resident before the player materializes; component D closes the cell-resolution residual).
|
||||
|
||||
**Probe lifecycle:** the `tp-probe` (`PhysicsDiagnostics.ProbeTeleportEnabled` + the 5 log points) is the
|
||||
acceptance apparatus. It stays until the fix is verified, then is removed (it is tagged `REMOVABLE`). If it
|
||||
proves durably useful, promote it instead of removing — decide at verification time.
|
||||
|
||||
---
|
||||
|
||||
## 8. Divergence register impact
|
||||
|
||||
- **Retire** the "outdoor teleport places immediately because streaming doesn't progress during a hold"
|
||||
divergence (the comment + behavior at `TeleportArrivalController.cs:134-142` / `GameWindow.cs:5519-5523`):
|
||||
the premise was measured false (streaming DOES progress; only APPLY was slow). The new transit-until-resident
|
||||
model is retail-faithful, so this retires a deviation rather than adding one.
|
||||
- **Add** a row for the **fade cover instead of the authentic 3D portal-swirl** (an approximation deviation,
|
||||
sibling to AP-49's fade-curve note). Cite `gmSmartBoxUI` tunnel rendering as the unported mechanism.
|
||||
- **Add** a row for the deferred CreateObject-flood timeslicing if post-materialization object pop-in is
|
||||
observed (it is an adaptation/stopgap until #2 lands).
|
||||
|
||||
All register edits land in the same commits as the behavior they describe.
|
||||
|
||||
---
|
||||
|
||||
## 9. Retail / reference anchors
|
||||
|
||||
- ACE teleport (place + `Hidden`/`IgnoreCollisions` + poll `CreateWorldObjectsCompleted`):
|
||||
`references/ACE/Source/ACE.Server/WorldObjects/Player_Location.cs:679-760`.
|
||||
- TAS decomp + the 7-state machine: `docs/research/2026-06-21-teleport-issues-handoff.md`,
|
||||
`src/AcDream.Core/World/TeleportAnimSequencer.cs`.
|
||||
- Outbound (cell, position) self-consistency (#107): `src/AcDream.App/Rendering/GameWindow.cs:7783-7807`.
|
||||
- Streaming threading + apply: `src/AcDream.App/Streaming/StreamingController.cs`,
|
||||
`src/AcDream.App/Streaming/LandblockStreamer.cs`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue