fix(streaming): publish restore backend before exact acknowledgement #474
This commit is contained in:
parent
7c1dde3f3a
commit
220bda797f
8 changed files with 985 additions and 85 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue