fix(physics): activate collision generations atomically
This commit is contained in:
parent
3e0f3b6206
commit
be94bc9b06
18 changed files with 1402 additions and 80 deletions
|
|
@ -12,6 +12,8 @@
|
|||
<InternalsVisibleTo Include="AcDream.App" />
|
||||
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||
<InternalsVisibleTo Include="AcDream.Core.Tests" />
|
||||
<InternalsVisibleTo Include="acdream-headless" />
|
||||
<InternalsVisibleTo Include="AcDream.Headless.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public sealed class RuntimeCollisionAdmission
|
|||
}
|
||||
|
||||
internal RuntimePhysicsState Owner { get; }
|
||||
internal bool AssetsAdmitted { get; set; }
|
||||
internal bool AssetsPrepared { get; set; }
|
||||
internal bool Completed { get; set; }
|
||||
public uint LandblockId { get; }
|
||||
public ulong Generation { get; }
|
||||
|
|
@ -63,7 +63,109 @@ public sealed class RuntimeCollisionAdmission
|
|||
public readonly record struct RuntimeCollisionAcknowledgement(
|
||||
uint LandblockId,
|
||||
ulong Generation,
|
||||
bool WasResident);
|
||||
bool WasResident,
|
||||
bool Ready);
|
||||
|
||||
public readonly record struct RuntimeCollisionGenerationCommit(
|
||||
RuntimeCollisionAcknowledgement Acknowledgement,
|
||||
uint[] DirtyDynamicOwnerIds)
|
||||
{
|
||||
public bool Committed => Acknowledgement.Ready;
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCollisionGenerationCommitted(
|
||||
uint LandblockId,
|
||||
ulong Generation,
|
||||
bool Ready);
|
||||
|
||||
/// <summary>
|
||||
/// One off-side collision generation. It owns a private cache, cell graph,
|
||||
/// engine, and shadow registry cloned from the previous complete generation.
|
||||
/// Hosts may populate it incrementally, but only Runtime can activate it.
|
||||
/// </summary>
|
||||
internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
||||
{
|
||||
private readonly RuntimePhysicsState _owner;
|
||||
private readonly RuntimeCollisionAdmission _admission;
|
||||
private readonly Dictionary<uint, ulong> _dynamicOwnerVersions = new();
|
||||
private bool _disposed;
|
||||
|
||||
internal PreparedLandblockCollisionGeneration(
|
||||
RuntimePhysicsState owner,
|
||||
RuntimeCollisionAdmission admission,
|
||||
PhysicsDataCache dataCache,
|
||||
PhysicsEngine engine)
|
||||
{
|
||||
_owner = owner;
|
||||
_admission = admission;
|
||||
DataCache = dataCache;
|
||||
Engine = engine;
|
||||
}
|
||||
|
||||
internal PhysicsDataCache DataCache { get; }
|
||||
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 bool IsDisposed => _disposed;
|
||||
|
||||
internal bool Matches(
|
||||
RuntimePhysicsState owner,
|
||||
RuntimeCollisionAdmission admission) =>
|
||||
ReferenceEquals(_owner, owner)
|
||||
&& ReferenceEquals(_admission, admission);
|
||||
|
||||
internal void SetAssetClosure(uint[] gfxObjectIds, uint[] setupIds)
|
||||
{
|
||||
EnsureUsable();
|
||||
GfxObjectIds = gfxObjectIds ?? throw new ArgumentNullException(nameof(gfxObjectIds));
|
||||
SetupIds = setupIds ?? throw new ArgumentNullException(nameof(setupIds));
|
||||
}
|
||||
|
||||
internal void RefreshDynamicOwner(uint ownerId)
|
||||
{
|
||||
EnsureUsable();
|
||||
bool retained = Engine.ShadowObjects.RefreshDynamicOwnerFrom(
|
||||
_owner.Engine.ShadowObjects,
|
||||
ownerId,
|
||||
_admission.LandblockId,
|
||||
out ulong version);
|
||||
if (retained)
|
||||
_dynamicOwnerVersions[ownerId] = version;
|
||||
else
|
||||
_dynamicOwnerVersions.Remove(ownerId);
|
||||
}
|
||||
|
||||
internal uint[] FindDirtyDynamicOwners()
|
||||
{
|
||||
EnsureUsable();
|
||||
return _owner.Engine.ShadowObjects.FindDirtyDynamicOwners(
|
||||
_admission.LandblockId,
|
||||
_dynamicOwnerVersions);
|
||||
}
|
||||
|
||||
internal void MarkCommitted()
|
||||
{
|
||||
EnsureUsable();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
Engine.Clear();
|
||||
_dynamicOwnerVersions.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void EnsureUsable()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(PreparedLandblockCollisionGeneration));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-free mutable physics world for one Runtime/session owner.
|
||||
|
|
@ -83,9 +185,12 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
||||
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
||||
_collisionAdmissions = new();
|
||||
private int _collisionMutationThreadId;
|
||||
private bool _disposed;
|
||||
|
||||
public event Action<RuntimePhysicsCellCommit>? CellCommitted;
|
||||
public event Action<RuntimeCollisionGenerationCommitted>?
|
||||
CollisionGenerationCommitted;
|
||||
|
||||
internal RuntimePhysicsState(
|
||||
RuntimeEntityDirectory entities,
|
||||
|
|
@ -865,6 +970,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
EnsureCollisionMutationThread();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
canonical,
|
||||
|
|
@ -880,11 +986,29 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
return admission;
|
||||
}
|
||||
|
||||
public void AdmitCollisionAssets(
|
||||
internal PreparedLandblockCollisionGeneration PrepareCollisionGeneration(
|
||||
RuntimeCollisionAdmission admission)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
PhysicsDataCache stagingCache = DataCache.CreateCollisionStagingCopy();
|
||||
PhysicsEngine stagingEngine =
|
||||
Engine.CreateCollisionStagingCopy(stagingCache);
|
||||
return new PreparedLandblockCollisionGeneration(
|
||||
this,
|
||||
admission,
|
||||
stagingCache,
|
||||
stagingEngine);
|
||||
}
|
||||
|
||||
internal void StageCollisionAssets(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared,
|
||||
RuntimeLandblockCollisionAssets assets)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
if (CanonicalLandblock(assets.LandblockId)
|
||||
!= admission.LandblockId)
|
||||
|
|
@ -898,13 +1022,13 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
throw new InvalidOperationException(
|
||||
"A completed collision admission cannot publish more assets.");
|
||||
}
|
||||
if (admission.AssetsAdmitted)
|
||||
if (admission.AssetsPrepared)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision assets were already admitted by this receipt.");
|
||||
"Collision assets were already prepared by this receipt.");
|
||||
}
|
||||
|
||||
Engine.AddLandblock(
|
||||
prepared.Engine.AddLandblock(
|
||||
admission.LandblockId,
|
||||
assets.Terrain,
|
||||
assets.CellSurfaces,
|
||||
|
|
@ -914,38 +1038,106 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
if ((assets.CurrentCellId & 0xFFFF0000u)
|
||||
== (admission.LandblockId & 0xFFFF0000u))
|
||||
{
|
||||
Engine.UpdatePlayerCurrCell(assets.CurrentCellId);
|
||||
prepared.Engine.UpdatePlayerCurrCell(assets.CurrentCellId);
|
||||
}
|
||||
admission.AssetsAdmitted = true;
|
||||
admission.AssetsPrepared = true;
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement CompleteCollisionAdmission(
|
||||
RuntimeCollisionAdmission admission)
|
||||
internal uint[] CaptureCollisionDynamicOwners(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
if (!admission.AssetsAdmitted)
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
return Engine.ShadowObjects.CaptureDynamicRefloodOwnersForLandblock(
|
||||
admission.LandblockId);
|
||||
}
|
||||
|
||||
internal void RefreshCollisionDynamicOwner(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared,
|
||||
uint ownerId)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
prepared.RefreshDynamicOwner(ownerId);
|
||||
}
|
||||
|
||||
internal RuntimeCollisionGenerationCommit CommitCollisionGeneration(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
if (!admission.AssetsPrepared)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision admission cannot complete before its assets publish.");
|
||||
"Collision generation cannot commit before its assets are prepared.");
|
||||
}
|
||||
if (admission.Completed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision admission has already completed.");
|
||||
"Collision generation has already completed.");
|
||||
}
|
||||
|
||||
uint[] dirtyOwners = prepared.FindDirtyDynamicOwners();
|
||||
if (dirtyOwners.Length != 0)
|
||||
{
|
||||
return new RuntimeCollisionGenerationCommit(
|
||||
new RuntimeCollisionAcknowledgement(
|
||||
admission.LandblockId,
|
||||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||
Ready: false),
|
||||
dirtyOwners);
|
||||
}
|
||||
|
||||
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);
|
||||
admission.Completed = true;
|
||||
_collisionAdmissions.Remove(admission.LandblockId);
|
||||
return new RuntimeCollisionAcknowledgement(
|
||||
prepared.MarkCommitted();
|
||||
var acknowledgement = new RuntimeCollisionAcknowledgement(
|
||||
admission.LandblockId,
|
||||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId));
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
|
||||
PublishCollisionGenerationCommitted(
|
||||
new RuntimeCollisionGenerationCommitted(
|
||||
acknowledgement.LandblockId,
|
||||
acknowledgement.Generation,
|
||||
acknowledgement.Ready));
|
||||
return new RuntimeCollisionGenerationCommit(
|
||||
acknowledgement,
|
||||
Array.Empty<uint>());
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
|
||||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
EnsureCollisionMutationThread();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||
InvalidateCollisionAdmission(canonical);
|
||||
|
|
@ -953,13 +1145,15 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
return new RuntimeCollisionAcknowledgement(
|
||||
canonical,
|
||||
_collisionGenerations[canonical],
|
||||
resident);
|
||||
resident,
|
||||
Ready: Engine.IsLandblockTerrainResident(canonical));
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement WithdrawCollision(
|
||||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
EnsureCollisionMutationThread();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||
InvalidateCollisionAdmission(canonical);
|
||||
|
|
@ -967,7 +1161,8 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
return new RuntimeCollisionAcknowledgement(
|
||||
canonical,
|
||||
_collisionGenerations[canonical],
|
||||
resident);
|
||||
resident,
|
||||
Ready: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -981,6 +1176,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
_collisionAdmissions.Clear();
|
||||
_collisionGenerations.Clear();
|
||||
CellCommitted = null;
|
||||
CollisionGenerationCommitted = null;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
|
|
@ -1053,6 +1249,20 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
private void EnsureNotDisposed() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
private void EnsureCollisionMutationThread()
|
||||
{
|
||||
int current = Environment.CurrentManagedThreadId;
|
||||
int owner = Interlocked.CompareExchange(
|
||||
ref _collisionMutationThreadId,
|
||||
current,
|
||||
0);
|
||||
if (owner != 0 && owner != current)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision generations must be staged and committed on one update thread.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCurrent(RuntimeEntityRecord record)
|
||||
{
|
||||
if (!Entities.IsCurrent(record))
|
||||
|
|
@ -1081,6 +1291,39 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ValidatePreparedGeneration(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prepared);
|
||||
ObjectDisposedException.ThrowIf(prepared.IsDisposed, prepared);
|
||||
if (!prepared.Matches(this, admission))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Prepared collision generation is stale or belongs to another admission.");
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishCollisionGenerationCommitted(
|
||||
RuntimeCollisionGenerationCommitted committed)
|
||||
{
|
||||
Delegate[] observers = CollisionGenerationCommitted?
|
||||
.GetInvocationList() ?? Array.Empty<Delegate>();
|
||||
foreach (Delegate observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
((Action<RuntimeCollisionGenerationCommitted>)observer)(committed);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
System.Diagnostics.Trace.TraceError(
|
||||
"Collision-generation commit observer failed after activation: {0}",
|
||||
error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCollisionAdmission(uint landblockId)
|
||||
{
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue