fix(world): #344 — a mid-teleport world-frame disagreement defers the projection instead of crashing
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
During a portal transit the two world-frame owners legitimately rebase on different edges (Runtime at TeleportAdvanced, streaming only after old-window retirement), and a spawn projection landing in that window hit the #283 invariant as an unhandled render-path throw — the crash the user hit entering a dungeon. The guard's check is unchanged; only the disagreement RESPONSE is discriminated on the canonical transit authority (RuntimeWorldTransitState.IsTeleportActive, the same field the App layer already reads for portal-in-flight): in transit -> the materializer's existing "not yet" return, parking the projection on its established retry rides (OnLandblockLoaded's re-attempt loop, whose ordering guarantees agreement on retry because the recenter coordinator adopts the new origin BEFORE unblocking new landblock loads — verified at source; plus OnPosition recovery and OnAppearance). Outside transit -> still throws: genuine corruption stays loud. The implementer explicitly ruled out riding the Runtime placement pump, which would have acknowledged-and-discarded the completion receipt and silently dropped the entity forever. Sabotage: removing the discriminator reddened the pre-existing #283 throw tests as well as the new not-in-transit test — the sabotage defeats the original contract, not merely the new coverage. Four new tests cover defer, defer-then-agree-then-succeed (projected exactly once), throw-outside-transit, and the agreeing pass-through. Clean-room suite: 11,261 passed / 6 skipped / 0 failed. #346 filed for a sixth, distinct load-sensitive allocation flake observed during the runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3f2b2dc3ed
commit
52bdf4df71
5 changed files with 205 additions and 12 deletions
|
|
@ -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
|
## #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).
|
**Filed:** 2026-08-07 (`339-fix-gate.log`, full stack).
|
||||||
|
|
||||||
During a long-distance portal into dungeon landblock `0x5A48`, entity
|
During a long-distance portal into dungeon landblock `0x5A48`, entity
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,8 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.AnimationPresenter,
|
live.AnimationPresenter,
|
||||||
live.StaticAnimationScheduler,
|
live.StaticAnimationScheduler,
|
||||||
d.WorldOrigin,
|
d.WorldOrigin,
|
||||||
d.UpdateClock);
|
d.UpdateClock,
|
||||||
|
d.Runtime.TransitOwner);
|
||||||
var originCoordinator = new LiveEntityWorldOriginCoordinator(
|
var originCoordinator = new LiveEntityWorldOriginCoordinator(
|
||||||
d.WorldOrigin,
|
d.WorldOrigin,
|
||||||
streaming,
|
streaming,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ using AcDream.Core.Plugins;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Physics;
|
using AcDream.Runtime.Physics;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -48,6 +49,14 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
|
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
|
||||||
private readonly LiveWorldOriginState _origin;
|
private readonly LiveWorldOriginState _origin;
|
||||||
private readonly IPhysicsScriptTimeSource _gameTime;
|
private readonly IPhysicsScriptTimeSource _gameTime;
|
||||||
|
/// <summary>
|
||||||
|
/// #344: Runtime's authoritative "is a portal/teleport transit currently
|
||||||
|
/// in flight" signal (the same field App's
|
||||||
|
/// <c>LocalPlayerTeleportController.IsActive</c> already exposes) — the
|
||||||
|
/// discriminator between the ordinary #283 corruption case (throw) and
|
||||||
|
/// the legitimate mid-transit ordering race (defer).
|
||||||
|
/// </summary>
|
||||||
|
private readonly RuntimeWorldTransitState _transit;
|
||||||
|
|
||||||
private int _received;
|
private int _received;
|
||||||
private int _hydrated;
|
private int _hydrated;
|
||||||
|
|
@ -79,7 +88,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
LiveEntityAnimationPresenter animationPresenter,
|
LiveEntityAnimationPresenter animationPresenter,
|
||||||
RetailStaticAnimatingObjectScheduler staticAnimations,
|
RetailStaticAnimatingObjectScheduler staticAnimations,
|
||||||
LiveWorldOriginState origin,
|
LiveWorldOriginState origin,
|
||||||
IPhysicsScriptTimeSource gameTime)
|
IPhysicsScriptTimeSource gameTime,
|
||||||
|
RuntimeWorldTransitState transit)
|
||||||
{
|
{
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
|
|
@ -101,6 +111,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
_staticAnimations = staticAnimations ?? throw new ArgumentNullException(nameof(staticAnimations));
|
_staticAnimations = staticAnimations ?? throw new ArgumentNullException(nameof(staticAnimations));
|
||||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||||
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
|
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
|
||||||
|
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSessionState()
|
public void ResetSessionState()
|
||||||
|
|
@ -164,12 +175,26 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
CreateObject.ServerPosition position = canonicalSpawn.Position.Value;
|
CreateObject.ServerPosition position = canonicalSpawn.Position.Value;
|
||||||
int lbX = (int)((position.LandblockId >> 24) & 0xFFu);
|
int lbX = (int)((position.LandblockId >> 24) & 0xFFu);
|
||||||
int lbY = (int)((position.LandblockId >> 16) & 0xFFu);
|
int lbY = (int)((position.LandblockId >> 16) & 0xFFu);
|
||||||
// #283: permanent invariant - the two world-frame owners must agree
|
// #283/#344: permanent invariant - the two world-frame owners must
|
||||||
// before their conversions can be mixed. Proven unreachable by the
|
// agree before their conversions can be mixed. Outside an in-flight
|
||||||
// 2026-08-03 probe run; this keeps it that way.
|
// portal/teleport transit a disagreement has no legitimate
|
||||||
_origin.EnsureAgreesWithRuntimeFrame(
|
// explanation and still throws loudly (proven unreachable there by
|
||||||
_runtime.Physics.WorldFrameCenterLandblockId,
|
// the 2026-08-03 probe run; this keeps it that way). DURING a
|
||||||
position.LandblockId);
|
// 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)
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeWorldFrameEnabled)
|
||||||
ProbeWorldFrameAgreement(position.LandblockId);
|
ProbeWorldFrameAgreement(position.LandblockId);
|
||||||
var worldOrigin = new Vector3(
|
var worldOrigin = new Vector3(
|
||||||
|
|
|
||||||
|
|
@ -62,20 +62,69 @@ internal sealed class LiveWorldOriginState
|
||||||
/// so no conversion can observe the gap. This is the permanent guard that
|
/// 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
|
/// keeps that true — it converts a silent 192 m-multiple misplacement into
|
||||||
/// a loud failure if a future change ever reopens the window.</para>
|
/// a loud failure if a future change ever reopens the window.</para>
|
||||||
|
///
|
||||||
|
/// <para>#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
|
||||||
|
/// <c>TeleportAdvanced</c> 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
|
||||||
|
/// <see cref="TryEnsureAgreesWithRuntimeFrame"/>.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void EnsureAgreesWithRuntimeFrame(
|
public void EnsureAgreesWithRuntimeFrame(
|
||||||
uint runtimeCenterLandblockId,
|
uint runtimeCenterLandblockId,
|
||||||
uint projectingLandblockId)
|
uint projectingLandblockId) =>
|
||||||
|
TryEnsureAgreesWithRuntimeFrame(
|
||||||
|
runtimeCenterLandblockId,
|
||||||
|
projectingLandblockId,
|
||||||
|
transitInFlight: false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #344: the discriminated form of <see cref="EnsureAgreesWithRuntimeFrame"/>.
|
||||||
|
/// The agreement check itself is UNCHANGED — the guard is correct and stays
|
||||||
|
/// correct; only the response to a genuine disagreement now depends on
|
||||||
|
/// <paramref name="transitInFlight"/>, the authoritative signal for
|
||||||
|
/// "a portal/teleport transit is currently in flight"
|
||||||
|
/// (<c>RuntimeWorldTransitState.IsTeleportActive</c>, the same field
|
||||||
|
/// <c>LocalPlayerTeleportController.IsActive</c> 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
|
||||||
|
/// <see cref="Recenter"/> catches up, not crash. Outside a transit, a
|
||||||
|
/// disagreement has no legitimate explanation and still throws exactly as
|
||||||
|
/// before.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> if the owners agree (or there is nothing yet to
|
||||||
|
/// agree on); <see langword="false"/> if they disagree while
|
||||||
|
/// <paramref name="transitInFlight"/> is <see langword="true"/> — the
|
||||||
|
/// caller's existing "not yet" outcome, so the projection parks and
|
||||||
|
/// retries on the same cadence it already uses.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// The owners disagree and <paramref name="transitInFlight"/> is
|
||||||
|
/// <see langword="false"/> — the #283 invariant failure, unchanged.
|
||||||
|
/// </exception>
|
||||||
|
public bool TryEnsureAgreesWithRuntimeFrame(
|
||||||
|
uint runtimeCenterLandblockId,
|
||||||
|
uint projectingLandblockId,
|
||||||
|
bool transitInFlight)
|
||||||
{
|
{
|
||||||
// Before either owner is established there is nothing to agree on;
|
// Before either owner is established there is nothing to agree on;
|
||||||
// the placement itself is gated separately (#284).
|
// the placement itself is gated separately (#284).
|
||||||
if (!IsKnown || runtimeCenterLandblockId == 0u)
|
if (!IsKnown || runtimeCenterLandblockId == 0u)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
int runtimeCenterX = (int)((runtimeCenterLandblockId >> 24) & 0xFFu);
|
int runtimeCenterX = (int)((runtimeCenterLandblockId >> 24) & 0xFFu);
|
||||||
int runtimeCenterY = (int)((runtimeCenterLandblockId >> 16) & 0xFFu);
|
int runtimeCenterY = (int)((runtimeCenterLandblockId >> 16) & 0xFFu);
|
||||||
if (runtimeCenterX == CenterX && runtimeCenterY == CenterY)
|
if (runtimeCenterX == CenterX && runtimeCenterY == CenterY)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
|
if (transitInFlight)
|
||||||
|
return false;
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"World-frame owners disagree: Runtime centre "
|
"World-frame owners disagree: Runtime centre "
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,99 @@ public sealed class LiveWorldOriginStateTests
|
||||||
|
|
||||||
state.EnsureAgreesWithRuntimeFrame(0xF682FFFFu, 0xF6820033u);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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<InvalidOperationException>(() =>
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue