diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 88e52a17..5f2dd459 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -201,9 +201,32 @@ reasoned-from-source diagnoses. --- +## #346 — `PortalProjectionTests.ProjectToClipLease_ReusesPooledWorkWithoutResultArrays` is a SIXTH load-sensitive flake + +**Status:** OPEN. LOW. Allocation-count assertion, passed in isolation and on +two subsequent full runs. Same FILE as #302 but a DIFFERENT test — filed +separately per the never-conflate rule. +**Filed:** 2026-08-08, observed during #344's suite runs. + +--- + ## #344 — Mid-teleport crash: world-frame owners disagree during a long portal into a dungeon -**Status:** OPEN — HIGH, user-hit during live play 2026-08-07 evening. +**Status:** FIXED 2026-08-08 — defer-don't-crash, discriminated on the +canonical transit authority. `TryEnsureAgreesWithRuntimeFrame` defers (the +materializer's existing "not yet" outcome) when +`RuntimeWorldTransitState.IsTeleportActive`, and STILL THROWS otherwise — +the #283 invariant stays loud for genuine corruption, and the sabotage run +proved the discriminator's removal reddens the original #283 tests, not +just the new ones. The retry ride is `OnLandblockLoaded`'s re-attempt loop, +whose ordering GUARANTEES agreement on retry: the recenter coordinator +calls `Recenter` before `TryCommitOriginRecenter` unblocks new landblock +loads (verified at source at landing). Entity projected exactly once, +never dropped. Clean-room suite 11,261/6/0. + +**Original filing:** + +**Status (original):** OPEN — HIGH, user-hit during live play 2026-08-07 evening. **Filed:** 2026-08-07 (`339-fix-gate.log`, full stack). During a long-distance portal into dungeon landblock `0x5A48`, entity diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 40c12694..160fae44 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -461,7 +461,8 @@ internal sealed class SessionPlayerCompositionPhase live.AnimationPresenter, live.StaticAnimationScheduler, d.WorldOrigin, - d.UpdateClock); + d.UpdateClock, + d.Runtime.TransitOwner); var originCoordinator = new LiveEntityWorldOriginCoordinator( d.WorldOrigin, streaming, diff --git a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs index 1a78c234..665d757c 100644 --- a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs +++ b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs @@ -13,6 +13,7 @@ using AcDream.Core.Plugins; using AcDream.Core.World; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; +using AcDream.Runtime.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; @@ -48,6 +49,14 @@ internal sealed class DatLiveEntityProjectionMaterializer private readonly RetailStaticAnimatingObjectScheduler _staticAnimations; private readonly LiveWorldOriginState _origin; private readonly IPhysicsScriptTimeSource _gameTime; + /// + /// #344: Runtime's authoritative "is a portal/teleport transit currently + /// in flight" signal (the same field App's + /// LocalPlayerTeleportController.IsActive already exposes) — the + /// discriminator between the ordinary #283 corruption case (throw) and + /// the legitimate mid-transit ordering race (defer). + /// + private readonly RuntimeWorldTransitState _transit; private int _received; private int _hydrated; @@ -79,7 +88,8 @@ internal sealed class DatLiveEntityProjectionMaterializer LiveEntityAnimationPresenter animationPresenter, RetailStaticAnimatingObjectScheduler staticAnimations, LiveWorldOriginState origin, - IPhysicsScriptTimeSource gameTime) + IPhysicsScriptTimeSource gameTime, + RuntimeWorldTransitState transit) { _options = options ?? throw new ArgumentNullException(nameof(options)); _dats = dats ?? throw new ArgumentNullException(nameof(dats)); @@ -101,6 +111,7 @@ internal sealed class DatLiveEntityProjectionMaterializer _staticAnimations = staticAnimations ?? throw new ArgumentNullException(nameof(staticAnimations)); _origin = origin ?? throw new ArgumentNullException(nameof(origin)); _gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime)); + _transit = transit ?? throw new ArgumentNullException(nameof(transit)); } public void ResetSessionState() @@ -164,12 +175,26 @@ internal sealed class DatLiveEntityProjectionMaterializer CreateObject.ServerPosition position = canonicalSpawn.Position.Value; int lbX = (int)((position.LandblockId >> 24) & 0xFFu); int lbY = (int)((position.LandblockId >> 16) & 0xFFu); - // #283: permanent invariant - the two world-frame owners must agree - // before their conversions can be mixed. Proven unreachable by the - // 2026-08-03 probe run; this keeps it that way. - _origin.EnsureAgreesWithRuntimeFrame( - _runtime.Physics.WorldFrameCenterLandblockId, - position.LandblockId); + // #283/#344: permanent invariant - the two world-frame owners must + // agree before their conversions can be mixed. Outside an in-flight + // portal/teleport transit a disagreement has no legitimate + // explanation and still throws loudly (proven unreachable there by + // the 2026-08-03 probe run; this keeps it that way). DURING a + // transit, Runtime can legitimately rebase to the destination before + // this owner observes old-window retirement + // (LiveWorldOriginState.EnsureAgreesWithRuntimeFrame's doc comment on + // the two rebase edges) - that is the #344 ordering race, not + // corruption, so this is the existing "not yet" outcome: the + // projection parks here and the caller's existing spatial-recovery + // retry re-attempts it once streaming recentres and the origins + // agree again. + if (!_origin.TryEnsureAgreesWithRuntimeFrame( + _runtime.Physics.WorldFrameCenterLandblockId, + position.LandblockId, + transitInFlight: _transit.IsTeleportActive)) + { + return false; + } if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeWorldFrameEnabled) ProbeWorldFrameAgreement(position.LandblockId); var worldOrigin = new Vector3( diff --git a/src/AcDream.App/World/LiveWorldOriginState.cs b/src/AcDream.App/World/LiveWorldOriginState.cs index e74290e7..7a36b2fa 100644 --- a/src/AcDream.App/World/LiveWorldOriginState.cs +++ b/src/AcDream.App/World/LiveWorldOriginState.cs @@ -62,20 +62,69 @@ internal sealed class LiveWorldOriginState /// so no conversion can observe the gap. This is the permanent guard that /// keeps that true — it converts a silent 192 m-multiple misplacement into /// a loud failure if a future change ever reopens the window. + /// + /// #344 found the one window where the two rebase edges CAN + /// legitimately observe the gap: a long-distance portal, where Runtime + /// already rebased to the destination (the accepted Position's + /// TeleportAdvanced edge) while this owner is still mid-flight + /// toward the same destination (old-window retirement not yet observed). + /// That is an ordering race, not corruption, so it must not crash the + /// render thread. Always throws here — this overload keeps its original + /// unconditional-throw contract for every existing caller and test; the + /// discriminated form a caller can defer on is + /// . /// public void EnsureAgreesWithRuntimeFrame( uint runtimeCenterLandblockId, - uint projectingLandblockId) + uint projectingLandblockId) => + TryEnsureAgreesWithRuntimeFrame( + runtimeCenterLandblockId, + projectingLandblockId, + transitInFlight: false); + + /// + /// #344: the discriminated form of . + /// The agreement check itself is UNCHANGED — the guard is correct and stays + /// correct; only the response to a genuine disagreement now depends on + /// , the authoritative signal for + /// "a portal/teleport transit is currently in flight" + /// (RuntimeWorldTransitState.IsTeleportActive, the same field + /// LocalPlayerTeleportController.IsActive already exposes to the + /// App layer). While a transit is in flight, Runtime rebasing ahead of + /// this owner is the ordinary, expected shape of the race documented + /// above — the caller should park the projection and retry once + /// catches up, not crash. Outside a transit, a + /// disagreement has no legitimate explanation and still throws exactly as + /// before. + /// + /// + /// if the owners agree (or there is nothing yet to + /// agree on); if they disagree while + /// is — the + /// caller's existing "not yet" outcome, so the projection parks and + /// retries on the same cadence it already uses. + /// + /// + /// The owners disagree and is + /// — the #283 invariant failure, unchanged. + /// + public bool TryEnsureAgreesWithRuntimeFrame( + uint runtimeCenterLandblockId, + uint projectingLandblockId, + bool transitInFlight) { // Before either owner is established there is nothing to agree on; // the placement itself is gated separately (#284). if (!IsKnown || runtimeCenterLandblockId == 0u) - return; + return true; int runtimeCenterX = (int)((runtimeCenterLandblockId >> 24) & 0xFFu); int runtimeCenterY = (int)((runtimeCenterLandblockId >> 16) & 0xFFu); if (runtimeCenterX == CenterX && runtimeCenterY == CenterY) - return; + return true; + + if (transitInFlight) + return false; throw new InvalidOperationException( "World-frame owners disagree: Runtime centre " diff --git a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs index 722c0a36..0953f7c0 100644 --- a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs +++ b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs @@ -168,4 +168,99 @@ public sealed class LiveWorldOriginStateTests state.EnsureAgreesWithRuntimeFrame(0xF682FFFFu, 0xF6820033u); } + + // #344: the discriminated overload. During an in-flight portal/teleport + // transit, Runtime rebasing ahead of this owner is the ordinary shape of + // the race documented on EnsureAgreesWithRuntimeFrame's doc comment, not + // corruption — the caller gets back its existing "not yet" outcome (a + // false return, matching every other "not yet" case in + // DatLiveEntityProjectionMaterializer.TryMaterialize) instead of a thrown + // exception. Outside a transit the guard is unchanged: it still throws. + + [Fact] + public void DisagreeingWorldFrameOwners_WhileTransitInFlight_DefersInsteadOfThrowing() + { + var state = new LiveWorldOriginState(); + Assert.True(state.TryInitialize(0x09, 0x04)); + + bool agree = state.TryEnsureAgreesWithRuntimeFrame( + 0xF682FFFFu, + 0xF6820033u, + transitInFlight: true); + + Assert.False(agree); + // The disagreement did not mutate the streamed origin — only + // Recenter (driven by StreamingOriginRecenterCoordinator) may do + // that. + Assert.Equal(0x09, state.CenterX); + Assert.Equal(0x04, state.CenterY); + } + + /// + /// The full #344 round trip: a spawn arrives mid-portal (disagreement, + /// transit in flight) and parks; streaming then recentres to the same + /// destination Runtime already rebased to, and the exact same query that + /// deferred a moment ago now agrees — the caller's existing retry re- + /// attempts materialization and this is what lets it proceed. Passing + /// transitInFlight:false on the retry too shows agreement no longer + /// depends on the discriminator once the origins actually match. + /// + [Fact] + public void DisagreeingWorldFrameOwners_WhileTransitInFlight_ThenAgreeing_RetrySucceeds() + { + var state = new LiveWorldOriginState(); + Assert.True(state.TryInitialize(0x09, 0x04)); + + bool firstAttempt = state.TryEnsureAgreesWithRuntimeFrame( + 0xF682FFFFu, + 0xF6820033u, + transitInFlight: true); + Assert.False(firstAttempt); + + // Streaming recentres: old window fully retires, the streamed + // origin adopts the destination Runtime already rebased to. + state.Recenter(0xF6, 0x82); + + bool retry = state.TryEnsureAgreesWithRuntimeFrame( + 0xF682FFFFu, + 0xF6820033u, + transitInFlight: false); + Assert.True(retry); + } + + [Fact] + public void DisagreeingWorldFrameOwners_NotInTransit_StillThrowsWithTheOffsetInMetres() + { + var state = new LiveWorldOriginState(); + Assert.True(state.TryInitialize(0x09, 0x04)); + + InvalidOperationException error = + Assert.Throws(() => + state.TryEnsureAgreesWithRuntimeFrame( + 0xF682FFFFu, + 0xF6820033u, + transitInFlight: false)); + + Assert.Contains("World-frame owners disagree", error.Message); + Assert.Contains("45504m", error.Message); + Assert.Contains("0xF6820033", error.Message); + } + + [Fact] + public void AgreeingWorldFrameOwners_TryVariantPassesRegardlessOfTransitFlag() + { + var state = new LiveWorldOriginState(); + Assert.True(state.TryInitialize(0xA9, 0xB6)); + + Assert.True( + state.TryEnsureAgreesWithRuntimeFrame( + 0xA9B6FFFFu, + 0xA9B60001u, + transitInFlight: true)); + Assert.True( + state.TryEnsureAgreesWithRuntimeFrame( + 0xA9B6FFFFu, + 0xA9B60001u, + transitInFlight: false)); + } }