feat(runtime): atomically replace collision generations

This commit is contained in:
Erik 2026-08-01 17:33:34 +02:00
parent 99bf1751bb
commit 9b0f59bd1b
21 changed files with 2435 additions and 408 deletions

View file

@ -106,8 +106,7 @@ internal readonly record struct RuntimeCollisionPrefixMutationPermission(
RuntimeCollisionPrefixQuiescenceToken Quiescence,
ImmutableArray<RuntimePlacementProjectionToken> Withdrawals)
{
internal bool IsValid => Quiescence.IsValid
&& !Withdrawals.IsDefault;
internal bool IsValid => Quiescence.IsValid;
}
internal readonly record struct RuntimeCollisionEvaluationAuthority(
@ -373,8 +372,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
{ get; } = [];
internal bool ResidentsParked { get; set; }
internal bool PermissionIssued { get; set; }
internal bool AbortReleaseInProgress { get; set; }
internal ulong AbortRestoreGeneration { get; set; }
internal bool ReleaseInProgress { get; set; }
internal ulong ReleaseGeneration { get; set; }
internal bool ReleaseGenerationReady { get; set; }
}
private sealed class ContactCommitGuard(
@ -504,10 +504,10 @@ internal sealed class RuntimeSetPositionState : IDisposable
if (_collisionPrefixQuiescence.TryGetValue(
prefix,
out CollisionPrefixQuiescence? active)
&& active.AbortReleaseInProgress)
&& active.ReleaseInProgress)
{
throw new InvalidOperationException(
$"Collision quiescence 0x{prefix:X8}/{active.Token.OperationId} is restoring its retained generation.");
$"Collision quiescence 0x{prefix:X8}/{active.Token.OperationId} is releasing its retained residents.");
}
_collisionPrefixQuiescence.Remove(
prefix,
@ -592,7 +592,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
current.PermissionIssued = true;
permission = new RuntimeCollisionPrefixMutationPermission(
current.Token,
current.RetainedWithdrawals.ToImmutableArray());
current.RetainedWithdrawals.Count == 0
? default
: current.RetainedWithdrawals.ToImmutableArray());
return true;
}
@ -635,39 +637,93 @@ internal sealed class RuntimeSetPositionState : IDisposable
return removedBeforePark;
}
if (successorGeneration == 0UL
|| !successorReady
|| current.PendingWithdrawals.Count != 0)
if (successorGeneration == 0UL)
return false;
return AdvanceCollisionPrefixRelease(
token,
successorGeneration,
successorReady,
requireMutationPermission: false);
}
internal bool CancelCollisionPrefixQuiescenceToUnavailable(
in RuntimeCollisionPrefixQuiescenceToken token)
{
EnsureNotDisposed();
return AdvanceCollisionPrefixRelease(
token,
generation: 0UL,
ready: false,
requireMutationPermission: false);
}
internal bool ReleaseCollisionPrefixAfterMutation(
in RuntimeCollisionPrefixQuiescenceToken token,
ulong activeGeneration,
bool ready)
{
EnsureNotDisposed();
return AdvanceCollisionPrefixRelease(
token,
activeGeneration,
ready,
requireMutationPermission: true);
}
private bool AdvanceCollisionPrefixRelease(
in RuntimeCollisionPrefixQuiescenceToken token,
ulong generation,
bool ready,
bool requireMutationPermission)
{
if ((generation == 0UL && ready)
|| !TryGetCurrentQuiescence(
token,
out CollisionPrefixQuiescence? state))
{
return false;
}
if (!current.AbortReleaseInProgress)
CollisionPrefixQuiescence current = state!;
if (current.PendingWithdrawals.Count != 0)
return false;
if (requireMutationPermission
&& !current.PermissionIssued
&& !current.ReleaseInProgress)
{
current.AbortReleaseInProgress = true;
current.AbortRestoreGeneration = successorGeneration;
current.PermissionIssued = false;
RebindQuiescedDeferredOperations(
token,
successorGeneration,
ready: true);
return false;
}
else if (current.AbortRestoreGeneration != successorGeneration)
if (!current.ReleaseInProgress)
{
current.ReleaseInProgress = true;
current.ReleaseGeneration = generation;
current.ReleaseGenerationReady = ready;
current.PermissionIssued = false;
}
else if (current.ReleaseGeneration != generation
|| current.ReleaseGenerationReady != ready)
{
return false;
}
// A re-entrant/network placement may have joined the still-closed
// prefix after abort release started. Transfer every exact newcomer
// on each poll before deciding the barrier can open.
RebindQuiescedDeferredOperations(
token,
successorGeneration,
ready: true);
// prefix after release started. Transfer every exact newcomer on each
// poll before deciding the barrier can open.
if (_operations.Count != 0)
{
RebindQuiescedDeferredOperations(
token,
ready ? generation : 0UL,
ready,
releaseUnavailable: !ready);
}
if (current.PendingRestorePlacements.Count != 0
|| HasQuiescedDeferredOperations(token.LandblockPrefix))
{
return false;
}
bool removed = _collisionPrefixQuiescence.Remove(
token.LandblockPrefix);
if (removed)
@ -675,19 +731,6 @@ internal sealed class RuntimeSetPositionState : IDisposable
return removed;
}
internal bool CompleteCollisionPrefixQuiescence(
in RuntimeCollisionPrefixMutationPermission permission)
{
EnsureNotDisposed();
if (!IsCollisionPrefixMutationPermissionCurrent(permission))
return false;
bool removed = _collisionPrefixQuiescence.Remove(
permission.Quiescence.LandblockPrefix);
if (removed)
_physics.AdvanceCollisionQuiescenceAuthority();
return removed;
}
internal void BindEventStream(RuntimeEntityObjectEventStream events)
{
EnsureNotDisposed();
@ -770,6 +813,34 @@ internal sealed class RuntimeSetPositionState : IDisposable
portal,
captureMoverPreparationAuthority: true);
internal void PrepareDormantLocalActivationOwnership(
RuntimeEntityRecord record,
PhysicsBody body,
in RuntimeEntityPlacementToken token)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
if (!token.IsValid
|| record.Key != token.Entity
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|| operation.Token != token
|| operation.Stage is not RuntimeEntityPlacementStage
.AwaitingPreparation
|| !ReferenceEquals(operation.Record, record)
|| record.PhysicsBody is not null
|| !IsCurrent(operation)
|| body.InWorld
|| (body.TransientState & TransientStateFlags.Active) != 0)
{
throw new InvalidOperationException(
"Dormant local activation must bind to the exact current placement owner.");
}
operation.Body = body;
operation.DormantLocalActivation = true;
}
private RuntimeEntityPlacementToken BeginAcceptedPlacementCore(
RuntimeEntityRecord record,
ulong expectedPositionAuthorityVersion,
@ -2531,6 +2602,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
uint prefix,
bool includeOutdoorCells)
{
if (_physics.SpatialRootCount == 0)
return false;
var roots = new List<RuntimeEntityRecord>();
_physics.CopySpatialRootsTo(roots);
for (int index = 0; index < roots.Count; index++)
@ -2586,7 +2659,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
uint prefix = state.Token.LandblockPrefix;
foreach (Operation operation in _operations.Values)
{
if (operation.WakeableLostCell)
if (operation.WakeableLostCell || operation.DormantLocalActivation)
continue;
if (PlacementTouchesPrefix(operation.Command.Physics, prefix)
|| ResultTouchesPrefix(operation.Result, prefix)
@ -2703,7 +2776,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|| !_collisionPrefixQuiescence.TryGetValue(
operation.CollisionPrefix,
out CollisionPrefixQuiescence? state)
|| !state.AbortReleaseInProgress)
|| !state.ReleaseInProgress
|| !state.ReleaseGenerationReady)
{
return;
}
@ -2745,21 +2819,40 @@ internal sealed class RuntimeSetPositionState : IDisposable
private void RebindQuiescedDeferredOperations(
in RuntimeCollisionPrefixQuiescenceToken token,
ulong successorGeneration,
bool ready)
bool ready,
bool releaseUnavailable = false)
{
foreach (Operation operation in _operations.Values.ToArray())
{
bool unavailableAfterReadyCommit = ready
&& operation.CollisionQuiescenceHeld
&& operation.CollisionGeneration == 0UL
&& operation.CollisionPrefix == token.LandblockPrefix;
if (!operation.WakeableLostCell
|| operation.CollisionGeneration != token.CollisionGeneration
|| operation.CollisionPrefix != token.LandblockPrefix)
|| !operation.CollisionQuiescenceHeld
|| operation.CollisionPrefix != token.LandblockPrefix
|| (operation.CollisionGeneration
!= token.CollisionGeneration
&& !unavailableAfterReadyCommit))
{
continue;
}
UnindexDeferred(operation);
operation.CollisionGeneration = successorGeneration;
ulong reboundGeneration = releaseUnavailable
|| unavailableAfterReadyCommit
? 0UL
: successorGeneration;
operation.CollisionGeneration = reboundGeneration;
operation.CollisionGenerationReady = ready
&& successorGeneration != 0UL;
if (successorGeneration != 0UL)
&& reboundGeneration != 0UL;
if (releaseUnavailable || unavailableAfterReadyCommit)
{
operation.CollisionQuiescenceHeld = false;
operation.Stage = operation.RequiresPreparation
? RuntimeEntityPlacementStage.AwaitingPreparation
: RuntimeEntityPlacementStage.AwaitingCell;
}
if (reboundGeneration != 0UL)
IndexDeferred(operation);
else
IndexUnboundDeferred(operation);
@ -3073,26 +3166,27 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.Command.Physics,
out CollisionPrefixQuiescence? blocking))
{
if (blocking!.AbortReleaseInProgress
if (blocking!.ReleaseInProgress
&& blocking.ReleaseGenerationReady
&& operation.CollisionQuiescenceHeld
&& operation.CollisionPrefix
== blocking.Token.LandblockPrefix)
{
// The old collision generation remains active. Keep the
// admission barrier closed to new commands while this exact
// parked operation restores and its Place receipt drains.
// The selected collision generation is active. Keep the
// admission barrier closed while this exact parked operation
// restores and its Place receipt drains.
restoringQuiescence = blocking.Token;
}
else
{
UnindexDeferred(operation);
operation.CollisionPrefix = blocking.Token.LandblockPrefix;
operation.CollisionGeneration = blocking.Token.CollisionGeneration;
operation.CollisionGenerationReady = false;
operation.CollisionQuiescenceHeld = true;
operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
IndexDeferred(operation);
return;
UnindexDeferred(operation);
operation.CollisionPrefix = blocking.Token.LandblockPrefix;
operation.CollisionGeneration = blocking.Token.CollisionGeneration;
operation.CollisionGenerationReady = false;
operation.CollisionQuiescenceHeld = true;
operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld;
IndexDeferred(operation);
return;
}
}