fix(physics): seal collision generations before activation

This commit is contained in:
Erik 2026-07-31 15:53:05 +02:00
parent be94bc9b06
commit d94145e6b8
15 changed files with 1556 additions and 410 deletions

View file

@ -68,7 +68,7 @@ public readonly record struct RuntimeCollisionAcknowledgement(
public readonly record struct RuntimeCollisionGenerationCommit(
RuntimeCollisionAcknowledgement Acknowledgement,
uint[] DirtyDynamicOwnerIds)
uint[] DirtyRetainedOwnerIds)
{
public bool Committed => Acknowledgement.Ready;
}
@ -87,7 +87,11 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
{
private readonly RuntimePhysicsState _owner;
private readonly RuntimeCollisionAdmission _admission;
private readonly Dictionary<uint, ulong> _dynamicOwnerVersions = new();
private readonly Dictionary<uint, ulong> _retainedOwnerVersions = new();
private readonly List<uint> _retainedOwnerIds = new();
private ShadowObjectRegistry.RetainedRefloodOwnerScan? _retainedOwnerScan;
private PhysicsEngine.LandblockReplacementBuilder? _sealBuilder;
private PhysicsEngine.PreparedPhysicsEngineLandblock? _sealedReplacement;
private bool _disposed;
internal PreparedLandblockCollisionGeneration(
@ -106,9 +110,13 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
internal PhysicsEngine Engine { get; }
internal uint[] GfxObjectIds { get; private set; } = Array.Empty<uint>();
internal uint[] SetupIds { get; private set; } = Array.Empty<uint>();
internal IReadOnlyDictionary<uint, ulong> DynamicOwnerVersions =>
_dynamicOwnerVersions;
internal IReadOnlyDictionary<uint, ulong> RetainedOwnerVersions =>
_retainedOwnerVersions;
internal bool IsDisposed => _disposed;
internal bool RetainedOwnerCaptureComplete { get; private set; }
internal ulong RetainedOwnerCaptureMutationVersion { get; private set; }
internal bool IsSealed => _sealedReplacement is not null;
internal ulong SealedShadowMutationVersion { get; private set; }
internal bool Matches(
RuntimePhysicsState owner,
@ -123,26 +131,155 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
SetupIds = setupIds ?? throw new ArgumentNullException(nameof(setupIds));
}
internal void RefreshDynamicOwner(uint ownerId)
internal void RefreshRetainedOwner(uint ownerId)
{
EnsureUsable();
bool retained = Engine.ShadowObjects.RefreshDynamicOwnerFrom(
bool retained = Engine.ShadowObjects.RefreshRetainedOwnerFrom(
_owner.Engine.ShadowObjects,
ownerId,
_admission.LandblockId,
out ulong version);
if (retained)
_dynamicOwnerVersions[ownerId] = version;
_retainedOwnerVersions[ownerId] = version;
else
_dynamicOwnerVersions.Remove(ownerId);
_retainedOwnerVersions.Remove(ownerId);
}
internal uint[] FindDirtyDynamicOwners()
internal RuntimeCollisionOwnerCaptureStep AdvanceRetainedOwnerCapture()
{
EnsureUsable();
return _owner.Engine.ShadowObjects.FindDirtyDynamicOwners(
if (RetainedOwnerCaptureComplete)
{
return new RuntimeCollisionOwnerCaptureStep(
Completed: true,
Restarted: false,
HasOwner: false,
OwnerId: 0u);
}
_retainedOwnerScan ??= _owner.Engine.ShadowObjects
.CreateRetainedRefloodOwnerScan(_admission.LandblockId);
ShadowObjectRegistry.RetainedRefloodOwnerScanStep step =
_retainedOwnerScan.Advance();
if (step.Completed && !step.Stable)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionOwnerCaptureStep(
Completed: false,
Restarted: true,
HasOwner: false,
OwnerId: 0u);
}
if (step.HasOwner)
_retainedOwnerIds.Add(step.OwnerId);
if (step.Completed)
{
_retainedOwnerScan.Dispose();
_retainedOwnerScan = null;
RetainedOwnerCaptureMutationVersion = step.SourceMutationVersion;
RetainedOwnerCaptureComplete = true;
}
return new RuntimeCollisionOwnerCaptureStep(
RetainedOwnerCaptureComplete,
Restarted: false,
step.HasOwner,
step.OwnerId);
}
internal IReadOnlyList<uint> RetainedOwnerIds
{
get
{
EnsureUsable();
if (!RetainedOwnerCaptureComplete)
{
throw new InvalidOperationException(
"Retained collision-owner capture is incomplete.");
}
return _retainedOwnerIds;
}
}
internal void ResetRetainedOwnerCapture()
{
EnsureUsable();
_retainedOwnerScan?.Dispose();
_retainedOwnerScan = null;
_retainedOwnerIds.Clear();
_retainedOwnerVersions.Clear();
RetainedOwnerCaptureComplete = false;
RetainedOwnerCaptureMutationVersion = 0UL;
_sealedReplacement = null;
_sealBuilder?.Dispose();
_sealBuilder = null;
SealedShadowMutationVersion = 0UL;
}
internal RuntimeCollisionSealStep AdvanceSeal()
{
EnsureUsable();
if (!RetainedOwnerCaptureComplete)
{
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
WorkUnits: 0);
}
if (_owner.Engine.ShadowObjects.MutationVersion
!= RetainedOwnerCaptureMutationVersion)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
WorkUnits: 0);
}
if (_retainedOwnerVersions.Count != _retainedOwnerIds.Count)
{
throw new InvalidOperationException(
"Every retained collision owner must refresh before sealing.");
}
_sealBuilder ??= _owner.Engine.CreateLandblockReplacementBuilder(
Engine,
_admission.LandblockId,
_dynamicOwnerVersions);
GfxObjectIds,
SetupIds,
_retainedOwnerVersions);
int before = _sealBuilder.WorkUnits;
bool completed = _sealBuilder.Advance();
int workUnits = _sealBuilder.WorkUnits - before;
if (!completed)
{
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: false,
workUnits);
}
if (!_sealBuilder.IsStable || _sealBuilder.Prepared is null)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
workUnits);
}
_sealedReplacement = _sealBuilder.Prepared;
_sealBuilder.Dispose();
_sealBuilder = null;
SealedShadowMutationVersion =
_owner.Engine.ShadowObjects.MutationVersion;
return new RuntimeCollisionSealStep(
Completed: true,
Restarted: false,
workUnits);
}
internal PhysicsEngine.PreparedPhysicsEngineLandblock TakeSealedReplacement()
{
EnsureUsable();
return _sealedReplacement
?? throw new InvalidOperationException(
"Collision generation must be sealed before activation.");
}
internal void MarkCommitted()
@ -156,7 +293,14 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
if (_disposed)
return;
Engine.Clear();
_dynamicOwnerVersions.Clear();
_retainedOwnerScan?.Dispose();
_retainedOwnerScan = null;
_retainedOwnerIds.Clear();
_retainedOwnerVersions.Clear();
_sealBuilder?.Dispose();
_sealBuilder = null;
_sealedReplacement = null;
SealedShadowMutationVersion = 0UL;
_disposed = true;
}
@ -167,6 +311,17 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
}
}
internal readonly record struct RuntimeCollisionOwnerCaptureStep(
bool Completed,
bool Restarted,
bool HasOwner,
uint OwnerId);
internal readonly record struct RuntimeCollisionSealStep(
bool Completed,
bool Restarted,
int WorkUnits);
/// <summary>
/// Presentation-free mutable physics world for one Runtime/session owner.
/// Immutable prepared collision inputs may be supplied by a graphical or
@ -1001,6 +1156,43 @@ public sealed class RuntimePhysicsState : IDisposable
stagingEngine);
}
/// <summary>
/// Cancels only the named unpublished generation. The currently active
/// collision world is never withdrawn. A stale receipt may dispose its
/// own staging storage but cannot invalidate a newer admission.
/// </summary>
internal void CancelCollisionGeneration(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration? prepared = null)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
ArgumentNullException.ThrowIfNull(admission);
if (!ReferenceEquals(admission.Owner, this))
{
throw new ArgumentException(
"Collision admission belongs to another Runtime.",
nameof(admission));
}
if (prepared is not null && !prepared.Matches(this, admission))
{
throw new ArgumentException(
"Prepared collision generation belongs to another admission.",
nameof(prepared));
}
prepared?.Dispose();
if (_collisionAdmissions.TryGetValue(
admission.LandblockId,
out RuntimeCollisionAdmission? current)
&& ReferenceEquals(current, admission))
{
_collisionAdmissions.Remove(admission.LandblockId);
_collisionGenerations[admission.LandblockId] = checked(
admission.Generation + 1UL);
}
}
internal void StageCollisionAssets(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared,
@ -1043,18 +1235,17 @@ public sealed class RuntimePhysicsState : IDisposable
admission.AssetsPrepared = true;
}
internal uint[] CaptureCollisionDynamicOwners(
internal RuntimeCollisionOwnerCaptureStep AdvanceCollisionRetainedOwnerCapture(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
return Engine.ShadowObjects.CaptureDynamicRefloodOwnersForLandblock(
admission.LandblockId);
return prepared.AdvanceRetainedOwnerCapture();
}
internal void RefreshCollisionDynamicOwner(
internal void RefreshCollisionRetainedOwner(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared,
uint ownerId)
@ -1062,7 +1253,32 @@ public sealed class RuntimePhysicsState : IDisposable
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
prepared.RefreshDynamicOwner(ownerId);
prepared.RefreshRetainedOwner(ownerId);
}
internal void RestartCollisionRetainedOwnerCapture(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
prepared.ResetRetainedOwnerCapture();
}
internal RuntimeCollisionSealStep AdvanceCollisionGenerationSeal(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
if (!admission.AssetsPrepared)
{
throw new InvalidOperationException(
"Collision generation cannot seal before its assets are prepared.");
}
return prepared.AdvanceSeal();
}
internal RuntimeCollisionGenerationCommit CommitCollisionGeneration(
@ -1083,38 +1299,25 @@ public sealed class RuntimePhysicsState : IDisposable
"Collision generation has already completed.");
}
uint[] dirtyOwners = prepared.FindDirtyDynamicOwners();
if (dirtyOwners.Length != 0)
if (!prepared.IsSealed)
{
throw new InvalidOperationException(
"Collision generation cannot activate before sealing.");
}
if (Engine.ShadowObjects.MutationVersion
!= prepared.SealedShadowMutationVersion)
{
prepared.ResetRetainedOwnerCapture();
return new RuntimeCollisionGenerationCommit(
new RuntimeCollisionAcknowledgement(
admission.LandblockId,
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
dirtyOwners);
Array.Empty<uint>());
}
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
Engine.PrepareLandblockReplacement(
prepared.Engine,
admission.LandblockId,
prepared.GfxObjectIds,
prepared.SetupIds,
prepared.DynamicOwnerVersions);
if (!Engine.ValidateLandblockReplacement(replacement))
{
dirtyOwners = prepared.FindDirtyDynamicOwners();
return new RuntimeCollisionGenerationCommit(
new RuntimeCollisionAcknowledgement(
admission.LandblockId,
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
dirtyOwners);
}
Engine.CommitLandblockReplacement(replacement);
Engine.CommitLandblockReplacement(prepared.TakeSealedReplacement());
admission.Completed = true;
_collisionAdmissions.Remove(admission.LandblockId);
prepared.MarkCommitted();