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

14 KiB
Raw Blame History

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 SPEEDfailed 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-INOnTeleportStarted (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. AIMOnLivePositionUpdated (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. MATERIALIZEPlaceTeleportArrival 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 IsSpawnClaimUnhydratableArrivalReadiness.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.