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

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:
Erik 2026-08-07 10:07:45 +02:00
parent 3f2b2dc3ed
commit 52bdf4df71
5 changed files with 205 additions and 12 deletions

View file

@ -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,

View file

@ -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;
/// <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 _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(

View file

@ -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.</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>
public void EnsureAgreesWithRuntimeFrame(
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;
// 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 "