fix(streaming): publish restore backend before exact acknowledgement #474

This commit is contained in:
Erik 2026-09-05 09:53:34 +02:00
parent 7c1dde3f3a
commit 220bda797f
8 changed files with 985 additions and 85 deletions

View file

@ -702,16 +702,19 @@ internal sealed class SessionPlayerCompositionPhase
portal.Projection.DestinationCell));
// C4 route 4b-2 (2026-08-04): the graphical remote-placement drive
// controller — route 4b-1's dormant owner, now driven by the remote
// far snap. Its service window is the graphical host's near-tier
// residency (GraphicalRemotePlacementServiceWindow's own remarks
// justify IsNearTier over IsNearTierOrPending). Shares the SAME entity
// far snap. Its service window is the graphical host's completed
// presentation prefix plus near-tier residency
// (GraphicalRemotePlacementServiceWindow's own remarks justify the
// shared fence and IsNearTier over IsNearTierOrPending). Shares the SAME entity
// lifetime, clock, and prepared-collision source as the two drive
// controllers above.
var remotePlacementDrive = new RuntimeRemotePlacementDriveController(
d.EntityObjects,
d.Runtime.Clock,
firstEntryCollision,
new GraphicalRemotePlacementServiceWindow(live.WorldState));
new GraphicalRemotePlacementServiceWindow(
live.WorldState,
streaming.IsLandblockPresentationReady));
var hydration = new LiveEntityHydrationController(
live.LiveEntities,
d.EntityObjects,

View file

@ -11,7 +11,8 @@ namespace AcDream.App.Streaming;
/// </summary>
/// <remarks>
/// <para>
/// <b>Predicate choice — <see cref="GpuWorldState.IsNearTier"/>, not
/// <b>Predicate choice — completed presentation plus
/// <see cref="GpuWorldState.IsNearTier"/>, not
/// <see cref="GpuWorldState.IsNearTierOrPending"/>.</b> The contract asks
/// whether near-tier residency is exactly co-extensive with collision
/// publication and, if not, to propose the correct predicate rather than
@ -23,19 +24,19 @@ namespace AcDream.App.Streaming;
/// collision-published. Using it here would let this guard say "go" for a
/// destination whose collision generation has not committed, reopening
/// exactly the DeferredCell park this route exists to prevent.
/// <see cref="GpuWorldState.IsNearTier"/> alone is the correct predicate:
/// <see cref="GpuWorldState.IsNearTier"/> remains the residency half of the
/// predicate; the borrowed pipeline query closes an in-place refresh window
/// where Near is still true but its exact publication prefix is incomplete:
/// </para>
/// <para>
/// <b>Promotion direction (verified by reading, not assumed):</b> the only
/// two writers of <c>_tierByLandblock[...] = LandblockStreamTier.Near</c> are
/// <c>GpuWorldState.CommitLandblockSpatialCore</c> (:969) and
/// <c>CommitEntitiesToExistingLandblockSpatialCore</c> (:1972), both called
/// from <c>LandblockPresentationPipeline.Advance</c>'s
/// <c>SpatialPresentationCommitted</c> stage — which runs strictly AFTER the
/// preceding <c>PresentationCommitted</c> stage, the one that drives
/// <c>LandblockPhysicsPublisher</c>'s staged collision/EnvCell advance to
/// completion (<c>LandblockPresentationPipeline.cs</c> ~826-935). A
/// landblock's tier cannot read Near before its collision has committed.
/// <b>Promotion direction (verified by reading, not assumed):</b> ordinary
/// publication still commits spatial presentation after the full presentation
/// suffix. A resumed post-engine collision refresh is different: it commits
/// the graphical backend before the exact restore acknowledgement and static
/// plugin suffix complete, so that acknowledgement can observe the backend it
/// requires. Near therefore proves graphical residency, but no longer proves
/// that the exact presentation prefix has completed; the borrowed pipeline
/// query supplies that second half of the service-window predicate.
/// </para>
/// <para>
/// <b>Retirement direction (verified by reading, not assumed):</b>
@ -51,27 +52,26 @@ namespace AcDream.App.Streaming;
/// retiring.
/// </para>
/// <para>
/// <b>Residual, not closed:</b> a landblock-prefix collision MUTATION that is
/// not a full retirement (a live in-place quiescence/refresh while the tier
/// stays Near) is not ruled out by this reading and is not exercised by any
/// gate this route adds. <c>RuntimeSetPositionState</c>'s private
/// <c>TryGetBlockingQuiescence</c> (:3820-3863) is Core's own check for
/// exactly this case — an active <c>CollisionPrefixQuiescence</c> entry for
/// the destination's landblock prefix, independent of tier/residency — and
/// it is what a placement can still hit even after this guard passes,
/// producing the narrow <c>DeferredCell</c> residual
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController"/>'s
/// class doc describes.
/// <b>#474 refinement:</b> a retained collision refresh can install the
/// replacement engine while exact restore receipts are pending. Its spatial
/// backend now activates at that immutable boundary, but this service window
/// remains closed until the complete presentation prefix commits. The same
/// borrowed query gates world reveal; no duplicate readiness owner exists.
/// </para>
/// </remarks>
internal sealed class GraphicalRemotePlacementServiceWindow
: IRuntimeRemotePlacementServiceWindow
{
private readonly GpuWorldState _state;
private readonly Func<uint, bool> _isPresentationReady;
internal GraphicalRemotePlacementServiceWindow(GpuWorldState state)
internal GraphicalRemotePlacementServiceWindow(
GpuWorldState state,
Func<uint, bool> isPresentationReady)
{
_state = state ?? throw new ArgumentNullException(nameof(state));
_isPresentationReady = isPresentationReady
?? throw new ArgumentNullException(nameof(isPresentationReady));
}
/// <summary>
@ -84,8 +84,13 @@ internal sealed class GraphicalRemotePlacementServiceWindow
/// <see cref="GpuWorldState"/> tier writer/reader does
/// (<c>DetachNearLayer</c>, <c>CommitLandblockSpatialCore</c>) before the
/// dictionary lookup — <see cref="GpuWorldState.IsNearTier"/> itself does
/// not canonicalize its argument.
/// not canonicalize its argument. The same canonical id is passed to the
/// retained-presentation query before the tier lookup.
/// </summary>
public bool IsWithinServiceWindow(uint landblockId) =>
_state.IsNearTier((landblockId & 0xFFFF0000u) | 0xFFFFu);
public bool IsWithinServiceWindow(uint landblockId)
{
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
return _isPresentationReady(canonical)
&& _state.IsNearTier(canonical);
}
}

View file

@ -215,6 +215,27 @@ public sealed class LandblockPresentationPipeline
internal bool MatchesState(GpuWorldState state) =>
ReferenceEquals(_state, state);
/// <summary>
/// Returns whether the exact landblock has crossed the presentation and
/// observer-atomic spatial activation boundary. Later EnvCell replay and
/// live-recovery work deliberately do not close this admission gate.
/// </summary>
internal bool IsLandblockPresentationReady(uint landblockId)
{
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
foreach (PublicationTransaction transaction in _publications.Values)
{
if ((transaction.LandblockId & 0xFFFF0000u) ==
(canonical & 0xFFFF0000u)
&& (!transaction.PresentationCommitted
|| !transaction.SpatialPresentationCommitted))
{
return false;
}
}
return true;
}
public bool IsRetirementPending(uint landblockId) =>
_retirements.IsPending(landblockId);
@ -808,6 +829,16 @@ public sealed class LandblockPresentationPipeline
}
while (!transaction.PhysicsPublication.CompletionCommitted)
{
if (transaction.PhysicsPublication.EngineMutationCommitted
&& transaction.PhysicsPublication.RuntimeMutationPending
&& !transaction.SpatialPresentationCommitted
&& !TryRun(
default,
"publication-spatial-commit",
() => CommitSpatialPresentation(transaction)))
{
return new LandblockPublicationAdvance(false, progressed);
}
int entityOperations =
transaction.PhysicsPublication.GfxCursor
< transaction.PhysicsPublication.GfxObjectIds.Length
@ -845,6 +876,15 @@ public sealed class LandblockPresentationPipeline
}
if (transaction.PhysicsPublication.RuntimeMutationPending)
{
if (transaction.PhysicsPublication.EngineMutationCommitted
&& !transaction.SpatialPresentationCommitted
&& !TryRun(
default,
"publication-spatial-commit",
() => CommitSpatialPresentation(transaction)))
{
return new LandblockPublicationAdvance(false, progressed);
}
// #418: Runtime's collision-generation activation is a
// deliberate two-poll transaction — the first
// CommitCollisionGeneration poll closes the prefix
@ -948,51 +988,7 @@ public sealed class LandblockPresentationPipeline
if (!TryRun(
default,
"publication-spatial-commit",
() =>
{
using GpuWorldState.MutationBatch mutation =
_state.BeginMutationBatch();
if (!transaction.SpatialCommitted)
{
IEnumerable<ulong>? renderIds =
transaction.Build.EnvCells?.Shells.Select(
static shell => shell.GeometryId);
IEnumerable<ulong>? ordinaryRenderIds =
transaction.Build.EnvCells?.WalkBuildingMeshDependencies;
transaction.SpatialPublication = transaction.Kind switch
{
PublicationKind.Loaded
or PublicationKind.PromoteSelfContained
or PublicationKind.Far =>
_state.CommitLandblockSpatial(
transaction.Build.Landblock,
renderIds,
transaction.Tier,
ordinaryRenderIds),
PublicationKind.PromoteExisting =>
_state.CommitEntitiesToExistingLandblockSpatial(
transaction.LandblockId,
transaction.Build.Landblock.Entities,
renderIds,
ordinaryRenderIds),
_ => throw new InvalidOperationException(
$"Unknown landblock publication kind {transaction.Kind}."),
};
transaction.SpatialCommitted = true;
}
_state.ActivateLandblockPresentation(
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
if (transaction.SpatialPublication.RequiresActivation)
{
_staticProjectionSink?.Reconcile(
transaction.Build,
transaction.SpatialPublication);
}
transaction.SpatialPresentationCommitted = true;
}))
() => CommitSpatialPresentation(transaction)))
{
return new LandblockPublicationAdvance(false, progressed);
}
@ -1049,4 +1045,49 @@ public sealed class LandblockPresentationPipeline
}
return new LandblockPublicationAdvance(true, progressed);
}
private void CommitSpatialPresentation(PublicationTransaction transaction)
{
using GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
if (!transaction.SpatialCommitted)
{
IEnumerable<ulong>? renderIds =
transaction.Build.EnvCells?.Shells.Select(
static shell => shell.GeometryId);
IEnumerable<ulong>? ordinaryRenderIds =
transaction.Build.EnvCells?.WalkBuildingMeshDependencies;
transaction.SpatialPublication = transaction.Kind switch
{
PublicationKind.Loaded
or PublicationKind.PromoteSelfContained
or PublicationKind.Far =>
_state.CommitLandblockSpatial(
transaction.Build.Landblock,
renderIds,
transaction.Tier,
ordinaryRenderIds),
PublicationKind.PromoteExisting =>
_state.CommitEntitiesToExistingLandblockSpatial(
transaction.LandblockId,
transaction.Build.Landblock.Entities,
renderIds,
ordinaryRenderIds),
_ => throw new InvalidOperationException(
$"Unknown landblock publication kind {transaction.Kind}."),
};
transaction.SpatialCommitted = true;
}
_state.ActivateLandblockPresentation(
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
if (transaction.SpatialPublication.RequiresActivation)
{
_staticProjectionSink?.Reconcile(
transaction.Build,
transaction.SpatialPublication);
}
transaction.SpatialPresentationCommitted = true;
}
}

View file

@ -218,10 +218,14 @@ public sealed class StreamingController
}
internal bool IsCollapsedToDungeon => _collapsed;
internal bool IsLandblockPresentationReady(uint landblockId) =>
_presentation.IsLandblockPresentationReady(landblockId);
/// <summary>
/// True once every in-bounds landblock in the requested Chebyshev window
/// has crossed the render-thread publication barrier. Worker completion and
/// world-state registration are not sufficient: all static GfxObj and
/// has crossed the complete presentation prefix and render-thread
/// publication barrier. Worker completion and world-state registration
/// are not sufficient: all static GfxObj and
/// EnvCell shell meshes must have completed their render-thread upload.
/// Portal-space exit uses this alongside physics residency so the world
/// cannot be revealed while its render slots are still absent.
@ -270,6 +274,8 @@ public sealed class StreamingController
continue;
uint canonical = ((uint)nx << 24) | ((uint)ny << 16) | 0xFFFFu;
if (!_presentation.IsLandblockPresentationReady(canonical))
return false;
// GpuWorldState.IsRenderReady already implies IsLoaded, and it is a
// real drawability test out here rather than a stamp: a Far-tier
// landblock carries a spawn-adapter registration with an empty mesh