diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 4320fbb8..9e9478c9 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -5472,6 +5472,9 @@ public sealed class GameWindow : IDisposable
EnsureTeleportArrivalController();
_pendingTeleportRot = rot;
_teleportArrival!.BeginArrival(newWorldPos, p.LandblockId);
+ AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
+ "AIM", p.LandblockId,
+ $"lb={lbX},{lbY} indoor={((p.LandblockId & 0xFFFFu) >= 0x0100u)} diffLb={differentLandblock}");
}
}
@@ -5561,6 +5564,8 @@ public sealed class GameWindow : IDisposable
dt: 1f / 60f);
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
+ AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
+ "PLACED", resolved.CellId, $"forced={forced}");
Console.WriteLine($"live: teleport complete — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
// Tell the server the client finished loading the new landblock (holtburger
@@ -5813,6 +5818,25 @@ public sealed class GameWindow : IDisposable
// case), which blocks the render thread for at most that duration.
// This is the minimum correct behavior; a future pass can reduce
// contention by pre-building render-thread work on the worker.
+ // tp-probe (2026-06-22, REMOVABLE): measure lock-WAIT (the _datLock
+ // contention signal — large only when the render thread is hammering the
+ // lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
+ // cost). Identical work in both branches; the probe branch only adds the
+ // stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
+ if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeTeleportEnabled)
+ {
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ lock (_datLock)
+ {
+ long waitedMs = sw.ElapsedMilliseconds;
+ sw.Restart();
+ var built = BuildLandblockForStreamingLocked(landblockId, kind);
+ AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
+ "BUILD", landblockId,
+ $"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={kind}");
+ return built;
+ }
+ }
lock (_datLock)
{
return BuildLandblockForStreamingLocked(landblockId, kind);
@@ -6414,6 +6438,7 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Terrain.LandblockMeshData meshData)
{
if (_terrain is null || _dats is null) return;
+ AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport("APPLY", lb.LandblockId);
// Phase A.1 hotfix: render-thread path also takes the dat lock so it
// doesn't race with BuildLandblockForStreaming on the worker thread.
diff --git a/src/AcDream.App/Streaming/LandblockStreamer.cs b/src/AcDream.App/Streaming/LandblockStreamer.cs
index ffaa6de7..f5602b12 100644
--- a/src/AcDream.App/Streaming/LandblockStreamer.cs
+++ b/src/AcDream.App/Streaming/LandblockStreamer.cs
@@ -127,6 +127,7 @@ public sealed class LandblockStreamer : IDisposable
{
if (System.Threading.Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(LandblockStreamer));
+ AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport("ENQ", landblockId, $"kind={kind}");
_inbox.Writer.TryWrite(new LandblockStreamJob.Load(landblockId, kind));
}
diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
index 06a3180c..1412249c 100644
--- a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
+++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
@@ -423,6 +423,31 @@ public static class PhysicsDiagnostics
public static bool ProbeSweptEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SWEPT") == "1";
+ ///
+ /// Teleport-foundation timing probe (2026-06-22 — REMOVABLE diagnostic).
+ /// Emits one [tp-probe] line per teleport-pipeline event with a
+ /// cross-thread monotonic timestamp ()
+ /// so the offline reader can order AIM / ENQ / BUILD / APPLY / PLACED across
+ /// the render thread and the streamer worker. Disambiguates the three
+ /// candidate roots for "destination not resident fast": apply-THROTTLE
+ /// (APPLY lands before PLACED), _datLock CONTENTION (BUILD waited=
+ /// large), and a streaming-command GATE (ENQ never fires for the dest).
+ /// Initial state from ACDREAM_PROBE_TELEPORT=1. Strip after capture.
+ ///
+ public static bool ProbeTeleportEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_TELEPORT") == "1";
+
+ ///
+ /// One [tp-probe] line. Self-guards on ,
+ /// so callers need not pre-check (the cost when off is a single bool read).
+ ///
+ public static void LogTeleport(string point, uint id, string extra = "")
+ {
+ if (!ProbeTeleportEnabled) return;
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[tp-probe] {point,-6} id=0x{id:X8} t={Environment.TickCount64} {extra}"));
+ }
+
///
/// A6.P3 issue #98 step-walk investigation (2026-05-23). When true,
/// emits one [step-walk] line at selected points in the transition
@@ -549,6 +574,7 @@ public static class PhysicsDiagnostics
ProbePlacementFailEnabled = false;
ProbeSweptEnabled = false;
ProbeStepWalkEnabled = false;
+ ProbeTeleportEnabled = false;
// Side-channel fields
LastBspHitPoly = null;