docs(teleport): foundation-investigation handoff for a fresh-look session

Captures the full teleport-flow journey (Slice 1 kept, Slice 2 hold built +
reverted), the root-cause findings (the IsLandblockLoaded key bug + the real
foundation problem: destination doesn't stream fast/complete during teleport,
likely _datLock starvation from the CreateObject flood), the open foundation
issues (#138 slow/incomplete streaming, lost-collision-after-teleport, FPS
leak Work-C, the PortalSpace freeze-vs-run-through question), and the clean
baseline (dd2eb8b). Written as evidence + open questions, not conclusions, so
a fresh session can re-examine the whole approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 10:00:23 +02:00
parent dd2eb8b39d
commit f8cda7e86d

View file

@ -0,0 +1,176 @@
# Handoff — teleport foundation (fresh-look investigation) — 2026-06-21
**Branch:** `claude/thirsty-goldberg-51bb9b` — committed, NOT merged.
**Clean baseline commit:** `dd2eb8b` (the Slice 2 revert). Build + tests green here.
**Account for repro:** `notan` / `MittSnus81!``+Je`. The character is currently left at a far
outdoor town (landblock ~125,100 = `0x7D64`); portal via the Town Network hub (`0x0007`, an indoor cell)
to reach far towns.
> **Read this with fresh eyes.** The previous session built a "hold the player until the destination
> loads" teleport feature, hit a wall, and reverted it. This doc gives you the **evidence and the open
> questions** — NOT a prescription. You are explicitly invited to question every assumption below,
> including whether a "hold" is the right model at all. Investigate first (read-only), report, then
> brainstorm the fix with the user. **No band-aids** (the user's words: "feels shaky and bandaid").
---
## TL;DR
Teleporting in acdream feels bad: **long transitions, the character STOPS at the portal instead of
running through, FPS sags after portaling, and a building lost its collision after a death→lifestone
teleport.** The previous session traced the "long transition" to a real root: **the destination
landblock does not stream in fast (or completely) during/around a teleport** (the #138 streaming gap),
likely amplified by **`_datLock` contention** from ACE's CreateObject flood on arrival. A "hold until
loaded" gate just made every teleport wait ~10 s for a load that wasn't happening — so it was reverted.
**The real work is the foundation: make teleport-destination streaming fast AND complete, and fix the
post-teleport FPS leak.** Then (and only then) a retail-style visual cover is polish, not a crutch.
---
## What was built, and what was reverted
The original goal was **Work item B: the retail teleport flow** (fade → portal-tunnel → hold-until-loaded
→ place; covers login/logout/death/portal). Design + plan are committed and still valid as *aspiration*:
- Spec: `docs/superpowers/specs/2026-06-21-retail-teleport-flow-design.md`
- Plan: `docs/superpowers/plans/2026-06-21-retail-teleport-flow.md` (22 TDD tasks, 5 slices)
- Prior research: `docs/research/2026-06-21-teleport-issues-handoff.md` (the TAS state-machine decomp).
Shipped, then status:
- **Slice 1 — `TeleportAnimSequencer`** (pure 7-state TAS machine, `src/AcDream.Core/World/`): **KEPT.**
Dormant, unwired, fully unit-tested (29 tests). Harmless. Reusable later.
- **Slice 2 — the readiness-gate hold** (outdoor teleport holds until the physics landblock is loaded):
**REVERTED** (`dd2eb8b`, reverting `ad8c24e..c880973` back to `00ef47e`). Outdoor teleports now place
**immediately** again (fast). See "Why reverted" below.
- **Slices 35 (the TAS animation/controller, login/logout/death unification): PARKED** (never started).
### Why Slice 2 was reverted (the key learning)
Slice 2 gated outdoor arrival on "is the destination physics landblock resident?" and held otherwise.
Live testing showed **every outdoor teleport froze ~10 s then force-placed**, because the destination
landblock **never became resident during the hold** (`lbs=0` the whole time). The hold was band-aiding a
foundation problem (slow/incomplete streaming) instead of fixing it — and it didn't even prevent the
#145 edge cascade it was designed for (it force-snapped onto NO-LANDBLOCK after the timeout regardless).
---
## Root-cause findings so far (verify, don't trust)
A 3-agent read-only investigation (workflow `wf_8b67a9d1-35c`, all high-confidence) + manual
source-verification found **two distinct problems** behind the 10 s freeze:
### 1. A real (now-reverted) bug: `IsLandblockLoaded` key mismatch
The Slice 2 gate queried `IsLandblockLoaded(destCell & 0xFFFF0000)` (e.g. `0x7D640000`), but streaming
stores landblocks under the `EncodeLandblockId` form — low 16 bits = `0xFFFF` (e.g. `0x7D64FFFF`)
(`src/AcDream.App/Streaming/StreamingRegion.cs:98-99`). A raw `ContainsKey` never matched → the gate was
permanently `NotReady`. This was **fixed** (`c880973`, normalize the key + the regression test the
original missed) then **reverted along with the rest of Slice 2**. *If a future hold is reintroduced, the
predicate must normalize the key (the fix is preserved in history at `c880973`).*
### 2. The actual foundation problem: the destination doesn't stream in fast/complete
Even with the key bug fixed, `lbs=0` (zero landblocks resident) for the entire hold — the destination
genuinely was not loaded. Evidence + leads:
- The streaming machinery is *commanded* correctly: on a dungeon-OUT teleport the gate logs
`streaming: dungeon EXIT-expand -> (Lx,Ly)` and the observer is correctly pinned to the destination
during the hold (`src/AcDream.App/Rendering/GameWindow.cs` ~7441-7459 PortalSpace observer-pin;
`DungeonStreamingGate.cs:44-45` returns `InsideDungeon=false` for a teleport hold;
`StreamingController.cs` ~130-163 Tick dispatch, ~279-297 `ExitDungeonExpand`, ~161-163 `DrainAndApply`
runs unconditionally). So the *command* path is not obviously gated on `InWorld`.
- **Yet nothing loads during the hold, and the landblock appears the instant the player flips to
`InWorld`.** The leading hypothesis (MEDIUM confidence — NOT proven) is **`_datLock` starvation**: ACE
floods CreateObjects on arrival; `WorldSession.Tick` drains them in a tight loop
(`src/AcDream.Core.Net/WorldSession.cs` ~598-607, no yield), each under `lock(_datLock)`
(`GameWindow.cs` ~2679-2682), and the streaming worker also needs `_datLock` to build a landblock
(`BuildLandblockForStreaming` — confirm the exact site; the investigation cited a `_datLock` acquire on
the build path). So the render-thread CreateObject flood may block the background streamer for the whole
arrival window. **This is the #1 thing to confirm or refute.**
- `AddLandblock` registers terrain + cells + portals **atomically** (`PhysicsEngine.cs` ~66-74), and the
spatial lookups (`SampleTerrainZ`, `TryGetLandblockContext`) find landblocks by world-XY **bounds**, not
by dictionary key — which is why a force-placed player grounds correctly the instant the LB appears.
### 3. The lost-collision symptom (death→lifestone → building has no collision)
Not yet root-caused. Hypothesis: the destination loaded **incompletely** — terrain present but the
building/cell **collision** (`ShadowObjects` / the per-cell shadow list) not registered. See
`memory/feedback_retail_per_cell_shadow_list.md` (retail collision = per-cell list with portal-aware
registration). Likely the same root as #2 (incomplete async load), but **verify independently** — it may
be a distinct registration-order bug.
### 4. FPS sag after portaling (Work item C)
Separate known issue (a cumulative leak). Candidates from the prior research: streaming/entity state not
freed on teleport round-trips, `_lastSpawnByGuid` / `GpuWorldState` accumulation, dungeon collapse/expand
not cleaning up. The `_datLock` contention also contributes to per-frame hitching during the flood.
**Note:** `ACDREAM_PROBE_RESOLVE=1` is itself a massive FPS killer (it `Console.WriteLine`s every physics
resolve — a 561k-line log). Do NOT measure FPS with it on; it confounds everything.
---
## The investigation to run (the new session's job)
Root-cause, read-only, then report + brainstorm. Core questions:
1. **Why is the teleport destination not resident fast?** Confirm/refute the `_datLock` starvation
hypothesis: instrument (lightly) the streamer worker's load completions and the `_datLock` hold time
during an arrival, and see whether the worker is blocked by the CreateObject flood. Is the async
load→apply pipeline gated, starved, or just slow? Does the dungeon collapse→EXIT-expand transition add
latency?
2. **Why does collision get dropped** after a teleport (the lost-building-collision)? Trace where cell /
ShadowObject collision is registered during streaming and what order vs terrain; is it skipped or
raced on a teleport arrival?
3. **What leaks** (Work item C) across teleport round-trips? Capture landblock + entity counts across N
teleports; find what grows unbounded.
4. **What does retail actually do?** The decomp (`docs/research/named-retail/`) + the prior TAS research
show retail plays a tunnel animation that *covers a synchronous-feeling load*, gated on
`position_update_complete` (the DDD interrogation). Retail's load is fast within the tunnel. So the
question isn't "hold vs no-hold" in the abstract — it's **"how do we make the load fast + complete,"**
after which either a short hold-with-cover OR place-immediately becomes viable.
## Open DESIGN questions to reconsider with fresh eyes (don't assume the prior answers)
- **Hold vs place-immediately.** The reverted baseline places immediately (fast, but risks pop-in /
the #145 edge cascade on a not-yet-streamed edge arrival). Retail holds behind a tunnel anim but loads
fast. Which model fits once streaming is fast? (AC2D, a reference, sends movement keys + trusts server
Z and does no client-side hold — worth weighing.)
- **The `PortalSpace` input-freeze.** Today entering a teleport freezes input (`OnTeleportStarted` sets
`PlayerState.PortalSpace`), so the **character STOPS at the portal** instead of running through. The
user explicitly wants "run through the portal." Is freezing input on portal-enter even correct? What
does retail do (keep momentum? freeze?)? This is independent of streaming and may be its own fix.
- **The #145 edge cascade.** With place-immediately restored, a teleport that arrives near a landblock
EDGE onto an unstreamed neighbour can still march the cell label + free-fall Z (the original #145
residual). The hold never actually fixed this. The real fix is fast/complete streaming so the arrival
lands on a loaded world; failing that, the cell-relative physics frame (#145 Slices 13, already
shipped) limits it.
---
## Apparatus / how to reproduce + diagnose
- **Launch** (PowerShell; client lifecycle is the USER's — launch with plain `dotnet run`, do NOT
pre-close clients; if a rebuild is locked by a running client, ASK):
```powershell
$env:ACDREAM_DAT_DIR="$env:USERPROFILE\Documents\Asheron's Call"; $env:ACDREAM_LIVE="1"
$env:ACDREAM_TEST_HOST="127.0.0.1"; $env:ACDREAM_TEST_PORT="9000"
$env:ACDREAM_TEST_USER="notan"; $env:ACDREAM_TEST_PASS="MittSnus81!"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object launch.log
```
Build green FIRST (`dotnet build src\AcDream.App\AcDream.App.csproj -c Debug`); launch uses `--no-build`.
- **Repro the bad teleport:** in-world, portal via the hub (`0x0007`) to a far town, or recall to
lifestone. Watch the `live: teleport started``teleport complete` gap and `[snap] ... NO-LANDBLOCK
(lbs=0)`.
- **Diagnostics:** `ACDREAM_PROBE_RESOLVE=1` exists but is FPS-toxic + confounding — prefer a *new, light*
probe (e.g. log streamer load-completions + `_datLock` hold-time during an arrival). The prior apparatus
logs (`launch-gate-retest*.log`, `launch-clean-noprobe*.log`) are in the worktree root but transient —
the evidence above is the durable summary.
- **It's a Debug build** — FPS is not representative; a `Release` build is far faster. Build Release if you
want a real frame-rate read.
## Clean baseline — what NOT to touch
- `dd2eb8b` is the clean baseline: outdoor teleports place immediately, Slice 1 sequencer dormant.
- Do NOT re-introduce the Slice 2 hold without fixing the streaming foundation first (that's the band-aid
we removed).
- Frozen/shipped phases per the milestones doc remain off-limits.
## Key references
- Render/physics digests: `claude-memory/project_render_pipeline_digest.md`,
`claude-memory/project_physics_collision_digest.md` (DO-NOT-RETRY tables).
- Streaming refs: `memory/reference_two_tier_streaming.md`, `memory/reference_indoor_cell_tracking.md`.
- `memory/feedback_apparatus_for_physics_bugs.md`, `memory/feedback_retail_per_cell_shadow_list.md`,
`memory/feedback_verify_subagent_claims_against_source.md` (read cited code yourself; agents fabricate).
- ISSUES: #138 (teleport-OUT EXPAND gap), #145 (edge cascade), Work item C (FPS leak) — update
`docs/ISSUES.md` with the new symptoms (lost-collision-after-teleport; char-stops-at-portal) as part of
the investigation.