fix(physics): activate collision generations atomically

This commit is contained in:
Erik 2026-07-31 15:19:25 +02:00
parent 3e0f3b6206
commit be94bc9b06
18 changed files with 1402 additions and 80 deletions

View file

@ -311,27 +311,28 @@ public sealed class RuntimePhysicsStateTests
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
Assert.Throws<InvalidOperationException>(() =>
first.Physics.AdmitCollisionAssets(
admission,
CollisionAssets(0xA9B4FFFFu)));
first.Physics.PrepareCollisionGeneration(admission));
Assert.Throws<InvalidOperationException>(() =>
second.Physics.AdmitCollisionAssets(
newer,
CollisionAssets(0xA9B4FFFFu)));
second.Physics.PrepareCollisionGeneration(newer));
first.Physics.AdmitCollisionAssets(
using PreparedLandblockCollisionGeneration prepared =
first.Physics.PrepareCollisionGeneration(newer);
first.Physics.StageCollisionAssets(
newer,
prepared,
CollisionAssets(0xA9B4FFFFu));
RuntimeCollisionAcknowledgement completed =
first.Physics.CompleteCollisionAdmission(newer);
RuntimeCollisionGenerationCommit commit =
first.Physics.CommitCollisionGeneration(newer, prepared);
RuntimeCollisionAcknowledgement completed = commit.Acknowledgement;
Assert.True(commit.Committed);
Assert.True(completed.WasResident);
Assert.Equal(1, first.Physics.Engine.LandblockCount);
Assert.Equal(
0,
first.Physics.CaptureOwnership().CollisionAdmissionCount);
Assert.Throws<InvalidOperationException>(() =>
first.Physics.CompleteCollisionAdmission(newer));
first.Physics.CommitCollisionGeneration(newer, prepared));
RuntimeCollisionAcknowledgement withdrawn =
first.Physics.WithdrawCollision(0xA9B4FFFFu);
@ -340,6 +341,193 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, first.Physics.Engine.LandblockCount);
}
[Fact]
public void CollisionGenerationKeepsPreviousWorldVisibleUntilOneCommitNotification()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
RuntimeCollisionAdmission firstAdmission =
physics.BeginCollisionAdmission(0xA9B4FFFFu);
using (PreparedLandblockCollisionGeneration first =
physics.PrepareCollisionGeneration(firstAdmission))
{
physics.StageCollisionAssets(
firstAdmission,
first,
CollisionAssets(0xA9B4FFFFu, terrainHeight: 10f));
Assert.True(physics.CommitCollisionGeneration(
firstAdmission,
first).Committed);
}
int notifications = 0;
physics.CollisionGenerationCommitted += _ => notifications++;
RuntimeCollisionAdmission replacementAdmission =
physics.BeginCollisionAdmission(0xA9B4FFFFu);
using PreparedLandblockCollisionGeneration replacement =
physics.PrepareCollisionGeneration(replacementAdmission);
physics.StageCollisionAssets(
replacementAdmission,
replacement,
CollisionAssets(0xA9B4FFFFu, terrainHeight: 25f));
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
Assert.Equal(0, notifications);
RuntimeCollisionGenerationCommit committed =
physics.CommitCollisionGeneration(
replacementAdmission,
replacement);
Assert.True(committed.Committed);
Assert.Equal(25f, physics.Engine.SampleTerrainZ(1f, 1f));
Assert.Equal(1, notifications);
}
[Fact]
public void CollisionGenerationRejectsStaleReplacementWithoutMutatingActiveWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
RuntimeCollisionAdmission stale =
physics.BeginCollisionAdmission(0xA9B4FFFFu);
using PreparedLandblockCollisionGeneration preparedStale =
physics.PrepareCollisionGeneration(stale);
physics.StageCollisionAssets(
stale,
preparedStale,
CollisionAssets(0xA9B4FFFFu, terrainHeight: 10f));
RuntimeCollisionAdmission current =
physics.BeginCollisionAdmission(0xA9B4FFFFu);
Assert.Throws<InvalidOperationException>(() =>
physics.CommitCollisionGeneration(stale, preparedStale));
Assert.False(physics.Engine.IsLandblockTerrainResident(0xA9B4FFFFu));
using PreparedLandblockCollisionGeneration preparedCurrent =
physics.PrepareCollisionGeneration(current);
physics.StageCollisionAssets(
current,
preparedCurrent,
CollisionAssets(0xA9B4FFFFu, terrainHeight: 20f));
Assert.True(physics.CommitCollisionGeneration(
current,
preparedCurrent).Committed);
Assert.Equal(20f, physics.Engine.SampleTerrainZ(1f, 1f));
}
[Fact]
public void MovingDynamicOwnerDuringStagingMustRefreshBeforeCommit()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
RuntimeCollisionAdmission initialAdmission =
physics.BeginCollisionAdmission(0x0101FFFFu);
using (PreparedLandblockCollisionGeneration initial =
physics.PrepareCollisionGeneration(initialAdmission))
{
physics.StageCollisionAssets(
initialAdmission,
initial,
CollisionAssets(0x0101FFFFu, terrainHeight: 5f));
Assert.True(physics.CommitCollisionGeneration(
initialAdmission,
initial).Committed);
}
physics.Engine.ShadowObjects.Register(
42u,
0x01000001u,
new Vector3(10f, 10f, 0f),
Quaternion.Identity,
0.5f,
0f,
0f,
0x0101FFFFu,
seedCellId: 0x01010001u,
isStatic: false);
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(0x0101FFFFu);
using PreparedLandblockCollisionGeneration prepared =
physics.PrepareCollisionGeneration(admission);
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(0x0101FFFFu, terrainHeight: 15f));
uint owner = Assert.Single(physics.CaptureCollisionDynamicOwners(
admission,
prepared));
physics.RefreshCollisionDynamicOwner(admission, prepared, owner);
physics.Engine.ShadowObjects.UpdatePosition(
owner,
new Vector3(30f, 10f, 0f),
Quaternion.Identity,
0f,
0f,
0x0101FFFFu,
seedCellId: 0x01010009u);
RuntimeCollisionGenerationCommit rejected =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(rejected.Committed);
Assert.Equal(owner, Assert.Single(rejected.DirtyDynamicOwnerIds));
Assert.Equal(5f, physics.Engine.SampleTerrainZ(1f, 1f));
physics.RefreshCollisionDynamicOwner(admission, prepared, owner);
Assert.True(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
Assert.Contains(
physics.Engine.ShadowObjects.GetObjectsInCell(0x01010009u),
entry => entry.EntityId == owner);
}
[Fact]
public void SpawnAndDeleteDuringStagingAreBothGenerationGated()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(0x0101FFFFu);
using PreparedLandblockCollisionGeneration prepared =
physics.PrepareCollisionGeneration(admission);
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(0x0101FFFFu, terrainHeight: 12f));
Assert.Empty(physics.CaptureCollisionDynamicOwners(admission, prepared));
physics.Engine.ShadowObjects.Register(
77u,
0x01000001u,
new Vector3(10f, 10f, 0f),
Quaternion.Identity,
0.5f,
0f,
0f,
0x0101FFFFu,
seedCellId: 0x01010001u,
isStatic: false);
RuntimeCollisionGenerationCommit spawned =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(spawned.Committed);
Assert.Equal(77u, Assert.Single(spawned.DirtyDynamicOwnerIds));
physics.RefreshCollisionDynamicOwner(admission, prepared, 77u);
physics.Engine.ShadowObjects.Deregister(77u);
RuntimeCollisionGenerationCommit deleted =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(deleted.Committed);
Assert.Equal(77u, Assert.Single(deleted.DirtyDynamicOwnerIds));
physics.RefreshCollisionDynamicOwner(admission, prepared, 77u);
Assert.True(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
}
[Fact]
public void TerminalDisposalClearsLandblocksShadowsAndWorksets()
{
@ -352,10 +540,13 @@ public sealed class RuntimePhysicsStateTests
RuntimeCollisionAdmission admission =
lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu);
lifetime.Physics.AdmitCollisionAssets(
using PreparedLandblockCollisionGeneration prepared =
lifetime.Physics.PrepareCollisionGeneration(admission);
lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(0x0101FFFFu));
_ = lifetime.Physics.CompleteCollisionAdmission(admission);
_ = lifetime.Physics.CommitCollisionGeneration(admission, prepared);
lifetime.Physics.Engine.ShadowObjects.Register(
entityId: record.LocalEntityId!.Value,
gfxObjId: 0x01000001u,
@ -740,10 +931,12 @@ public sealed class RuntimePhysicsStateTests
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId)
uint landblockId,
float terrainHeight = 0f)
{
var heights = new byte[81];
var table = new float[256];
table[0] = terrainHeight;
return new RuntimeLandblockCollisionAssets(
landblockId,
new TerrainSurface(heights, table),