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

@ -1053,8 +1053,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_sessionClearInProgress = true;
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.SetPosition.ResetSession();
Physics.CollisionReports.ResetSession();
Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
foreach (RuntimeEntityRecord canonical in active)
{

View file

@ -388,6 +388,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
}
// Seal the exact SetPosition owner before the irreversible no-fail
// suffix. Any internal invariant failure therefore leaves every
// canonical body/controller owner untouched.
_physics.SetPosition.PrepareDormantLocalActivationOwnership(
candidate.Record,
candidate.Body,
candidate.PreparedActivation.Token.Placement);
// All validation is complete. The remaining stores are callback-free,
// non-allocating, and cannot fail on this single Runtime update thread.
// The controller remains RuntimeOwnedDormant; the subsequent world

View file

@ -37,6 +37,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
bool IsCollisionReportDispatching,
int CollisionPrefixQuiescenceCount,
int PendingCollisionPrefixProjectionCount,
int CollisionPrefixMutationCount,
int CommittedCollisionPrefixMutationCount,
int CollisionAdmissionCount,
int CollisionGenerationCount,
bool OwnsProductionDataCache,
@ -75,6 +77,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& !IsCollisionReportDispatching
&& CollisionPrefixQuiescenceCount == 0
&& PendingCollisionPrefixProjectionCount == 0
&& CollisionPrefixMutationCount == 0
&& CommittedCollisionPrefixMutationCount == 0
&& CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0
&& OwnsProductionDataCache;
@ -100,11 +104,13 @@ public sealed class RuntimeCollisionAdmission
internal RuntimeCollisionAdmission(
RuntimePhysicsState owner,
uint landblockId,
ulong generation)
ulong generation,
ulong previousGeneration)
{
Owner = owner;
LandblockId = landblockId;
Generation = generation;
PreviousGeneration = previousGeneration;
}
internal RuntimePhysicsState Owner { get; }
@ -112,6 +118,32 @@ public sealed class RuntimeCollisionAdmission
internal bool Completed { get; set; }
public uint LandblockId { get; }
public ulong Generation { get; }
internal ulong PreviousGeneration { get; }
}
internal enum RuntimeCollisionPrefixMutationKind : byte
{
Activation,
Demotion,
Withdrawal,
}
internal sealed class RuntimeCollisionPrefixMutation
{
internal required RuntimeCollisionPrefixMutationKind Kind { get; init; }
internal required uint LandblockId { get; init; }
internal required ulong PreviousGeneration { get; init; }
internal required ulong TargetGeneration { get; init; }
internal ulong InvalidatedGeneration { get; init; }
internal required RuntimeCollisionPrefixQuiescenceToken Quiescence
{ get; init; }
internal RuntimeCollisionAdmission? Admission { get; init; }
internal PreparedLandblockCollisionGeneration? Prepared { get; init; }
internal RuntimeCollisionPrefixMutationPermission Permission { get; set; }
internal bool EngineMutationCommitted { get; set; }
internal bool CancellationRequested { get; set; }
internal bool WasResident { get; set; }
internal bool Ready { get; set; }
}
public readonly record struct RuntimeCollisionAcknowledgement(
@ -120,11 +152,23 @@ public readonly record struct RuntimeCollisionAcknowledgement(
bool WasResident,
bool Ready);
public readonly record struct RuntimeCollisionMutationResult(
RuntimeCollisionAcknowledgement Acknowledgement,
bool Completed)
{
public uint LandblockId => Acknowledgement.LandblockId;
public ulong Generation => Acknowledgement.Generation;
public bool WasResident => Acknowledgement.WasResident;
public bool Ready => Acknowledgement.Ready;
}
public readonly record struct RuntimeCollisionGenerationCommit(
RuntimeCollisionAcknowledgement Acknowledgement,
uint[] DirtyRetainedOwnerIds)
uint[] DirtyRetainedOwnerIds,
bool EngineCommitted,
bool Completed)
{
public bool Committed => Acknowledgement.Ready;
public bool Committed => Completed;
}
public readonly record struct RuntimeCollisionGenerationCommitted(
@ -1045,6 +1089,8 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionAdmissions = new();
private readonly Dictionary<uint, PreparedLandblockCollisionGeneration>
_preparedCollisionGenerations = new();
private readonly Dictionary<uint, RuntimeCollisionPrefixMutation>
_collisionPrefixMutations = new();
private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new();
private readonly Dictionary<uint, List<PreparedLandblockCollisionGeneration>>
_collisionOwnerSubscribers = new();
@ -1175,6 +1221,9 @@ public sealed class RuntimePhysicsState : IDisposable
collisionReports.IsDispatching,
setPosition.CollisionPrefixQuiescenceCount,
setPosition.PendingQuiescenceProjectionCount,
_collisionPrefixMutations.Count,
_collisionPrefixMutations.Values.Count(
mutation => mutation.EngineMutationCommitted),
_collisionAdmissions.Count,
_collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache),
@ -1907,6 +1956,29 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRoots.Clear();
}
internal void ResetSessionPhysics()
{
EnsureNotDisposed();
// GameRuntime serializes reset after the host update loop has stopped.
// Reset may therefore run on the lifecycle/disposal thread rather than
// the retired generation's update thread. It clears every mutation
// owner before releasing affinity so the next generation can bind its
// own update thread without admitting concurrent mutation.
foreach ((_, PreparedLandblockCollisionGeneration prepared) in
_preparedCollisionGenerations)
{
prepared.Dispose();
}
_preparedCollisionGenerations.Clear();
_collisionPrefixMutations.Clear();
_collisionAdmissions.Clear();
SetPosition.ResetSession();
CollisionReports.ResetSession();
TrimCollisionOwnerJournal();
AdvanceCollisionWorldAuthority();
Volatile.Write(ref _collisionMutationThreadId, 0);
}
internal RuntimeCollisionPrefixQuiescenceToken
BeginCollisionPrefixQuiescence(
uint landblockId,
@ -1957,25 +2029,29 @@ public sealed class RuntimePhysicsState : IDisposable
successorReady);
}
internal bool CompleteCollisionPrefixQuiescence(
in RuntimeCollisionPrefixMutationPermission permission)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
return SetPosition.CompleteCollisionPrefixQuiescence(permission);
}
public RuntimeCollisionAdmission BeginCollisionAdmission(
uint landblockId)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
if (_collisionPrefixMutations.ContainsKey(canonical))
{
throw new InvalidOperationException(
$"Collision prefix 0x{canonical:X8} is still completing its previous mutation transaction.");
}
ulong currentGeneration = _collisionGenerations.TryGetValue(
canonical,
out ulong current)
? current
: 0UL;
ulong previousGeneration = currentGeneration;
AdvanceCollisionWorldAuthority();
if (_collisionAdmissions.Remove(
canonical,
out RuntimeCollisionAdmission? superseded))
{
previousGeneration = superseded.PreviousGeneration;
SetPosition.CancelCollisionGeneration(
canonical,
superseded.Generation);
@ -1986,16 +2062,13 @@ public sealed class RuntimePhysicsState : IDisposable
prepared.Dispose();
}
}
ulong generation = _collisionGenerations.TryGetValue(
canonical,
out ulong current)
? checked(current + 1UL)
: 1UL;
ulong generation = checked(currentGeneration + 1UL);
_collisionGenerations[canonical] = generation;
var admission = new RuntimeCollisionAdmission(
this,
canonical,
generation);
generation,
previousGeneration);
_collisionAdmissions[canonical] = admission;
SetPosition.BeginCollisionGeneration(canonical, generation);
return admission;
@ -2047,7 +2120,7 @@ public sealed class RuntimePhysicsState : IDisposable
/// collision world is never withdrawn. A stale receipt may dispose its
/// own staging storage but cannot invalidate a newer admission.
/// </summary>
internal void CancelCollisionGeneration(
internal bool CancelCollisionGeneration(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration? prepared = null)
{
@ -2067,6 +2140,38 @@ public sealed class RuntimePhysicsState : IDisposable
nameof(prepared));
}
if (_collisionPrefixMutations.TryGetValue(
admission.LandblockId,
out RuntimeCollisionPrefixMutation? mutation))
{
if (mutation.Kind is not RuntimeCollisionPrefixMutationKind.Activation
|| !ReferenceEquals(mutation.Admission, admission)
|| (prepared is not null
&& !ReferenceEquals(mutation.Prepared, prepared)))
{
return false;
}
if (mutation.EngineMutationCommitted)
return AdvanceCommittedActivation(mutation).Committed;
mutation.CancellationRequested = true;
bool previousReady = mutation.PreviousGeneration != 0UL
&& Engine.IsLandblockTerrainResident(
mutation.LandblockId);
bool released = mutation.PreviousGeneration == 0UL
? SetPosition.CancelCollisionPrefixQuiescenceToUnavailable(
mutation.Quiescence)
: CancelCollisionPrefixQuiescence(
mutation.Quiescence,
mutation.PreviousGeneration,
previousReady);
if (!released)
{
return false;
}
_collisionPrefixMutations.Remove(mutation.LandblockId);
}
prepared?.Dispose();
if (prepared is not null
&& _preparedCollisionGenerations.TryGetValue(
@ -2090,6 +2195,7 @@ public sealed class RuntimePhysicsState : IDisposable
AdvanceCollisionWorldAuthority();
}
TrimCollisionOwnerJournal();
return true;
}
internal void StageCollisionAssets(
@ -2261,8 +2367,29 @@ public sealed class RuntimePhysicsState : IDisposable
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureNotDisposed();
EnsureCollisionMutationThread();
ArgumentNullException.ThrowIfNull(admission);
ArgumentNullException.ThrowIfNull(prepared);
if (_collisionPrefixMutations.TryGetValue(
admission.LandblockId,
out RuntimeCollisionPrefixMutation? pending))
{
if (pending.Kind is not RuntimeCollisionPrefixMutationKind.Activation
|| !ReferenceEquals(pending.Admission, admission)
|| !ReferenceEquals(pending.Prepared, prepared))
{
throw new InvalidOperationException(
"A different collision-prefix mutation owns this landblock.");
}
if (pending.EngineMutationCommitted)
return AdvanceCommittedActivation(pending);
if (pending.CancellationRequested)
return PendingActivation(pending);
}
ValidateAdmission(admission);
ValidatePreparedGeneration(admission, prepared);
if (!admission.AssetsPrepared)
{
@ -2292,8 +2419,62 @@ public sealed class RuntimePhysicsState : IDisposable
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
Array.Empty<uint>());
Array.Empty<uint>(),
EngineCommitted: false,
Completed: false);
}
RuntimeCollisionPrefixMutation mutation;
if (!_collisionPrefixMutations.TryGetValue(
admission.LandblockId,
out mutation!))
{
RuntimeCollisionPrefixQuiescenceToken token =
BeginCollisionPrefixQuiescence(
admission.LandblockId,
admission.Generation,
includeOutdoorCells: true);
mutation = new RuntimeCollisionPrefixMutation
{
Kind = RuntimeCollisionPrefixMutationKind.Activation,
LandblockId = admission.LandblockId,
PreviousGeneration = admission.PreviousGeneration,
TargetGeneration = admission.Generation,
Quiescence = token,
Admission = admission,
Prepared = prepared,
WasResident = Engine.IsLandblockTerrainResident(
admission.LandblockId),
};
_collisionPrefixMutations.Add(admission.LandblockId, mutation);
// The first poll deliberately closes the prefix and marks its
// resident-parking pass complete, even when the prefix is empty.
// A later poll alone may consume mutation permission; affected
// residents also require their exact Withdraw acknowledgements.
}
if (!TryAcquireCollisionPrefixMutationPermission(
mutation.Quiescence,
out RuntimeCollisionPrefixMutationPermission permission))
{
return PendingActivation(mutation);
}
mutation.Permission = permission;
// Parking a live owner writes to the canonical shadow journal. The
// prepared replacement must be resealed against that exact journal
// tail before permission can be consumed.
if (!IsCollisionPrefixMutationPermissionCurrent(permission)
|| !prepared.IsOwnerMutationReconciliationCurrent
|| !prepared.IsReadyForActivation
|| HasOlderPreparedGeneration(prepared)
|| prepared.HasPendingCommittedRebase
|| prepared.HasPendingRetirement)
{
return PendingActivation(mutation);
}
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
prepared.TakeSealedReplacement();
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
@ -2313,20 +2494,70 @@ public sealed class RuntimePhysicsState : IDisposable
if (later.Sequence > prepared.Sequence)
later.EnqueueCommittedRebase(replacement);
}
admission.Completed = true;
_collisionAdmissions.Remove(admission.LandblockId);
_preparedCollisionGenerations.Remove(admission.LandblockId);
prepared.MarkCommitted();
TrimCollisionOwnerJournal();
var acknowledgement = new RuntimeCollisionAcknowledgement(
admission.LandblockId,
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
mutation.EngineMutationCommitted = true;
mutation.Ready = Engine.IsLandblockTerrainResident(
admission.LandblockId);
mutation.WasResident = mutation.Ready;
SetPosition.CommitCollisionGeneration(
acknowledgement.LandblockId,
acknowledgement.Generation,
acknowledgement.Ready);
mutation.LandblockId,
mutation.TargetGeneration,
mutation.Ready);
RuntimeCollisionGenerationCommit completed =
AdvanceCommittedActivation(mutation);
return completed;
}
private RuntimeCollisionGenerationCommit PendingActivation(
RuntimeCollisionPrefixMutation mutation) => new(
new RuntimeCollisionAcknowledgement(
mutation.LandblockId,
mutation.TargetGeneration,
mutation.WasResident,
Ready: mutation.EngineMutationCommitted && mutation.Ready),
Array.Empty<uint>(),
EngineCommitted: mutation.EngineMutationCommitted,
Completed: false);
private RuntimeCollisionGenerationCommit AdvanceCommittedActivation(
RuntimeCollisionPrefixMutation mutation)
{
if (!mutation.EngineMutationCommitted)
throw new InvalidOperationException(
"Collision activation cannot release before its engine transaction commits.");
bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
mutation.Quiescence,
mutation.TargetGeneration,
mutation.Ready);
var acknowledgement = new RuntimeCollisionAcknowledgement(
mutation.LandblockId,
mutation.TargetGeneration,
mutation.WasResident,
mutation.Ready);
if (!completed)
{
return new RuntimeCollisionGenerationCommit(
acknowledgement,
Array.Empty<uint>(),
EngineCommitted: true,
Completed: false);
}
RuntimeCollisionAdmission admission = mutation.Admission
?? throw new InvalidOperationException(
"Collision activation lost its admission owner.");
admission.Completed = true;
if (_collisionAdmissions.TryGetValue(
mutation.LandblockId,
out RuntimeCollisionAdmission? current)
&& ReferenceEquals(current, admission))
{
_collisionAdmissions.Remove(mutation.LandblockId);
}
_collisionPrefixMutations.Remove(mutation.LandblockId);
TrimCollisionOwnerJournal();
PublishCollisionGenerationCommitted(
new RuntimeCollisionGenerationCommitted(
acknowledgement.LandblockId,
@ -2334,67 +2565,179 @@ public sealed class RuntimePhysicsState : IDisposable
acknowledgement.Ready));
return new RuntimeCollisionGenerationCommit(
acknowledgement,
Array.Empty<uint>());
Array.Empty<uint>(),
EngineCommitted: true,
Completed: true);
}
public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
private static RuntimeCollisionMutationResult PendingRetirement(
RuntimeCollisionPrefixMutation mutation) => new(
new RuntimeCollisionAcknowledgement(
mutation.LandblockId,
mutation.TargetGeneration,
mutation.WasResident,
Ready: mutation.EngineMutationCommitted && mutation.Ready),
Completed: false);
private void CommitCollisionInvalidation(
RuntimeCollisionPrefixMutation mutation)
{
_collisionGenerations[mutation.LandblockId] =
mutation.TargetGeneration;
SetPosition.CancelCollisionGeneration(
mutation.LandblockId,
mutation.InvalidatedGeneration);
if (_collisionAdmissions.TryGetValue(
mutation.LandblockId,
out RuntimeCollisionAdmission? currentAdmission)
&& ReferenceEquals(currentAdmission, mutation.Admission))
{
_collisionAdmissions.Remove(mutation.LandblockId);
}
if (_preparedCollisionGenerations.TryGetValue(
mutation.LandblockId,
out PreparedLandblockCollisionGeneration? currentPrepared)
&& ReferenceEquals(currentPrepared, mutation.Prepared))
{
_preparedCollisionGenerations.Remove(mutation.LandblockId);
currentPrepared.Dispose();
}
}
public RuntimeCollisionMutationResult DemoteCollisionToTerrain(
uint landblockId)
=> AdvanceCollisionRetirementMutation(
landblockId,
RuntimeCollisionPrefixMutationKind.Demotion);
public RuntimeCollisionMutationResult WithdrawCollision(
uint landblockId)
=> AdvanceCollisionRetirementMutation(
landblockId,
RuntimeCollisionPrefixMutationKind.Withdrawal);
private RuntimeCollisionMutationResult AdvanceCollisionRetirementMutation(
uint landblockId,
RuntimeCollisionPrefixMutationKind kind)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
bool resident = Engine.IsLandblockTerrainResident(canonical);
InvalidateCollisionAdmission(canonical);
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
_suppressCollisionOwnerJournal = true;
try
{
Engine.DemoteLandblockToTerrain(canonical);
}
finally
{
_suppressCollisionOwnerJournal = suppressOwnerJournal;
}
foreach ((_, PreparedLandblockCollisionGeneration prepared) in
_preparedCollisionGenerations)
{
prepared.RecordDemotion(canonical);
}
return new RuntimeCollisionAcknowledgement(
canonical,
_collisionGenerations[canonical],
resident,
Ready: Engine.IsLandblockTerrainResident(canonical));
}
if (canonical == 0u)
throw new ArgumentOutOfRangeException(nameof(landblockId));
if (kind is RuntimeCollisionPrefixMutationKind.Activation)
throw new ArgumentOutOfRangeException(nameof(kind));
public RuntimeCollisionAcknowledgement WithdrawCollision(
uint landblockId)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
bool resident = Engine.IsLandblockTerrainResident(canonical);
InvalidateCollisionAdmission(canonical);
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
_suppressCollisionOwnerJournal = true;
try
if (!_collisionPrefixMutations.TryGetValue(
canonical,
out RuntimeCollisionPrefixMutation? mutation))
{
Engine.RemoveLandblock(canonical);
ulong currentGeneration = _collisionGenerations.TryGetValue(
canonical,
out ulong current)
? current
: 0UL;
RuntimeCollisionAdmission? admission =
_collisionAdmissions.GetValueOrDefault(canonical);
ulong previousGeneration = admission?.PreviousGeneration
?? currentGeneration;
ulong invalidatedGeneration = admission is not null
? admission.Generation
: checked(currentGeneration + 1UL);
ulong targetGeneration = checked(
Math.Max(currentGeneration, invalidatedGeneration) + 1UL);
SetPosition.BeginCollisionGeneration(
canonical,
targetGeneration);
RuntimeCollisionPrefixQuiescenceToken token =
BeginCollisionPrefixQuiescence(
canonical,
targetGeneration,
includeOutdoorCells:
kind is RuntimeCollisionPrefixMutationKind.Withdrawal);
mutation = new RuntimeCollisionPrefixMutation
{
Kind = kind,
LandblockId = canonical,
PreviousGeneration = previousGeneration,
InvalidatedGeneration = invalidatedGeneration,
TargetGeneration = targetGeneration,
Quiescence = token,
Admission = admission,
Prepared = _preparedCollisionGenerations.GetValueOrDefault(
canonical),
WasResident = Engine.IsLandblockTerrainResident(canonical),
};
_collisionPrefixMutations.Add(canonical, mutation);
}
finally
if (mutation.Kind != kind)
{
_suppressCollisionOwnerJournal = suppressOwnerJournal;
throw new InvalidOperationException(
"A different collision-prefix mutation owns this landblock.");
}
foreach ((_, PreparedLandblockCollisionGeneration prepared) in
_preparedCollisionGenerations)
if (!mutation.EngineMutationCommitted)
{
prepared.RecordWithdrawal(canonical);
if (!TryAcquireCollisionPrefixMutationPermission(
mutation.Quiescence,
out RuntimeCollisionPrefixMutationPermission permission)
|| !IsCollisionPrefixMutationPermissionCurrent(permission))
{
return PendingRetirement(mutation);
}
mutation.Permission = permission;
CommitCollisionInvalidation(mutation);
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
_suppressCollisionOwnerJournal = true;
try
{
if (kind is RuntimeCollisionPrefixMutationKind.Demotion)
Engine.DemoteLandblockToTerrain(canonical);
else
Engine.RemoveLandblock(canonical);
AdvanceCollisionWorldAuthority();
}
finally
{
_suppressCollisionOwnerJournal = suppressOwnerJournal;
}
foreach ((_, PreparedLandblockCollisionGeneration prepared) in
_preparedCollisionGenerations)
{
if (kind is RuntimeCollisionPrefixMutationKind.Demotion)
prepared.RecordDemotion(canonical);
else
prepared.RecordWithdrawal(canonical);
}
mutation.EngineMutationCommitted = true;
mutation.Ready = kind is RuntimeCollisionPrefixMutationKind.Demotion
&& Engine.IsLandblockTerrainResident(canonical);
if (mutation.Ready)
{
SetPosition.CommitCollisionGeneration(
canonical,
mutation.TargetGeneration,
ready: true);
}
}
return new RuntimeCollisionAcknowledgement(
canonical,
_collisionGenerations[canonical],
resident,
Ready: false);
bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
mutation.Quiescence,
mutation.TargetGeneration,
mutation.Ready);
if (completed)
{
_collisionPrefixMutations.Remove(canonical);
TrimCollisionOwnerJournal();
}
return new RuntimeCollisionMutationResult(
new RuntimeCollisionAcknowledgement(
canonical,
mutation.TargetGeneration,
mutation.WasResident,
mutation.Ready),
completed);
}
public void Dispose()
@ -2419,6 +2762,7 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRemotes.Clear();
_spatialProjectiles.Clear();
_spatialRoots.Clear();
_collisionPrefixMutations.Clear();
_collisionAdmissions.Clear();
_collisionGenerations.Clear();
CellCommitted = null;
@ -2570,35 +2914,6 @@ public sealed class RuntimePhysicsState : IDisposable
}
}
private void InvalidateCollisionAdmission(uint landblockId)
{
AdvanceCollisionWorldAuthority();
ulong currentGeneration = _collisionGenerations.TryGetValue(
landblockId,
out ulong current)
? current
: 0UL;
ulong invalidatedGeneration = _collisionAdmissions.TryGetValue(
landblockId,
out RuntimeCollisionAdmission? admission)
? admission.Generation
: checked(currentGeneration + 1UL);
ulong generation = checked(
Math.Max(currentGeneration, invalidatedGeneration) + 1UL);
_collisionGenerations[landblockId] = generation;
SetPosition.CancelCollisionGeneration(
landblockId,
invalidatedGeneration);
_collisionAdmissions.Remove(landblockId);
if (_preparedCollisionGenerations.Remove(
landblockId,
out PreparedLandblockCollisionGeneration? prepared))
{
prepared.Dispose();
}
TrimCollisionOwnerJournal();
}
internal bool TryPrepareSpatialRootAdmission(RuntimeEntityRecord record)
{
EnsureNotDisposed();
@ -2960,7 +3275,13 @@ public sealed class RuntimePhysicsState : IDisposable
{
return;
}
subscribers.Remove(prepared);
for (int index = 0; index < subscribers.Count; index++)
{
if (!ReferenceEquals(subscribers[index], prepared))
continue;
subscribers.RemoveAt(index);
break;
}
if (subscribers.Count == 0)
_collisionOwnerSubscribers.Remove(ownerId);
}

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;
}
}