From 31602c6a240049614584c1470c6c84189cb4244f Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 22 Jun 2026 11:18:16 +0200 Subject: [PATCH] =?UTF-8?q?docs(teleport):=20spec=20=E2=80=94=20retail=20r?= =?UTF-8?q?esidency=20+=20fade-cover=20(refutes=20=5FdatLock=20starvation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...retail-teleport-residency-tunnel-design.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-retail-teleport-residency-tunnel-design.md diff --git a/docs/superpowers/specs/2026-06-22-retail-teleport-residency-tunnel-design.md b/docs/superpowers/specs/2026-06-22-retail-teleport-residency-tunnel-design.md new file mode 100644 index 00000000..031953d0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-retail-teleport-residency-tunnel-design.md @@ -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`.