acdream/docs/superpowers/specs/2026-06-22-retail-teleport-residency-tunnel-design.md
Erik 31602c6a24 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>
2026-06-22 11:18:16 +02:00

227 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 1014 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 1014 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 3070 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 1014 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** (3070 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 ~12 frames of its `BUILD` (was +1014 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`.