diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8910e919..e4f61dc3 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2394,6 +2394,7 @@ public sealed class GameWindow : IDisposable _vitalsVm?.SetLocalPlayerGuid(chosen.Id); Chat.SetLocalPlayerGuid(chosen.Id); _worldState.MarkPersistent(chosen.Id); + AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = chosen.Id; // TEMP #138-B probe Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}"); _liveSession.EnterWorld(user, characterIndex: 0); @@ -8014,7 +8015,21 @@ public sealed class GameWindow : IDisposable int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f); currentLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF); } - _worldState.RelocateEntity(pe, currentLb); + // #138-B (2026-06-24): do NOT relocate the avatar while a teleport + // is in transit (PortalSpace). The controller's cell is still the + // FROZEN SOURCE until PlaceTeleportArrival materializes the + // destination, so relocating here drags the avatar — which the + // teleport's rescue/re-inject (GpuWorldState.DrainRescued) already + // placed at the destination center — back into the now-UNLOADED + // SOURCE landblock's pending bucket, where nothing recovers it + // (RelocateEntity only scans _loaded). Net: the avatar vanishes + // after teleporting out. The teleport machinery owns the avatar's + // landblock during transit; per-frame relocation resumes at + // FireLoginComplete (InWorld). Probe-confirmed: launch4 line 561 + // APPEND guid=player lb=0x0007FFFF(source dungeon) -> PENDING -> + // DRAWSET ABSENT, never recovered. + if (_playerController.State != AcDream.App.Input.PlayerState.PortalSpace) + _worldState.RelocateEntity(pe, currentLb); } // Update chase camera(s). The CameraController exposes whichever diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 2819c3c5..30db299f 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -726,6 +726,10 @@ public sealed class RetailPViewRenderer { EntitySphere(e, out var c, out float r); bool indoor = InteriorEntityPartition.IsIndoorCellId(e.ParentCellId); + // TEMP (#138-B): trace the avatar's survival through this cull. + bool isProbePlayer = AcDream.App.Streaming.EntityVanishProbe.Enabled + && AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0 + && e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid; // #118: under an interior root, outdoor-classified dynamics drew in // the outside stage (pre-clear, seal-protected) — retail draws them // via LScape::draw's per-landcell DrawSortCell, never in the @@ -734,10 +738,18 @@ public sealed class RetailPViewRenderer // seal. Indoor dynamics (incl. exit-portal straddlers, which drew // in BOTH stages) stay — this pass is retail's loop C. if (!rootIsOutdoor && !indoor) + { + if (isProbePlayer) + AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange( + $"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)"); continue; + } bool visible = indoor ? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r) : viewcone.SphereVisibleOutside(c, r); + if (isProbePlayer) + AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange( + $"cell=0x{(e.ParentCellId ?? 0):X8} indoor={indoor} rootOutdoor={rootIsOutdoor} viewcone={visible} -> {(visible ? "DRAWN" : "CULLED(viewcone)")}"); if (visible) _dynamicsScratch.Add(e); } diff --git a/src/AcDream.App/Streaming/EntityVanishProbe.cs b/src/AcDream.App/Streaming/EntityVanishProbe.cs new file mode 100644 index 00000000..a991e261 --- /dev/null +++ b/src/AcDream.App/Streaming/EntityVanishProbe.cs @@ -0,0 +1,49 @@ +using System; + +namespace AcDream.App.Streaming; + +/// +/// TEMP diagnostic (#138-B avatar-vanish). Env-gated (ACDREAM_PROBE_ENT=1) +/// trace of the persistent player entity across teleport streaming churn: +/// whether it is present in the render draw-DATA ( +/// flat view) and whether it survives the dynamics CULL +/// (RetailPViewRenderer.DrawDynamicsLast). +/// +/// The two halves answer the decisive question: is the avatar +/// "missing for a moment" because it is absent from the draw set +/// (re-injected into the pending bucket while its landblock re-streams), or +/// because it is present but culled (in the dynamics list, dropped by +/// the outside-stage / viewcone gate)? +/// +/// STRIP once #138-B is root-caused (throwaway, like the dense-town FPS +/// apparatus). Observation-only — emits no behavior change. +/// +internal static class EntityVanishProbe +{ + public static readonly bool Enabled = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENT") == "1"; + + /// Player server guid, set once at world entry so the draw-side + /// [dyn] line can single out the avatar without plumbing the guid + /// through the render stack. + public static uint PlayerGuid; + + public static void Log(string msg) + { + if (Enabled) Console.WriteLine(msg); + } + + private static string _lastPlayerDyn = ""; + + /// Emit a [dyn] player line only when the outcome string + /// changes — a stationary, drawn avatar must not spam one line per frame + /// (that console+Tee I/O would itself depress FPS and bury the signal). + /// Fires on cell change or DRAWN↔CULLED transition. + public static void LogPlayerDynOnChange(string status) + { + if (!Enabled) return; + if (status == _lastPlayerDyn) return; + _lastPlayerDyn = status; + Console.WriteLine("[dyn] player " + status); + } +} diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs index 5f67b527..6217c814 100644 --- a/src/AcDream.App/Streaming/GpuWorldState.cs +++ b/src/AcDream.App/Streaming/GpuWorldState.cs @@ -298,6 +298,7 @@ public sealed class GpuWorldState if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid)) { _persistentRescued.Add(entity); + EntityVanishProbe.Log($"[ent] RESCUE guid=0x{entity.ServerGuid:X8} from=loaded lb=0x{landblockId:X8}"); } } @@ -331,7 +332,10 @@ public sealed class GpuWorldState foreach (var entity in pendingForLb) { if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid)) + { _persistentRescued.Add(entity); + EntityVanishProbe.Log($"[ent] RESCUE guid=0x{entity.ServerGuid:X8} from=pending lb=0x{landblockId:X8}"); + } } } @@ -439,6 +443,8 @@ public sealed class GpuWorldState _entityScriptActivator?.OnCreate(entity); uint canonicalLandblockId = (landblockId & 0xFFFF0000u) | 0xFFFFu; + bool probePersistent = EntityVanishProbe.Enabled + && entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid); if (_loaded.TryGetValue(canonicalLandblockId, out var lb)) { @@ -451,6 +457,8 @@ public sealed class GpuWorldState lb.LandblockId, lb.Heightmap, newEntities); + if (probePersistent) + EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> LOADED(drawn)"); RebuildFlatView(); return; } @@ -464,6 +472,8 @@ public sealed class GpuWorldState _pendingByLandblock[canonicalLandblockId] = bucket; } bucket.Add(entity); + if (probePersistent) + EntityVanishProbe.Log($"[ent] APPEND guid=0x{entity.ServerGuid:X8} lb=0x{canonicalLandblockId:X8} -> PENDING(hidden)"); } /// @@ -570,5 +580,29 @@ public sealed class GpuWorldState private void RebuildFlatView() { _flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray(); + if (EntityVanishProbe.Enabled) ProbeFlatViewTransitions(); + } + + // TEMP (#138-B): persistent guids currently present in the drawn flat + // view, for EntityVanishProbe transition logging. Strip with the probe. + private readonly HashSet _persistentInFlatProbe = new(); + + // TEMP (#138-B): log when a persistent (player) entity enters/leaves the + // drawn flat view. Transition-gated → low volume (fires at teleport + // boundaries, not every rebuild). Strip with EntityVanishProbe. + private void ProbeFlatViewTransitions() + { + var now = new HashSet(); + foreach (var e in _flatEntities) + if (e.ServerGuid != 0 && _persistentGuids.Contains(e.ServerGuid)) + now.Add(e.ServerGuid); + foreach (var g in now) + if (!_persistentInFlatProbe.Contains(g)) + EntityVanishProbe.Log($"[ent] DRAWSET guid=0x{g:X8} -> PRESENT (flatCount={_flatEntities.Count})"); + foreach (var g in _persistentInFlatProbe) + if (!now.Contains(g)) + EntityVanishProbe.Log($"[ent] DRAWSET guid=0x{g:X8} -> ABSENT (flatCount={_flatEntities.Count})"); + _persistentInFlatProbe.Clear(); + foreach (var g in now) _persistentInFlatProbe.Add(g); } }