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

@ -70,15 +70,46 @@ public sealed class LandblockPhysicsPublication : IDisposable
internal int RefloodCursor { get; set; } internal int RefloodCursor { get; set; }
internal bool RefloodCommitted { get; set; } internal bool RefloodCommitted { get; set; }
internal bool SealCommitted { get; set; } internal bool SealCommitted { get; set; }
internal bool EngineMutationCommitted { get; set; }
/// <summary>
/// The last Runtime mutation poll was nonterminal. This includes exact
/// placement acknowledgements, collision-report/shadow dispatch debt, and
/// Runtime's deliberate first quiescence poll.
/// </summary>
internal bool RuntimeMutationPending { get; set; }
internal bool BeginCommitted { get; set; } internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; } internal bool CompletionCommitted { get; set; }
internal bool CancellationRequested { get; private set; }
public void Dispose() public void Dispose()
{ {
if (!CompletionCommitted) if (!TryCancel())
Physics.CancelCollisionGeneration( {
CollisionAdmission, throw new InvalidOperationException(
PreparedGeneration); "Collision publication cancellation is waiting for exact placement acknowledgements and must remain retained.");
}
}
internal bool TryCancel()
{
if (CompletionCommitted)
return true;
CancellationRequested = true;
for (int poll = 0; poll < 2; poll++)
{
if (Physics.CancelCollisionGeneration(
CollisionAdmission,
PreparedGeneration))
{
return true;
}
if (Physics.CaptureOwnership()
.PendingCollisionPrefixProjectionCount != 0)
{
return false;
}
}
return false;
} }
public uint LandblockId => Build.Landblock.LandblockId; public uint LandblockId => Build.Landblock.LandblockId;
@ -246,9 +277,14 @@ public sealed class LandblockPhysicsPublisher
publication.SetupObjectIds); publication.SetupObjectIds);
return publication; return publication;
} }
catch catch (Exception publicationError)
{ {
_physics.CancelCollisionGeneration(collisionAdmission, prepared); if (!_physics.CancelCollisionGeneration(collisionAdmission, prepared))
{
throw new AggregateException(
"Collision preparation failed and its pre-engine cancellation did not converge.",
publicationError);
}
throw; throw;
} }
} }
@ -416,7 +452,7 @@ public sealed class LandblockPhysicsPublisher
/// presentation pipeline supplies the static-presentation owner callback /// presentation pipeline supplies the static-presentation owner callback
/// that preserves per-entity light-before-collision order. /// that preserves per-entity light-before-collision order.
/// </summary> /// </summary>
public void CompletePublication( public bool CompletePublication(
LandblockPhysicsPublication publication, LandblockPhysicsPublication publication,
Action<WorldEntity>? beforeStaticCollision = null) Action<WorldEntity>? beforeStaticCollision = null)
{ {
@ -426,7 +462,29 @@ public sealed class LandblockPhysicsPublisher
"Physics publication cannot complete before its prefix commits."); "Physics publication cannot complete before its prefix commits.");
while (!AdvanceCompleteOne(publication, beforeStaticCollision)) while (!AdvanceCompleteOne(publication, beforeStaticCollision))
{ {
if (publication.RuntimeMutationPending)
{
if (!CanContinueMutationSynchronously())
return false;
// The activation transaction deliberately closes its exact
// quiescence boundary on one poll and transfers the engine on
// the next. This synchronous compatibility API may consume
// that finite internal suffix; the frame-budgeted pipeline
// always yields on the first nonterminal commit below.
publication.RuntimeMutationPending = false;
}
} }
return true;
}
internal bool CanContinueMutationSynchronously()
{
RuntimePhysicsOwnershipSnapshot ownership = _physics.CaptureOwnership();
return ownership.PendingCollisionPrefixProjectionCount == 0
&& ownership.PendingCollisionReportCount == 0
&& ownership.PendingCollisionSetPositionDispatchCount == 0
&& ownership.PendingShadowSetPositionDispatchCount == 0
&& !ownership.IsCollisionReportDispatching;
} }
/// <summary> /// <summary>
@ -438,11 +496,15 @@ public sealed class LandblockPhysicsPublisher
Action<WorldEntity>? beforeStaticCollision = null) Action<WorldEntity>? beforeStaticCollision = null)
{ {
ValidateReceipt(publication); ValidateReceipt(publication);
if (publication.CancellationRequested)
throw new InvalidOperationException(
"A cancelled collision publication cannot resume.");
if (!publication.BeginCommitted) if (!publication.BeginCommitted)
throw new InvalidOperationException( throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits."); "Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted) if (publication.CompletionCommitted)
return true; return true;
publication.RuntimeMutationPending = false;
long started = Stopwatch.GetTimestamp(); long started = Stopwatch.GetTimestamp();
LoadedLandblock landblock = publication.Build.Landblock; LoadedLandblock landblock = publication.Build.Landblock;
@ -567,12 +629,20 @@ public sealed class LandblockPhysicsPublisher
_physics.CommitCollisionGeneration( _physics.CommitCollisionGeneration(
publication.CollisionAdmission, publication.CollisionAdmission,
publication.PreparedGeneration); publication.PreparedGeneration);
if (!commit.Committed) publication.EngineMutationCommitted |= commit.EngineCommitted;
if (!commit.Completed)
{ {
// Runtime coalesces post-seal arrivals in its owner journal. publication.RuntimeMutationPending = true;
// Resume that seal tail rather than restarting the if (!commit.EngineCommitted)
// completed generation-wide capture/reflood pass. {
publication.SealCommitted = false; // Runtime coalesces post-seal arrivals in its owner
// journal. Resume that seal tail rather than restarting
// the completed generation-wide capture/reflood pass.
publication.SealCommitted = false;
}
// Once EngineCommitted is true the replacement is canonical
// and cannot be resealed or rolled back. Later frames poll
// only the exact resident-restore acknowledgement suffix.
_completePublishTicks += Stopwatch.GetTimestamp() - started; _completePublishTicks += Stopwatch.GetTimestamp() - started;
return false; return false;
} }
@ -587,16 +657,54 @@ public sealed class LandblockPhysicsPublisher
return publication.CompletionCommitted; return publication.CompletionCommitted;
} }
public void DemoteToTerrain(uint landblockId) public bool DemoteToTerrain(uint landblockId)
{ {
_physics.DemoteCollisionToTerrain(landblockId); for (int poll = 0; poll < 2; poll++)
_demotionCount++; {
if (AdvanceDemotion(landblockId))
return true;
if (_physics.CaptureOwnership()
.PendingCollisionPrefixProjectionCount != 0)
{
return false;
}
}
return false;
} }
public void RemoveLandblock(uint landblockId) internal bool AdvanceDemotion(uint landblockId)
{ {
_physics.WithdrawCollision(landblockId); RuntimeCollisionMutationResult result =
_physics.DemoteCollisionToTerrain(landblockId);
if (!result.Completed)
return false;
_demotionCount++;
return true;
}
public bool RemoveLandblock(uint landblockId)
{
for (int poll = 0; poll < 2; poll++)
{
if (AdvanceRemoval(landblockId))
return true;
if (_physics.CaptureOwnership()
.PendingCollisionPrefixProjectionCount != 0)
{
return false;
}
}
return false;
}
internal bool AdvanceRemoval(uint landblockId)
{
RuntimeCollisionMutationResult result =
_physics.WithdrawCollision(landblockId);
if (!result.Completed)
return false;
_fullRemovalCount++; _fullRemovalCount++;
return true;
} }
private void PublishCell( private void PublishCell(
@ -1066,7 +1174,8 @@ public sealed class LandblockPhysicsPublisher
"The physics publication receipt belongs to another publisher.", "The physics publication receipt belongs to another publisher.",
nameof(publication)); nameof(publication));
} }
if (!publication.CompletionCommitted) if (!publication.CompletionCommitted
&& !publication.EngineMutationCommitted)
{ {
ObjectDisposedException.ThrowIf( ObjectDisposedException.ThrowIf(
publication.PreparedGeneration.IsDisposed, publication.PreparedGeneration.IsDisposed,

View file

@ -217,14 +217,34 @@ public sealed class LandblockPresentationPipeline
/// <summary> /// <summary>
/// Cancels retained publication receipts during a generation reset. A /// Cancels retained publication receipts during a generation reset. A
/// collision receipt owns only its private staging world until activation, /// pre-engine receipt restores its exact prior generation. A receipt that
/// so cancellation cannot withdraw or partially replace the active world. /// already transferred the engine generation remains retained until its
/// canonical post-engine placement acknowledgement suffix completes; the
/// committed transfer is never rolled back.
/// </summary> /// </summary>
internal void CancelPendingPublications() internal bool CancelPendingPublications()
{ {
foreach (PublicationTransaction transaction in _publications.Values) if (_publications.Count == 0)
transaction.PhysicsPublication?.Dispose(); return true;
_publications.Clear(); LandblockStreamResult[] pending = [.. _publications.Keys];
bool completed = true;
for (int index = 0; index < pending.Length; index++)
{
LandblockStreamResult result = pending[index];
if (!_publications.TryGetValue(
result,
out PublicationTransaction? transaction))
{
continue;
}
bool cancelled = transaction.PhysicsPublication is not { } physics
|| physics.TryCancel();
if (cancelled)
_publications.Remove(result);
else
completed = false;
}
return completed && _publications.Count == 0;
} }
public void ResumePublication(LandblockStreamResult result) public void ResumePublication(LandblockStreamResult result)
@ -247,10 +267,15 @@ public sealed class LandblockPresentationPipeline
return Advance(result, transaction, meter, ensureProgress); return Advance(result, transaction, meter, ensureProgress);
} }
public void AdvanceRetirements() => _retirements.Advance(); public void AdvanceRetirements()
{
_retirements.Advance();
}
public void AdvanceRetirements(StreamingWorkMeter meter) => public void AdvanceRetirements(StreamingWorkMeter meter)
{
_retirements.Advance(meter); _retirements.Advance(meter);
}
internal void AdvancePriorityRetirement( internal void AdvancePriorityRetirement(
uint landblockId, uint landblockId,
@ -261,14 +286,26 @@ public sealed class LandblockPresentationPipeline
{ {
_retirements.BeginFull(landblockId); _retirements.BeginFull(landblockId);
if (_retirements.UsesBudgetedSteps) if (_retirements.UsesBudgetedSteps)
{
_retirements.Advance(); _retirements.Advance();
if (_retirements.IsPending(landblockId)
&& (_physicsPublisher?.CanContinueMutationSynchronously()
?? true))
_retirements.Advance();
}
} }
public void BeginNearLayerRetirement(uint landblockId) public void BeginNearLayerRetirement(uint landblockId)
{ {
_retirements.BeginNearLayer(landblockId); _retirements.BeginNearLayer(landblockId);
if (_retirements.UsesBudgetedSteps) if (_retirements.UsesBudgetedSteps)
{
_retirements.Advance(); _retirements.Advance();
if (_retirements.IsPending(landblockId)
&& (_physicsPublisher?.CanContinueMutationSynchronously()
?? true))
_retirements.Advance();
}
} }
internal void EnqueueFullRetirement(uint landblockId) => internal void EnqueueFullRetirement(uint landblockId) =>
@ -754,6 +791,17 @@ public sealed class LandblockPresentationPipeline
{ {
return new LandblockPublicationAdvance(false, progressed); return new LandblockPublicationAdvance(false, progressed);
} }
if (transaction.PhysicsPublication.RuntimeMutationPending)
{
if (meter is not null
|| !_physicsPublisher
.CanContinueMutationSynchronously())
{
return new LandblockPublicationAdvance(
false,
progressed);
}
}
} }
while (!transaction.StaticPublication.CompletionCommitted) while (!transaction.StaticPublication.CompletionCommitted)
{ {

View file

@ -73,22 +73,20 @@ public sealed class LandblockPresentationRetirementOwner
static entity => entity.ServerGuid == 0, static entity => entity.ServerGuid == 0,
_staticPresentation.RemovePluginProjection); _staticPresentation.RemovePluginProjection);
if (!ticket.RunOnce(
LandblockRetirementStage.Physics,
() => ticket.Kind == LandblockRetirementKind.Full
? _physics.AdvanceRemoval(ticket.LandblockId)
: _physics.AdvanceDemotion(ticket.LandblockId)))
{
return;
}
if (ticket.Kind == LandblockRetirementKind.Full) if (ticket.Kind == LandblockRetirementKind.Full)
{ {
ticket.RunOnce( ticket.RunOnce(
LandblockRetirementStage.Terrain, LandblockRetirementStage.Terrain,
() => _render.RemoveTerrain(ticket.LandblockId)); () => _render.RemoveTerrain(ticket.LandblockId));
} }
ticket.RunOnce(
LandblockRetirementStage.Physics,
() =>
{
if (ticket.Kind == LandblockRetirementKind.Full)
_physics.RemoveLandblock(ticket.LandblockId);
else
_physics.DemoteToTerrain(ticket.LandblockId);
});
ticket.RunOnce( ticket.RunOnce(
LandblockRetirementStage.CellVisibility, LandblockRetirementStage.CellVisibility,
() => _render.RemoveCellVisibility(ticket.LandblockId)); () => _render.RemoveCellVisibility(ticket.LandblockId));
@ -128,13 +126,9 @@ public sealed class LandblockPresentationRetirementOwner
LandblockRetirementStage.Physics => LandblockRetirementStage.Physics =>
ticket.RunOnceStep( ticket.RunOnceStep(
LandblockRetirementStage.Physics, LandblockRetirementStage.Physics,
() => () => ticket.Kind == LandblockRetirementKind.Full
{ ? _physics.AdvanceRemoval(ticket.LandblockId)
if (ticket.Kind == LandblockRetirementKind.Full) : _physics.AdvanceDemotion(ticket.LandblockId)),
_physics.RemoveLandblock(ticket.LandblockId);
else
_physics.DemoteToTerrain(ticket.LandblockId);
}),
LandblockRetirementStage.CellVisibility => LandblockRetirementStage.CellVisibility =>
ticket.RunOnceStep( ticket.RunOnceStep(
LandblockRetirementStage.CellVisibility, LandblockRetirementStage.CellVisibility,

View file

@ -12,8 +12,8 @@ public enum LandblockRetirementStage : ushort
EntityLighting = 1 << 3, EntityLighting = 1 << 3,
EntityTranslucency = 1 << 4, EntityTranslucency = 1 << 4,
PluginProjection = 1 << 5, PluginProjection = 1 << 5,
Terrain = 1 << 6, Physics = 1 << 6,
Physics = 1 << 7, Terrain = 1 << 7,
CellVisibility = 1 << 8, CellVisibility = 1 << 8,
BuildingRegistry = 1 << 9, BuildingRegistry = 1 << 9,
EnvironmentCells = 1 << 10, EnvironmentCells = 1 << 10,
@ -24,6 +24,7 @@ internal enum LandblockRetirementOperationResult : byte
{ {
NoWork, NoWork,
Progressed, Progressed,
Pending,
Failed, Failed,
} }
@ -77,6 +78,28 @@ public sealed class LandblockRetirementTicket
} }
} }
public bool RunOnce(LandblockRetirementStage stage, Func<bool> operation)
{
ValidateSingleStage(stage);
ArgumentNullException.ThrowIfNull(operation);
if ((CompletedStages & stage) != 0)
return true;
try
{
if (!operation())
return false;
CompletedStages |= stage;
_failures.Remove(stage);
return true;
}
catch (Exception error)
{
_failures[stage] = error;
return false;
}
}
internal LandblockRetirementOperationResult RunOnceStep( internal LandblockRetirementOperationResult RunOnceStep(
LandblockRetirementStage stage, LandblockRetirementStage stage,
Action operation) Action operation)
@ -100,6 +123,31 @@ public sealed class LandblockRetirementTicket
} }
} }
internal LandblockRetirementOperationResult RunOnceStep(
LandblockRetirementStage stage,
Func<bool> operation)
{
ValidateSingleStage(stage);
ArgumentNullException.ThrowIfNull(operation);
if ((CompletedStages & stage) != 0)
return LandblockRetirementOperationResult.NoWork;
try
{
if (!operation())
return LandblockRetirementOperationResult.Pending;
CompletedStages |= stage;
_failures.Remove(stage);
return LandblockRetirementOperationResult.Progressed;
}
catch (Exception error)
{
_failures[stage] = error;
return LandblockRetirementOperationResult.Failed;
}
}
public bool RunForEachEntity( public bool RunForEachEntity(
LandblockRetirementStage stage, LandblockRetirementStage stage,
Func<WorldEntity, bool> predicate, Func<WorldEntity, bool> predicate,
@ -815,6 +863,8 @@ public sealed class LandblockRetirementCoordinator
} }
meter.Complete(); meter.Complete();
if (result == LandblockRetirementOperationResult.Pending)
return BudgetedAdvanceResult.Yielded;
return BudgetedAdvanceResult.Progressed; return BudgetedAdvanceResult.Progressed;
} }

View file

@ -1362,19 +1362,23 @@ public sealed class StreamingController
{ {
if (!transaction.PendingPublicationsCleared) if (!transaction.PendingPublicationsCleared)
{ {
bool cancelled = false;
if (!TryRunStreamingWork( if (!TryRunStreamingWork(
meter, meter,
new StreamingWorkCost(EntityOperations: 1), new StreamingWorkCost(EntityOperations: 1),
"recenter-cancel-publications", "recenter-cancel-publications",
() => () =>
{ {
_presentation.CancelPendingPublications(); cancelled =
transaction.PendingPublicationsCleared = true; _presentation.CancelPendingPublications();
return true; return true;
})) }))
{ {
return false; return false;
} }
transaction.PendingPublicationsCleared = cancelled;
if (!cancelled)
return false;
} }
if (!TryRunStreamingWork( if (!TryRunStreamingWork(
meter, meter,
@ -1519,19 +1523,23 @@ public sealed class StreamingController
{ {
if (!transaction.PendingPublicationsCleared) if (!transaction.PendingPublicationsCleared)
{ {
bool cancelled = false;
if (!TryRunStreamingWork( if (!TryRunStreamingWork(
meter, meter,
new StreamingWorkCost(EntityOperations: 1), new StreamingWorkCost(EntityOperations: 1),
"reload-cancel-publications", "reload-cancel-publications",
() => () =>
{ {
_presentation.CancelPendingPublications(); cancelled =
transaction.PendingPublicationsCleared = true; _presentation.CancelPendingPublications();
return true; return true;
})) }))
{ {
return false; return false;
} }
transaction.PendingPublicationsCleared = cancelled;
if (!cancelled)
return false;
} }
if (!TryRunStreamingWork( if (!TryRunStreamingWork(
meter, meter,

View file

@ -26,6 +26,12 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>AcDream.Runtime.Tests</_Parameter1> <_Parameter1>AcDream.Runtime.Tests</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>AcDream.App.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>AcDream.Headless.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" /> <ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />

View file

@ -1106,7 +1106,7 @@ public sealed class PhysicsEngine
/// Register a landblock with its terrain surface, indoor cells, portal /// Register a landblock with its terrain surface, indoor cells, portal
/// planes, and world-space origin offset. /// planes, and world-space origin offset.
/// </summary> /// </summary>
public void AddLandblock(uint landblockId, TerrainSurface terrain, internal void AddLandblock(uint landblockId, TerrainSurface terrain,
IReadOnlyList<CellSurface> cells, IReadOnlyList<PortalPlane> portals, IReadOnlyList<CellSurface> cells, IReadOnlyList<PortalPlane> portals,
float worldOffsetX, float worldOffsetY) float worldOffsetX, float worldOffsetY)
{ {
@ -1120,7 +1120,7 @@ public sealed class PhysicsEngine
/// <summary> /// <summary>
/// Remove a previously registered landblock, including its shadow objects. /// Remove a previously registered landblock, including its shadow objects.
/// </summary> /// </summary>
public void RemoveLandblock(uint landblockId) internal void RemoveLandblock(uint landblockId)
{ {
_landblocks.Remove(landblockId); _landblocks.Remove(landblockId);
RemoveLandblockSlot(landblockId); RemoveLandblockSlot(landblockId);
@ -1152,7 +1152,7 @@ public sealed class PhysicsEngine
/// owned by this engine. Runtime calls this only at terminal disposal; /// owned by this engine. Runtime calls this only at terminal disposal;
/// ordinary streaming still uses the typed per-landblock retirement path. /// ordinary streaming still uses the typed per-landblock retirement path.
/// </summary> /// </summary>
public void Clear() internal void Clear()
{ {
if (_landblocks.Count != 0) if (_landblocks.Count != 0)
{ {
@ -1173,7 +1173,7 @@ public sealed class PhysicsEngine
/// while preserving its terrain surface and world offset for Far-tier use. /// while preserving its terrain surface and world offset for Far-tier use.
/// The corresponding render-side demotion preserves the terrain slot too. /// The corresponding render-side demotion preserves the terrain slot too.
/// </summary> /// </summary>
public void DemoteLandblockToTerrain(uint landblockId) internal void DemoteLandblockToTerrain(uint landblockId)
{ {
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu; uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
if (_landblocks.TryGetValue(canonical, out var landblock)) if (_landblocks.TryGetValue(canonical, out var landblock))

View file

@ -18,9 +18,37 @@ internal interface IHeadlessCollisionNeighborhood
bool IsReady(uint fullCellId); bool IsReady(uint fullCellId);
} }
internal static class HeadlessCollisionGenerationTransaction internal readonly record struct HeadlessCollisionGenerationAdvance(
bool Completed,
bool Progressed,
bool WaitingForProjectionAcknowledgement,
bool YieldToCaller);
internal sealed class HeadlessCollisionGenerationTransaction
{ {
internal static RuntimeCollisionGenerationCommit Execute( private readonly RuntimePhysicsState _physics;
private readonly RuntimeCollisionAdmission _admission;
private readonly PreparedLandblockCollisionGeneration _prepared;
private bool _ownerCaptureCommitted;
private int _refreshCursor;
private bool _sealCommitted;
private HeadlessCollisionGenerationTransaction(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
_physics = physics;
_admission = admission;
_prepared = prepared;
}
internal uint LandblockId => _admission.LandblockId;
internal bool EngineMutationCommitted { get; private set; }
internal bool CompletionCommitted { get; private set; }
internal bool CancellationRequested { get; private set; }
internal static HeadlessCollisionGenerationTransaction Begin(
RuntimePhysicsState physics, RuntimePhysicsState physics,
uint landblockId, uint landblockId,
Action<RuntimeCollisionAdmission>? afterAdmission, Action<RuntimeCollisionAdmission>? afterAdmission,
@ -33,60 +61,103 @@ internal static class HeadlessCollisionGenerationTransaction
RuntimeCollisionAdmission admission = RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId); physics.BeginCollisionAdmission(landblockId);
PreparedLandblockCollisionGeneration? prepared = null; PreparedLandblockCollisionGeneration? prepared = null;
bool committed = false;
try try
{ {
// This hook exists so the exact post-admission/pre-prepare failure
// boundary remains covered. Production does not install one.
afterAdmission?.Invoke(admission); afterAdmission?.Invoke(admission);
prepared = physics.PrepareCollisionGeneration(admission); prepared = physics.PrepareCollisionGeneration(admission);
stage(admission, prepared); stage(admission, prepared);
return new HeadlessCollisionGenerationTransaction(
RuntimeCollisionOwnerCaptureStep ownerCapture; physics,
do admission,
{ prepared);
ownerCapture = physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared);
}
while (!ownerCapture.Completed);
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
{
throw new InvalidOperationException(
"Headless collision owner set changed during synchronous sealing.");
}
RuntimeCollisionGenerationCommit result =
physics.CommitCollisionGeneration(admission, prepared);
if (!result.Committed)
{
throw new InvalidOperationException(
"Headless collision generation changed during synchronous publication.");
}
committed = true;
return result;
} }
finally catch (Exception publicationError)
{ {
if (!committed) if (!physics.CancelCollisionGeneration(admission, prepared))
physics.CancelCollisionGeneration(admission, prepared); {
throw new AggregateException(
"Headless collision preparation failed and its pre-engine cancellation did not converge.",
publicationError);
}
throw;
} }
} }
internal HeadlessCollisionGenerationAdvance Advance()
{
if (CancellationRequested)
throw new InvalidOperationException(
"A cancelled headless collision generation cannot resume.");
if (CompletionCommitted)
return new(true, false, false, false);
if (!EngineMutationCommitted)
{
if (!_ownerCaptureCommitted)
{
RuntimeCollisionOwnerCaptureStep capture =
_physics.AdvanceCollisionRetainedOwnerCapture(
_admission,
_prepared);
_ownerCaptureCommitted = capture.Completed;
return new(false, true, false, false);
}
if (_refreshCursor < _prepared.RetainedOwnerIds.Count)
{
_physics.RefreshCollisionRetainedOwner(
_admission,
_prepared,
_prepared.RetainedOwnerIds[_refreshCursor]);
_refreshCursor++;
return new(false, true, false, false);
}
if (!_sealCommitted)
{
RuntimeCollisionSealStep seal =
_physics.AdvanceCollisionGenerationSeal(
_admission,
_prepared);
_sealCommitted = seal.Completed;
if (seal.Restarted)
{
_ownerCaptureCommitted = false;
_refreshCursor = 0;
}
return new(false, true, false, false);
}
}
RuntimeCollisionGenerationCommit commit =
_physics.CommitCollisionGeneration(_admission, _prepared);
EngineMutationCommitted |= commit.EngineCommitted;
CompletionCommitted = commit.Completed;
bool waiting = !commit.Completed
&& _physics.CaptureOwnership()
.PendingCollisionPrefixProjectionCount != 0;
if (!commit.Completed && !commit.EngineCommitted)
_sealCommitted = false;
return new(
commit.Completed,
Progressed: true,
WaitingForProjectionAcknowledgement: waiting,
YieldToCaller: !commit.Completed);
}
internal bool TryCancel()
{
if (CompletionCommitted)
return true;
CancellationRequested = true;
bool completed = _physics.CancelCollisionGeneration(
_admission,
_prepared);
if (completed && EngineMutationCommitted)
CompletionCommitted = true;
return completed;
}
} }
/// <summary> /// <summary>
@ -97,10 +168,23 @@ internal static class HeadlessCollisionGenerationTransaction
internal sealed class HeadlessCollisionNeighborhood internal sealed class HeadlessCollisionNeighborhood
: IHeadlessCollisionNeighborhood : IHeadlessCollisionNeighborhood
{ {
private readonly record struct PublicationSpec(
uint LandblockId,
Vector3 Origin,
bool Required);
private readonly GameRuntime _runtime; private readonly GameRuntime _runtime;
private readonly HeadlessProcessContentOwner private readonly HeadlessProcessContentOwner
.HeadlessProcessContentLease _content; .HeadlessProcessContentLease _content;
private readonly HashSet<uint> _resident = []; private readonly HashSet<uint> _resident = [];
private readonly Queue<uint> _retirementQueue = [];
private readonly Queue<PublicationSpec> _publicationQueue = [];
private HeadlessCollisionGenerationTransaction? _pendingPublication;
private bool _pendingPublicationCancellation;
private bool _resetRequired;
private bool _publicationPlanBuilt;
private uint _requestedCenterLandblock;
private uint _requestedFullCell;
private uint _centerLandblock; private uint _centerLandblock;
internal HeadlessCollisionNeighborhood( internal HeadlessCollisionNeighborhood(
@ -122,6 +206,21 @@ internal sealed class HeadlessCollisionNeighborhood
nameof(fullCellId), nameof(fullCellId),
"A collision neighborhood requires a real destination cell."); "A collision neighborhood requires a real destination cell.");
} }
if (_requestedCenterLandblock != center)
{
_requestedCenterLandblock = center;
_requestedFullCell = fullCellId;
_resetRequired = true;
_publicationPlanBuilt = false;
_publicationQueue.Clear();
_pendingPublicationCancellation =
_pendingPublication is not null;
}
else
{
_requestedFullCell = fullCellId;
}
if (_centerLandblock == center if (_centerLandblock == center
&& _resident.Contains(center) && _resident.Contains(center)
&& _runtime.EntityObjects.Physics.Engine && _runtime.EntityObjects.Physics.Engine
@ -131,55 +230,17 @@ internal sealed class HeadlessCollisionNeighborhood
.UpdatePlayerCurrCell(fullCellId); .UpdatePlayerCurrCell(fullCellId);
return; return;
} }
AdvanceWork();
RetireAll();
int centerX = (int)((center >> 24) & 0xFFu);
int centerY = (int)((center >> 16) & 0xFFu);
try
{
PublishOne(
center,
Vector3.Zero,
fullCellId,
required: true);
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0)
continue;
int landblockX = centerX + dx;
int landblockY = centerY + dy;
if ((uint)landblockX > byte.MaxValue
|| (uint)landblockY > byte.MaxValue)
{
continue;
}
uint landblockId =
((uint)landblockX << 24)
| ((uint)landblockY << 16)
| 0xFFFFu;
PublishOne(
landblockId,
new Vector3(dx * 192f, dy * 192f, 0f),
fullCellId,
required: false);
}
}
_centerLandblock = center;
_runtime.EntityObjects.Physics.Engine
.UpdatePlayerCurrCell(fullCellId);
}
catch
{
RetireAll();
throw;
}
} }
public bool IsReady(uint fullCellId) public bool IsReady(uint fullCellId)
{ {
uint center = CanonicalLandblock(fullCellId); uint center = CanonicalLandblock(fullCellId);
if (_requestedCenterLandblock == center)
{
_requestedFullCell = fullCellId;
AdvanceWork();
}
if (_centerLandblock != center if (_centerLandblock != center
|| !_resident.Contains(center) || !_resident.Contains(center)
|| !_runtime.EntityObjects.Physics.Engine || !_runtime.EntityObjects.Physics.Engine
@ -192,7 +253,7 @@ internal sealed class HeadlessCollisionNeighborhood
.GetCellStruct(fullCellId) is not null; .GetCellStruct(fullCellId) is not null;
} }
private void PublishOne( private HeadlessCollisionGenerationTransaction? CreatePublication(
uint landblockId, uint landblockId,
Vector3 origin, Vector3 origin,
uint currentCellId, uint currentCellId,
@ -207,7 +268,7 @@ internal sealed class HeadlessCollisionNeighborhood
throw new InvalidDataException( throw new InvalidDataException(
$"Required headless landblock 0x{landblockId:X8} is missing."); $"Required headless landblock 0x{landblockId:X8} is missing.");
} }
return; return null;
} }
IReadOnlyList<WorldEntity> staticEntities = IReadOnlyList<WorldEntity> staticEntities =
@ -244,7 +305,7 @@ internal sealed class HeadlessCollisionNeighborhood
landblock); landblock);
RuntimePhysicsState physics = _runtime.EntityObjects.Physics; RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
_ = HeadlessCollisionGenerationTransaction.Execute( return HeadlessCollisionGenerationTransaction.Begin(
physics, physics,
landblockId, landblockId,
afterAdmission: null, afterAdmission: null,
@ -294,22 +355,113 @@ internal sealed class HeadlessCollisionNeighborhood
collisions, collisions,
origin); origin);
}); });
_resident.Add(CanonicalLandblock(landblockId));
} }
private void RetireAll() private void AdvanceWork()
{ {
if (_resident.Count == 0) if (_pendingPublicationCancellation
&& _pendingPublication is { } cancelling)
{ {
_centerLandblock = 0u; bool wasCommitted = cancelling.EngineMutationCommitted;
return; if (!cancelling.TryCancel())
return;
if (wasCommitted || cancelling.EngineMutationCommitted)
_resident.Add(cancelling.LandblockId);
_pendingPublication = null;
_pendingPublicationCancellation = false;
} }
uint[] retiring = [.. _resident]; if (_resetRequired)
_resident.Clear(); {
_centerLandblock = 0u; _retirementQueue.Clear();
foreach (uint landblock in retiring) foreach (uint landblock in _resident)
_ = _runtime.EntityObjects.Physics.WithdrawCollision(landblock); _retirementQueue.Enqueue(landblock);
_centerLandblock = 0u;
_resetRequired = false;
}
while (_retirementQueue.TryPeek(out uint retiring))
{
RuntimeCollisionMutationResult result = _runtime.EntityObjects
.Physics.WithdrawCollision(retiring);
if (!result.Completed)
return;
_retirementQueue.Dequeue();
_resident.Remove(retiring);
}
if (!_publicationPlanBuilt)
{
BuildPublicationPlan(
_requestedCenterLandblock,
_publicationQueue);
_publicationPlanBuilt = true;
}
while (true)
{
if (_pendingPublication is null)
{
if (!_publicationQueue.TryDequeue(out PublicationSpec spec))
{
_centerLandblock = _requestedCenterLandblock;
_runtime.EntityObjects.Physics.Engine
.UpdatePlayerCurrCell(_requestedFullCell);
return;
}
_pendingPublication = CreatePublication(
spec.LandblockId,
spec.Origin,
_requestedFullCell,
spec.Required);
if (_pendingPublication is null)
continue;
}
HeadlessCollisionGenerationAdvance advance =
_pendingPublication.Advance();
if (advance.Completed)
{
_resident.Add(_pendingPublication.LandblockId);
_pendingPublication = null;
continue;
}
if (advance.WaitingForProjectionAcknowledgement)
return;
if (advance.YieldToCaller)
return;
if (!advance.Progressed)
throw new InvalidOperationException(
"Headless collision publication made no progress.");
}
}
private static void BuildPublicationPlan(
uint center,
Queue<PublicationSpec> destination)
{
destination.Enqueue(new PublicationSpec(
center,
Vector3.Zero,
Required: true));
int centerX = (int)((center >> 24) & 0xFFu);
int centerY = (int)((center >> 16) & 0xFFu);
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0)
continue;
int x = centerX + dx;
int y = centerY + dy;
if ((uint)x > byte.MaxValue || (uint)y > byte.MaxValue)
continue;
destination.Enqueue(new PublicationSpec(
((uint)x << 24) | ((uint)y << 16) | 0xFFFFu,
new Vector3(dx * 192f, dy * 192f, 0f),
Required: false));
}
}
} }
private static uint CanonicalLandblock(uint fullCellId) => private static uint CanonicalLandblock(uint fullCellId) =>

View file

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

View file

@ -388,6 +388,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority; 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, // All validation is complete. The remaining stores are callback-free,
// non-allocating, and cannot fail on this single Runtime update thread. // non-allocating, and cannot fail on this single Runtime update thread.
// The controller remains RuntimeOwnedDormant; the subsequent world // The controller remains RuntimeOwnedDormant; the subsequent world

View file

@ -37,6 +37,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
bool IsCollisionReportDispatching, bool IsCollisionReportDispatching,
int CollisionPrefixQuiescenceCount, int CollisionPrefixQuiescenceCount,
int PendingCollisionPrefixProjectionCount, int PendingCollisionPrefixProjectionCount,
int CollisionPrefixMutationCount,
int CommittedCollisionPrefixMutationCount,
int CollisionAdmissionCount, int CollisionAdmissionCount,
int CollisionGenerationCount, int CollisionGenerationCount,
bool OwnsProductionDataCache, bool OwnsProductionDataCache,
@ -75,6 +77,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& !IsCollisionReportDispatching && !IsCollisionReportDispatching
&& CollisionPrefixQuiescenceCount == 0 && CollisionPrefixQuiescenceCount == 0
&& PendingCollisionPrefixProjectionCount == 0 && PendingCollisionPrefixProjectionCount == 0
&& CollisionPrefixMutationCount == 0
&& CommittedCollisionPrefixMutationCount == 0
&& CollisionAdmissionCount == 0 && CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0 && CollisionGenerationCount == 0
&& OwnsProductionDataCache; && OwnsProductionDataCache;
@ -100,11 +104,13 @@ public sealed class RuntimeCollisionAdmission
internal RuntimeCollisionAdmission( internal RuntimeCollisionAdmission(
RuntimePhysicsState owner, RuntimePhysicsState owner,
uint landblockId, uint landblockId,
ulong generation) ulong generation,
ulong previousGeneration)
{ {
Owner = owner; Owner = owner;
LandblockId = landblockId; LandblockId = landblockId;
Generation = generation; Generation = generation;
PreviousGeneration = previousGeneration;
} }
internal RuntimePhysicsState Owner { get; } internal RuntimePhysicsState Owner { get; }
@ -112,6 +118,32 @@ public sealed class RuntimeCollisionAdmission
internal bool Completed { get; set; } internal bool Completed { get; set; }
public uint LandblockId { get; } public uint LandblockId { get; }
public ulong Generation { 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( public readonly record struct RuntimeCollisionAcknowledgement(
@ -120,11 +152,23 @@ public readonly record struct RuntimeCollisionAcknowledgement(
bool WasResident, bool WasResident,
bool Ready); 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( public readonly record struct RuntimeCollisionGenerationCommit(
RuntimeCollisionAcknowledgement Acknowledgement, RuntimeCollisionAcknowledgement Acknowledgement,
uint[] DirtyRetainedOwnerIds) uint[] DirtyRetainedOwnerIds,
bool EngineCommitted,
bool Completed)
{ {
public bool Committed => Acknowledgement.Ready; public bool Committed => Completed;
} }
public readonly record struct RuntimeCollisionGenerationCommitted( public readonly record struct RuntimeCollisionGenerationCommitted(
@ -1045,6 +1089,8 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionAdmissions = new(); _collisionAdmissions = new();
private readonly Dictionary<uint, PreparedLandblockCollisionGeneration> private readonly Dictionary<uint, PreparedLandblockCollisionGeneration>
_preparedCollisionGenerations = new(); _preparedCollisionGenerations = new();
private readonly Dictionary<uint, RuntimeCollisionPrefixMutation>
_collisionPrefixMutations = new();
private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new(); private readonly CollisionOwnerMutationJournal _collisionOwnerJournal = new();
private readonly Dictionary<uint, List<PreparedLandblockCollisionGeneration>> private readonly Dictionary<uint, List<PreparedLandblockCollisionGeneration>>
_collisionOwnerSubscribers = new(); _collisionOwnerSubscribers = new();
@ -1175,6 +1221,9 @@ public sealed class RuntimePhysicsState : IDisposable
collisionReports.IsDispatching, collisionReports.IsDispatching,
setPosition.CollisionPrefixQuiescenceCount, setPosition.CollisionPrefixQuiescenceCount,
setPosition.PendingQuiescenceProjectionCount, setPosition.PendingQuiescenceProjectionCount,
_collisionPrefixMutations.Count,
_collisionPrefixMutations.Values.Count(
mutation => mutation.EngineMutationCommitted),
_collisionAdmissions.Count, _collisionAdmissions.Count,
_collisionGenerations.Count, _collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache), ReferenceEquals(Engine.DataCache, DataCache),
@ -1907,6 +1956,29 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRoots.Clear(); _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 internal RuntimeCollisionPrefixQuiescenceToken
BeginCollisionPrefixQuiescence( BeginCollisionPrefixQuiescence(
uint landblockId, uint landblockId,
@ -1957,25 +2029,29 @@ public sealed class RuntimePhysicsState : IDisposable
successorReady); successorReady);
} }
internal bool CompleteCollisionPrefixQuiescence(
in RuntimeCollisionPrefixMutationPermission permission)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
return SetPosition.CompleteCollisionPrefixQuiescence(permission);
}
public RuntimeCollisionAdmission BeginCollisionAdmission( public RuntimeCollisionAdmission BeginCollisionAdmission(
uint landblockId) uint landblockId)
{ {
EnsureNotDisposed(); EnsureNotDisposed();
EnsureCollisionMutationThread(); EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId); 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(); AdvanceCollisionWorldAuthority();
if (_collisionAdmissions.Remove( if (_collisionAdmissions.Remove(
canonical, canonical,
out RuntimeCollisionAdmission? superseded)) out RuntimeCollisionAdmission? superseded))
{ {
previousGeneration = superseded.PreviousGeneration;
SetPosition.CancelCollisionGeneration( SetPosition.CancelCollisionGeneration(
canonical, canonical,
superseded.Generation); superseded.Generation);
@ -1986,16 +2062,13 @@ public sealed class RuntimePhysicsState : IDisposable
prepared.Dispose(); prepared.Dispose();
} }
} }
ulong generation = _collisionGenerations.TryGetValue( ulong generation = checked(currentGeneration + 1UL);
canonical,
out ulong current)
? checked(current + 1UL)
: 1UL;
_collisionGenerations[canonical] = generation; _collisionGenerations[canonical] = generation;
var admission = new RuntimeCollisionAdmission( var admission = new RuntimeCollisionAdmission(
this, this,
canonical, canonical,
generation); generation,
previousGeneration);
_collisionAdmissions[canonical] = admission; _collisionAdmissions[canonical] = admission;
SetPosition.BeginCollisionGeneration(canonical, generation); SetPosition.BeginCollisionGeneration(canonical, generation);
return admission; return admission;
@ -2047,7 +2120,7 @@ public sealed class RuntimePhysicsState : IDisposable
/// collision world is never withdrawn. A stale receipt may dispose its /// collision world is never withdrawn. A stale receipt may dispose its
/// own staging storage but cannot invalidate a newer admission. /// own staging storage but cannot invalidate a newer admission.
/// </summary> /// </summary>
internal void CancelCollisionGeneration( internal bool CancelCollisionGeneration(
RuntimeCollisionAdmission admission, RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration? prepared = null) PreparedLandblockCollisionGeneration? prepared = null)
{ {
@ -2067,6 +2140,38 @@ public sealed class RuntimePhysicsState : IDisposable
nameof(prepared)); 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(); prepared?.Dispose();
if (prepared is not null if (prepared is not null
&& _preparedCollisionGenerations.TryGetValue( && _preparedCollisionGenerations.TryGetValue(
@ -2090,6 +2195,7 @@ public sealed class RuntimePhysicsState : IDisposable
AdvanceCollisionWorldAuthority(); AdvanceCollisionWorldAuthority();
} }
TrimCollisionOwnerJournal(); TrimCollisionOwnerJournal();
return true;
} }
internal void StageCollisionAssets( internal void StageCollisionAssets(
@ -2261,8 +2367,29 @@ public sealed class RuntimePhysicsState : IDisposable
RuntimeCollisionAdmission admission, RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared) PreparedLandblockCollisionGeneration prepared)
{ {
ValidateAdmission(admission); EnsureNotDisposed();
EnsureCollisionMutationThread(); 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); ValidatePreparedGeneration(admission, prepared);
if (!admission.AssetsPrepared) if (!admission.AssetsPrepared)
{ {
@ -2292,8 +2419,62 @@ public sealed class RuntimePhysicsState : IDisposable
admission.Generation, admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId), Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false), 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 = PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
prepared.TakeSealedReplacement(); prepared.TakeSealedReplacement();
bool suppressOwnerJournal = _suppressCollisionOwnerJournal; bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
@ -2313,20 +2494,70 @@ public sealed class RuntimePhysicsState : IDisposable
if (later.Sequence > prepared.Sequence) if (later.Sequence > prepared.Sequence)
later.EnqueueCommittedRebase(replacement); later.EnqueueCommittedRebase(replacement);
} }
admission.Completed = true;
_collisionAdmissions.Remove(admission.LandblockId);
_preparedCollisionGenerations.Remove(admission.LandblockId); _preparedCollisionGenerations.Remove(admission.LandblockId);
prepared.MarkCommitted(); prepared.MarkCommitted();
TrimCollisionOwnerJournal(); mutation.EngineMutationCommitted = true;
var acknowledgement = new RuntimeCollisionAcknowledgement( mutation.Ready = Engine.IsLandblockTerrainResident(
admission.LandblockId, admission.LandblockId);
admission.Generation, mutation.WasResident = mutation.Ready;
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
SetPosition.CommitCollisionGeneration( SetPosition.CommitCollisionGeneration(
acknowledgement.LandblockId, mutation.LandblockId,
acknowledgement.Generation, mutation.TargetGeneration,
acknowledgement.Ready); 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( PublishCollisionGenerationCommitted(
new RuntimeCollisionGenerationCommitted( new RuntimeCollisionGenerationCommitted(
acknowledgement.LandblockId, acknowledgement.LandblockId,
@ -2334,67 +2565,179 @@ public sealed class RuntimePhysicsState : IDisposable
acknowledgement.Ready)); acknowledgement.Ready));
return new RuntimeCollisionGenerationCommit( return new RuntimeCollisionGenerationCommit(
acknowledgement, 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) uint landblockId)
=> AdvanceCollisionRetirementMutation(
landblockId,
RuntimeCollisionPrefixMutationKind.Demotion);
public RuntimeCollisionMutationResult WithdrawCollision(
uint landblockId)
=> AdvanceCollisionRetirementMutation(
landblockId,
RuntimeCollisionPrefixMutationKind.Withdrawal);
private RuntimeCollisionMutationResult AdvanceCollisionRetirementMutation(
uint landblockId,
RuntimeCollisionPrefixMutationKind kind)
{ {
EnsureNotDisposed(); EnsureNotDisposed();
EnsureCollisionMutationThread(); EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId); uint canonical = CanonicalLandblock(landblockId);
bool resident = Engine.IsLandblockTerrainResident(canonical); if (canonical == 0u)
InvalidateCollisionAdmission(canonical); throw new ArgumentOutOfRangeException(nameof(landblockId));
bool suppressOwnerJournal = _suppressCollisionOwnerJournal; if (kind is RuntimeCollisionPrefixMutationKind.Activation)
_suppressCollisionOwnerJournal = true; throw new ArgumentOutOfRangeException(nameof(kind));
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));
}
public RuntimeCollisionAcknowledgement WithdrawCollision( if (!_collisionPrefixMutations.TryGetValue(
uint landblockId) canonical,
{ out RuntimeCollisionPrefixMutation? mutation))
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
bool resident = Engine.IsLandblockTerrainResident(canonical);
InvalidateCollisionAdmission(canonical);
bool suppressOwnerJournal = _suppressCollisionOwnerJournal;
_suppressCollisionOwnerJournal = true;
try
{ {
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, bool completed = SetPosition.ReleaseCollisionPrefixAfterMutation(
_collisionGenerations[canonical], mutation.Quiescence,
resident, mutation.TargetGeneration,
Ready: false); mutation.Ready);
if (completed)
{
_collisionPrefixMutations.Remove(canonical);
TrimCollisionOwnerJournal();
}
return new RuntimeCollisionMutationResult(
new RuntimeCollisionAcknowledgement(
canonical,
mutation.TargetGeneration,
mutation.WasResident,
mutation.Ready),
completed);
} }
public void Dispose() public void Dispose()
@ -2419,6 +2762,7 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRemotes.Clear(); _spatialRemotes.Clear();
_spatialProjectiles.Clear(); _spatialProjectiles.Clear();
_spatialRoots.Clear(); _spatialRoots.Clear();
_collisionPrefixMutations.Clear();
_collisionAdmissions.Clear(); _collisionAdmissions.Clear();
_collisionGenerations.Clear(); _collisionGenerations.Clear();
CellCommitted = null; 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) internal bool TryPrepareSpatialRootAdmission(RuntimeEntityRecord record)
{ {
EnsureNotDisposed(); EnsureNotDisposed();
@ -2960,7 +3275,13 @@ public sealed class RuntimePhysicsState : IDisposable
{ {
return; 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) if (subscribers.Count == 0)
_collisionOwnerSubscribers.Remove(ownerId); _collisionOwnerSubscribers.Remove(ownerId);
} }

View file

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

View file

@ -2,9 +2,13 @@ using System.Collections.Immutable;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
using DatReaderWriter.Types; using DatReaderWriter.Types;
@ -19,6 +23,91 @@ public sealed class LandblockPhysicsPublisherTests
private static readonly float[] HeightTable = private static readonly float[] HeightTable =
Enumerable.Range(0, 256).Select(index => (float)index).ToArray(); Enumerable.Range(0, 256).Select(index => (float)index).ToArray();
[Fact]
public void ReplacementYieldsAtHeldWithdrawAndPlaceWithoutPostEngineReseal()
{
using var lifetime = new RuntimeEntityObjectLifetime(
new PhysicsDataCache());
RuntimePhysicsState physics = lifetime.Physics;
var publisher = new LandblockPhysicsPublisher(physics, HeightTable);
Publish(publisher, Build(FirstLandblock));
const uint guid = 0x70004101u;
const uint cell = 0xA9B40001u;
Vector3 position = new(10f, 10f, 0f);
RuntimeEntityRecord record = lifetime.RegisterEntity(
RuntimeSpawn(guid, cell, position)).Canonical!;
lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
lifetime.Entities.SetFullCell(record, cell, FirstLandblock);
var body = new PhysicsBody
{
Position = position,
Orientation = Quaternion.Identity,
LastUpdateTime = 1d,
State = PhysicsStateFlags.Gravity,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(cell, position, position);
lifetime.Entities.SetPhysicsBody(record, body);
record.ObjectClock.Activate();
physics.AcknowledgeSpatialProjection(record, spatial: true);
RuntimePlacementProjectionToken seeded = SeedRuntimePlacement(
physics,
record,
cell,
position);
Assert.True(physics.SetPosition.AcknowledgeProjection(seeded));
LandblockPhysicsPublication receipt = Begin(
publisher,
Build(FirstLandblock));
Assert.False(publisher.CompletePublication(receipt));
Assert.False(receipt.EngineMutationCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
Assert.False(publisher.CompletePublication(receipt));
Assert.True(receipt.EngineMutationCommitted);
Assert.True(receipt.SealCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot placement));
Assert.Equal(RuntimePlacementProjectionKind.Place, placement.Kind);
Assert.False(publisher.CompletePublication(receipt));
Assert.True(receipt.SealCommitted);
Assert.True(receipt.EngineMutationCommitted);
Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token));
Assert.True(publisher.CompletePublication(receipt));
Assert.True(receipt.CompletionCommitted);
LandblockPhysicsPublication cancelled = Begin(
publisher,
Build(FirstLandblock));
Assert.False(publisher.CompletePublication(cancelled));
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot cancelWithdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(
cancelWithdrawal.Token));
Assert.False(publisher.CompletePublication(cancelled));
Assert.True(cancelled.EngineMutationCommitted);
Assert.True(cancelled.SealCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot cancelPlacement));
Assert.False(cancelled.TryCancel());
Assert.True(cancelled.CancellationRequested);
Assert.True(cancelled.SealCommitted);
Assert.True(physics.SetPosition.AcknowledgeProjection(
cancelPlacement.Token));
Assert.True(cancelled.TryCancel());
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact] [Fact]
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput() public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
{ {
@ -821,6 +910,151 @@ public sealed class LandblockPhysicsPublisherTests
LandblockBuild build) => LandblockBuild build) =>
publisher.BeginPublication(RenderReceipt(build)); publisher.BeginPublication(RenderReceipt(build));
private static RuntimePlacementProjectionToken SeedRuntimePlacement(
RuntimePhysicsState physics,
RuntimeEntityRecord record,
uint cell,
Vector3 position)
{
Type coreAssemblyMarker = typeof(PhysicsEngine);
Type requestType = coreAssemblyMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionRequest",
throwOnError: true)!;
Type flagsType = coreAssemblyMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionFlags",
throwOnError: true)!;
Type placementClassType = coreAssemblyMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsPlacementClass",
throwOnError: true)!;
object request = Activator.CreateInstance(
requestType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
position,
Quaternion.Identity,
cell,
position,
ImmutableArray<FlatCollisionSphere>.Empty,
1f,
0.4f,
0.4f,
PhysicsStateFlags.None,
ObjectInfoState.None,
0u,
Enum.ToObject(placementClassType, 0),
Enum.ToObject(flagsType, 0x011u),
Vector3.Zero,
0f,
0f,
0u,
cell,
],
culture: null)!;
Type runtimeAssemblyMarker = typeof(RuntimePhysicsState);
Type commandType = runtimeAssemblyMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionCommand",
throwOnError: true)!;
Type kindType = runtimeAssemblyMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionOperationKind",
throwOnError: true)!;
object command = Activator.CreateInstance(
commandType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
request,
Enum.ToObject(kindType, 2),
10d,
0UL,
0f,
0f,
default(RuntimePortalPlacementAuthority),
],
culture: null)!;
MethodInfo apply = physics.SetPosition.GetType().GetMethod(
"Apply",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException("Runtime SetPosition.Apply");
object outcome = apply.Invoke(
physics.SetPosition,
[record, record.PositionAuthorityVersion, command])!;
return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty(
"Projection",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?.GetValue(outcome)
?? throw new MissingMemberException("Runtime placement projection"));
}
private static WorldSession.EntitySpawn RuntimeSpawn(
uint guid,
uint cell,
Vector3 position)
{
var serverPosition = new CreateObject.ServerPosition(
cell,
position.X,
position.Y,
position.Z,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: 1);
var spawnPhysics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.Gravity,
Position: serverPosition,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
serverPosition,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"app-collision-publication-fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.Gravity,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: spawnPhysics);
}
private static LandblockRenderPublication RenderReceipt(LandblockBuild build) private static LandblockRenderPublication RenderReceipt(LandblockBuild build)
{ {
var publisher = new LandblockRenderPublisher( var publisher = new LandblockRenderPublisher(

View file

@ -9,6 +9,106 @@ namespace AcDream.App.Tests.Streaming;
public sealed class LandblockRetirementCoordinatorTests public sealed class LandblockRetirementCoordinatorTests
{ {
[Fact]
public void PendingPhysicsStageYieldsBeforeTerrainAndRemainsRetryable()
{
const uint landblockId = 0x2020FFFFu;
var ticket = new LandblockRetirementTicket(
new GpuLandblockRetirement(
landblockId,
LandblockRetirementKind.Full,
Array.Empty<WorldEntity>()),
LandblockRetirementStage.Physics
| LandblockRetirementStage.Terrain);
int physicsPolls = 0;
int terrainPolls = 0;
Assert.Equal(
LandblockRetirementOperationResult.Pending,
ticket.RunOnceStep(
LandblockRetirementStage.Physics,
() =>
{
physicsPolls++;
return false;
}));
Assert.Equal(LandblockRetirementStage.Physics, ticket.NextIncompleteStage);
Assert.Equal(1, physicsPolls);
Assert.Equal(0, terrainPolls);
Assert.Equal(
LandblockRetirementOperationResult.Progressed,
ticket.RunOnceStep(
LandblockRetirementStage.Physics,
() =>
{
physicsPolls++;
return true;
}));
Assert.Equal(LandblockRetirementStage.Terrain, ticket.NextIncompleteStage);
Assert.True(ticket.RunOnce(
LandblockRetirementStage.Terrain,
() => terrainPolls++));
Assert.True(ticket.IsComplete);
Assert.Equal(2, physicsPolls);
Assert.Equal(1, terrainPolls);
}
[Fact]
public void BudgetedPendingPhysicsPollsExactlyOncePerAdvance()
{
const uint landblockId = 0x2023FFFFu;
var state = StateWith(landblockId);
int physicsPolls = 0;
int terrainPolls = 0;
bool acknowledgePhysics = false;
LandblockRetirementOperationResult AdvancePresentation(
LandblockRetirementTicket ticket) =>
ticket.NextIncompleteStage switch
{
LandblockRetirementStage.Physics => ticket.RunOnceStep(
LandblockRetirementStage.Physics,
() =>
{
physicsPolls++;
return acknowledgePhysics;
}),
LandblockRetirementStage.Terrain => ticket.RunOnceStep(
LandblockRetirementStage.Terrain,
() => terrainPolls++),
{ } stage => ticket.RunOnceStep(stage, () => { }),
};
LandblockRetirementCoordinator coordinator =
LandblockRetirementCoordinator.CreateBudgeted(
state,
AdvancePresentation,
ticket =>
{
while (!ticket.IsComplete)
_ = AdvancePresentation(ticket);
});
coordinator.BeginFull(landblockId);
var first = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
coordinator.Advance(first);
first.FinishFrame();
Assert.Equal(1, physicsPolls);
Assert.Equal(0, terrainPolls);
Assert.Equal(0, first.Snapshot.FailureCount);
Assert.Equal(1, coordinator.PendingCount);
acknowledgePhysics = true;
var second = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
coordinator.Advance(second);
second.FinishFrame();
Assert.Equal(2, physicsPolls);
Assert.Equal(1, terrainPolls);
Assert.Equal(0, second.Snapshot.FailureCount);
Assert.Equal(0, coordinator.PendingCount);
}
[Fact] [Fact]
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity() public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
{ {

View file

@ -1,6 +1,8 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Net; using System.Net;
using System.Numerics; using System.Numerics;
using System.Reflection;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
@ -605,7 +607,7 @@ public sealed class HeadlessSessionHostTests
using var lifetime = new RuntimeEntityObjectLifetime(); using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics; RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu; const uint landblockId = 0xA9B4FFFFu;
_ = HeadlessCollisionGenerationTransaction.Execute( CompleteCollisionGeneration(
physics, physics,
landblockId, landblockId,
afterAdmission: null, afterAdmission: null,
@ -616,7 +618,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f))); CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() => Assert.Throws<FixtureCollisionPublicationException>(() =>
HeadlessCollisionGenerationTransaction.Execute( CompleteCollisionGeneration(
physics, physics,
landblockId, landblockId,
_ => throw new FixtureCollisionPublicationException(), _ => throw new FixtureCollisionPublicationException(),
@ -629,13 +631,137 @@ public sealed class HeadlessSessionHostTests
Assert.Equal(0, ownership.CollisionAdmissionCount); Assert.Equal(0, ownership.CollisionAdmissionCount);
} }
[Fact]
public void CollisionTransactionYieldsTheFirstNonterminalRuntimePoll()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
HeadlessCollisionGenerationAdvance advance;
do
{
advance = transaction.Advance();
Assert.True(advance.Progressed);
}
while (!advance.YieldToCaller);
Assert.False(advance.Completed);
Assert.False(advance.WaitingForProjectionAcknowledgement);
Assert.False(transaction.CompletionCommitted);
do
{
advance = transaction.Advance();
Assert.True(advance.Progressed);
}
while (!advance.Completed && !advance.YieldToCaller);
if (!advance.Completed)
advance = transaction.Advance();
Assert.True(advance.Completed);
Assert.True(transaction.EngineMutationCommitted);
Assert.True(transaction.CompletionCommitted);
Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount);
}
[Fact]
public void CollisionTransactionRetainsPostEngineCancellationUntilPlaceAck()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 10f)));
const uint guid = 0x70004201u;
const uint cell = 0xA9B40001u;
Vector3 position = new(10f, 10f, 0f);
RuntimeEntityRecord record = lifetime.RegisterEntity(
Spawn(guid)).Canonical!;
lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
lifetime.Entities.SetFullCell(record, cell, landblockId);
var body = new PhysicsBody
{
Position = position,
Orientation = Quaternion.Identity,
LastUpdateTime = 1d,
State = PhysicsStateFlags.Gravity,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(cell, position, position);
lifetime.Entities.SetPhysicsBody(record, body);
record.ObjectClock.Activate();
physics.AcknowledgeSpatialProjection(record, spatial: true);
RuntimePlacementProjectionToken seeded = SeedRuntimePlacement(
physics,
record,
cell,
position);
Assert.True(physics.SetPosition.AcknowledgeProjection(seeded));
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId, 20f)));
HeadlessCollisionGenerationAdvance advance;
do
{
advance = transaction.Advance();
}
while (!advance.WaitingForProjectionAcknowledgement);
Assert.False(transaction.EngineMutationCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
do
{
advance = transaction.Advance();
}
while (!advance.WaitingForProjectionAcknowledgement);
Assert.True(transaction.EngineMutationCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot placement));
Assert.False(transaction.TryCancel());
Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token));
Assert.True(transaction.TryCancel());
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact] [Fact]
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld() public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
{ {
using var lifetime = new RuntimeEntityObjectLifetime(); using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics; RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu; const uint landblockId = 0xA9B4FFFFu;
_ = HeadlessCollisionGenerationTransaction.Execute( CompleteCollisionGeneration(
physics, physics,
landblockId, landblockId,
afterAdmission: null, afterAdmission: null,
@ -646,7 +772,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f))); CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() => Assert.Throws<FixtureCollisionPublicationException>(() =>
HeadlessCollisionGenerationTransaction.Execute( CompleteCollisionGeneration(
physics, physics,
landblockId, landblockId,
afterAdmission: null, afterAdmission: null,
@ -714,6 +840,107 @@ public sealed class HeadlessSessionHostTests
runtime.MovementOwner.Controller = controller; runtime.MovementOwner.Controller = controller;
} }
private static void CompleteCollisionGeneration(
RuntimePhysicsState physics,
uint landblockId,
Action<RuntimeCollisionAdmission>? afterAdmission,
Action<RuntimeCollisionAdmission,
PreparedLandblockCollisionGeneration> stage)
{
HeadlessCollisionGenerationTransaction transaction =
HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission,
stage);
while (true)
{
HeadlessCollisionGenerationAdvance advance = transaction.Advance();
if (advance.Completed)
return;
Assert.True(advance.Progressed);
Assert.False(advance.WaitingForProjectionAcknowledgement);
}
}
private static RuntimePlacementProjectionToken SeedRuntimePlacement(
RuntimePhysicsState physics,
RuntimeEntityRecord record,
uint cell,
Vector3 position)
{
Type coreMarker = typeof(PhysicsEngine);
Type requestType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionRequest",
throwOnError: true)!;
Type flagsType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsSetPositionFlags",
throwOnError: true)!;
Type placementClassType = coreMarker.Assembly.GetType(
"AcDream.Core.Physics.PhysicsPlacementClass",
throwOnError: true)!;
object request = Activator.CreateInstance(
requestType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
position,
Quaternion.Identity,
cell,
position,
ImmutableArray<FlatCollisionSphere>.Empty,
1f,
0.4f,
0.4f,
PhysicsStateFlags.None,
ObjectInfoState.None,
0u,
Enum.ToObject(placementClassType, 0),
Enum.ToObject(flagsType, 0x011u),
Vector3.Zero,
0f,
0f,
0u,
cell,
],
culture: null)!;
Type runtimeMarker = typeof(RuntimePhysicsState);
Type commandType = runtimeMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionCommand",
throwOnError: true)!;
Type kindType = runtimeMarker.Assembly.GetType(
"AcDream.Runtime.Physics.RuntimeSetPositionOperationKind",
throwOnError: true)!;
object command = Activator.CreateInstance(
commandType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
request,
Enum.ToObject(kindType, 2),
10d,
0UL,
0f,
0f,
default(RuntimePortalPlacementAuthority),
],
culture: null)!;
MethodInfo apply = physics.SetPosition.GetType().GetMethod(
"Apply",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException("Runtime SetPosition.Apply");
object outcome = apply.Invoke(
physics.SetPosition,
[record, record.PositionAuthorityVersion, command])!;
return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty(
"Projection",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?.GetValue(outcome)
?? throw new MissingMemberException("Runtime placement projection"));
}
private static RuntimeLandblockCollisionAssets CollisionAssets( private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId, uint landblockId,
float terrainHeight) float terrainHeight)

View file

@ -66,7 +66,33 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
.IsExactPreparedPlacementCurrent( .IsExactPreparedPlacementCurrent(
fixture.Record, fixture.Record,
fixture.Placement, fixture.Placement,
fixture.Command)); fixture.Command));
}
[Fact]
public void DormantOwnershipPreflightFailurePublishesNoCanonicalOwner()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken publication =
fixture.Prepare();
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
var unownedBody = new PhysicsBody();
Assert.Throws<InvalidOperationException>(() => fixture.Lifetime.Physics
.SetPosition.PrepareDormantLocalActivationOwnership(
fixture.Record,
unownedBody,
publication.Placement));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.False(unownedBody.InWorld);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
} }
[Fact] [Fact]
@ -2467,32 +2493,48 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
RuntimeCollisionAdmission admission, RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared) PreparedLandblockCollisionGeneration prepared)
{ {
while (true) bool engineCommitted = false;
for (int poll = 0; poll < 10_000; poll++)
{ {
while (!physics.AdvanceCollisionRetainedOwnerCapture( if (!engineCommitted)
admission,
prepared).Completed)
{ {
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
continue;
} }
foreach (uint ownerId in prepared.RetainedOwnerIds) RuntimeCollisionGenerationCommit result =
physics.CommitCollisionGeneration(admission, prepared);
if (result.Completed)
return result;
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{ {
physics.RefreshCollisionRetainedOwner( Assert.True(physics.SetPosition.AcknowledgeProjection(
admission, projection.Token));
prepared,
ownerId);
} }
RuntimeCollisionSealStep seal; engineCommitted = result.EngineCommitted;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (seal.Completed)
break;
} }
return physics.CommitCollisionGeneration(admission, prepared); throw new InvalidOperationException(
"Collision generation did not complete its Runtime mutation transaction.");
} }
private static RuntimeLandblockCollisionAssets CollisionAssets( private static RuntimeLandblockCollisionAssets CollisionAssets(

View file

@ -0,0 +1,479 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Physics;
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
{
[Fact]
public void ActivationWaitsForExactWithdrawAndPlaceReceipts()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimeEntityRecord record = fixture.Add(
0x70003101u,
1,
CellP,
new Vector3(11f, 40f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(11.5f, 40f, 0f));
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
int committedNotifications = 0;
physics.CollisionGenerationCommitted += _ => committedNotifications++;
RuntimeCollisionGenerationCommit first =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(first.EngineCommitted);
Assert.False(first.Completed);
Assert.False(physics.IsSpatialRoot(record));
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawn));
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawn.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawn.Token));
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit transferred =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(transferred.EngineCommitted);
Assert.False(transferred.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
RuntimeCollisionGenerationCommit completed =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(completed.EngineCommitted);
Assert.True(completed.Completed);
Assert.True(physics.IsSpatialRoot(record));
Assert.Equal(1, committedNotifications);
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
}
[Fact]
public void DemotionParksOnlyIndoorResidentsAndLeavesOutdoorPresentationLive()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimeEntityRecord outdoor = fixture.Add(
0x70003102u,
1,
CellP,
new Vector3(12f, 41f, 0f));
RuntimeEntityRecord indoor = fixture.Add(
0x70003103u,
1,
PrefixP | 0x0100u,
new Vector3(13f, 41f, 0f));
RuntimeCollisionMutationResult first =
fixture.Lifetime.Physics.DemoteCollisionToTerrain(PrefixP);
Assert.False(first.Completed);
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(indoor));
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.Equal(indoor.Key, withdrawal.Token.Entity);
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(withdrawal.Token));
RuntimeCollisionMutationResult completed =
fixture.Lifetime.Physics.DemoteCollisionToTerrain(PrefixP);
Assert.True(completed.Completed);
Assert.True(completed.Ready);
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(indoor));
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(out _));
}
[Fact]
public void WithdrawalLeavesResidentUnboundUntilLaterGenerationWakesIt()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimeEntityRecord record = fixture.Add(
0x70003104u,
1,
CellP,
new Vector3(14f, 42f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(14.5f, 42f, 0f));
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionMutationResult withdrawal =
physics.WithdrawCollision(PrefixP);
Assert.False(withdrawal.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot removed));
Assert.True(physics.SetPosition.AcknowledgeProjection(removed.Token));
withdrawal = physics.WithdrawCollision(PrefixP);
Assert.True(withdrawal.Completed);
Assert.False(withdrawal.Ready);
Assert.False(physics.IsSpatialRoot(record));
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
RuntimeCollisionGenerationCommit pending =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(pending.Completed);
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit completed =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(completed.EngineCommitted);
Assert.False(completed.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
completed = physics.CommitCollisionGeneration(admission, prepared);
Assert.True(completed.Completed);
Assert.True(physics.IsSpatialRoot(record));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void SessionResetClearsPreAndPostEngineMutationDebt(bool postEngine)
{
using var fixture = new Fixture(bindGeneration: true);
RuntimeEntityRecord record = fixture.Add(
0x70003105u,
1,
CellP,
new Vector3(15f, 43f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(15.5f, 43f, 0f));
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Completed);
if (postEngine)
{
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(
withdrawal.Token));
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit transferred =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(transferred.EngineCommitted);
Assert.False(transferred.Completed);
}
_ = fixture.Lifetime.BeginSessionClear();
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CommittedCollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CancelledActivationCannotCommitWhileRestoreAckIsPending()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission baselineAdmission =
physics.BeginCollisionAdmission(PrefixP);
using (PreparedLandblockCollisionGeneration baseline =
PrepareSealedMutation(physics, baselineAdmission, PrefixP))
{
Assert.False(physics.CommitCollisionGeneration(
baselineAdmission,
baseline).Completed);
_ = SealMutation(physics, baselineAdmission, baseline);
Assert.True(physics.CommitCollisionGeneration(
baselineAdmission,
baseline).Completed);
}
RuntimeEntityRecord record = fixture.Add(
0x70003106u,
1,
CellP,
new Vector3(16f, 44f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(16.5f, 44f, 0f));
Assert.True(physics.SetPosition.AcknowledgeProjection(
seeded.Projection));
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
RuntimeCollisionGenerationCommit parked =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(parked.EngineCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
Assert.False(physics.CancelCollisionGeneration(admission, prepared));
RuntimeCollisionGenerationCommit forbidden =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(forbidden.EngineCommitted);
Assert.False(forbidden.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
Assert.True(physics.IsSpatialRoot(record));
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount);
}
[Fact]
public void SupersededAdmissionCancellationRestoresExactBaselineGeneration()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission baseline =
physics.BeginCollisionAdmission(PrefixP);
using (PreparedLandblockCollisionGeneration preparedBaseline =
PrepareSealedMutation(physics, baseline, PrefixP))
{
Assert.False(physics.CommitCollisionGeneration(
baseline,
preparedBaseline).Completed);
Assert.True(physics.CommitCollisionGeneration(
baseline,
preparedBaseline).Completed);
}
RuntimeEntityRecord record = fixture.Add(
0x70003107u,
1,
CellP,
new Vector3(17f, 45f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(17.5f, 45f, 0f));
Assert.True(physics.SetPosition.AcknowledgeProjection(
seeded.Projection));
RuntimeCollisionAdmission admissionA =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration preparedA =
PrepareSealedMutation(physics, admissionA, PrefixP);
RuntimeCollisionAdmission admissionB =
physics.BeginCollisionAdmission(PrefixP);
Assert.Equal(baseline.Generation, admissionB.PreviousGeneration);
using PreparedLandblockCollisionGeneration preparedB =
PrepareSealedMutation(physics, admissionB, PrefixP);
RuntimeCollisionGenerationCommit parked =
physics.CommitCollisionGeneration(admissionB, preparedB);
Assert.False(parked.EngineCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.False(physics.CancelCollisionGeneration(admissionB, preparedB));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
Assert.False(physics.CancelCollisionGeneration(admissionB, preparedB));
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
Assert.True(physics.CancelCollisionGeneration(admissionB, preparedB));
Assert.True(physics.IsSpatialRoot(record));
Assert.Equal(restored.Token.ExactCellId, record.FullCellId);
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CancellationAfterEngineCommitFinishesExactPlaceAckWithoutRollback()
{
using var fixture = new Fixture(bindGeneration: true);
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission baseline =
physics.BeginCollisionAdmission(PrefixP);
using (PreparedLandblockCollisionGeneration preparedBaseline =
PrepareSealedMutation(physics, baseline, PrefixP))
{
Assert.False(physics.CommitCollisionGeneration(
baseline,
preparedBaseline).Completed);
Assert.True(physics.CommitCollisionGeneration(
baseline,
preparedBaseline).Completed);
}
RuntimeEntityRecord record = fixture.Add(
0x70003108u,
1,
CellP,
new Vector3(18f, 46f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(18.5f, 46f, 0f));
Assert.True(physics.SetPosition.AcknowledgeProjection(
seeded.Projection));
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
int committedNotifications = 0;
physics.CollisionGenerationCommitted += _ => committedNotifications++;
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit transferred =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(transferred.EngineCommitted);
Assert.False(transferred.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.False(physics.CancelCollisionGeneration(admission, prepared));
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
Assert.Equal(0, committedNotifications);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
Assert.True(physics.IsSpatialRoot(record));
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
Assert.Equal(1, committedNotifications);
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void ColdFirstGenerationCancellationReleasesToUnavailableWithoutDebt()
{
// The injected-engine constructor is an internal fixture seam. It can
// begin with collision resident before Runtime has assigned generation
// one; production Runtime constructs an empty PhysicsEngine.
using var fixture = new Fixture(bindGeneration: true);
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeEntityRecord record = fixture.Add(
0x70003109u,
1,
CellP,
new Vector3(19f, 47f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CellP,
new Vector3(19.5f, 47f, 0f));
Assert.True(physics.SetPosition.AcknowledgeProjection(
seeded.Projection));
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(PrefixP);
Assert.Equal(0UL, admission.PreviousGeneration);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, PrefixP);
RuntimeCollisionGenerationCommit parked =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(parked.EngineCommitted);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token));
Assert.True(physics.CancelCollisionGeneration(admission, prepared));
Assert.True(physics.Engine.IsLandblockTerrainResident(PrefixP));
Assert.False(physics.IsSpatialRoot(record));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
RuntimeSetPositionOwnershipSnapshot placement =
physics.SetPosition.CaptureOwnership();
Assert.Equal(1, placement.UnboundDeferredCellCount);
Assert.Equal(0, placement.DeferredBucketCount);
}
private static PreparedLandblockCollisionGeneration PrepareSealedMutation(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
uint landblock)
{
PreparedLandblockCollisionGeneration prepared =
physics.PrepareCollisionGeneration(admission);
physics.StageCollisionAssets(
admission,
prepared,
new RuntimeLandblockCollisionAssets(
landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f,
0u));
_ = SealMutation(physics, admission, prepared);
return prepared;
}
private static uint[] SealMutation(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
while (true)
{
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
physics.RefreshCollisionRetainedOwner(admission, prepared, ownerId);
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (seal.Completed)
return [.. prepared.RetainedOwnerIds];
}
}
}

View file

@ -10,7 +10,7 @@ using DatReaderWriter.Types;
namespace AcDream.Runtime.Tests.Physics; namespace AcDream.Runtime.Tests.Physics;
public sealed class RuntimeCollisionPrefixQuiescenceTests public sealed partial class RuntimeCollisionPrefixQuiescenceTests
{ {
private const uint PrefixP = 0xA9B40000u; private const uint PrefixP = 0xA9B40000u;
private const uint CellP = PrefixP | 0x0001u; private const uint CellP = PrefixP | 0x0001u;
@ -68,7 +68,10 @@ public sealed class RuntimeCollisionPrefixQuiescenceTests
Assert.True(fixture.Lifetime.Physics Assert.True(fixture.Lifetime.Physics
.IsCollisionPrefixMutationPermissionCurrent(permission)); .IsCollisionPrefixMutationPermissionCurrent(permission));
Assert.True(fixture.Lifetime.Physics Assert.True(fixture.Lifetime.Physics
.CompleteCollisionPrefixQuiescence(permission)); .CancelCollisionPrefixQuiescence(
token,
successorGeneration: 2UL,
successorReady: false));
Assert.False(fixture.Lifetime.Physics Assert.False(fixture.Lifetime.Physics
.IsCollisionPrefixMutationPermissionCurrent(permission)); .IsCollisionPrefixMutationPermissionCurrent(permission));
} }

View file

@ -0,0 +1,44 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Tests.Physics;
public sealed class RuntimePhysicsOwnershipBoundaryTests
{
[Fact]
public void PhysicsWorldRootMutatorsAreRuntimeOnlyProductionInternals()
{
string[] rootMutators =
[
nameof(PhysicsEngine.AddLandblock),
nameof(PhysicsEngine.RemoveLandblock),
nameof(PhysicsEngine.DemoteLandblockToTerrain),
nameof(PhysicsEngine.Clear),
];
Type engine = typeof(PhysicsEngine);
MethodInfo[] publicMethods = engine.GetMethods(
BindingFlags.Instance | BindingFlags.Public);
MethodInfo[] internalMethods = engine.GetMethods(
BindingFlags.Instance | BindingFlags.NonPublic);
foreach (string name in rootMutators)
{
Assert.DoesNotContain(
publicMethods,
method => method.Name == name);
Assert.Contains(
internalMethods,
method => method.Name == name);
}
string[] friends = engine.Assembly
.GetCustomAttributes<InternalsVisibleToAttribute>()
.Select(attribute => attribute.AssemblyName.Split(',')[0])
.ToArray();
Assert.Contains("AcDream.Runtime", friends);
Assert.DoesNotContain("AcDream.App", friends);
Assert.DoesNotContain("acdream-headless", friends);
Assert.Contains("AcDream.App.Tests", friends);
Assert.Contains("AcDream.Headless.Tests", friends);
}
}

View file

@ -336,7 +336,8 @@ public sealed class RuntimePhysicsStateTests
CommitPrepared(first.Physics, newer, prepared)); CommitPrepared(first.Physics, newer, prepared));
RuntimeCollisionAcknowledgement withdrawn = RuntimeCollisionAcknowledgement withdrawn =
first.Physics.WithdrawCollision(0xA9B4FFFFu); CompleteWithdrawal(first.Physics, 0xA9B4FFFFu)
.Acknowledgement;
Assert.True(withdrawn.WasResident); Assert.True(withdrawn.WasResident);
Assert.True(withdrawn.Generation > completed.Generation); Assert.True(withdrawn.Generation > completed.Generation);
Assert.Equal(0, first.Physics.Engine.LandblockCount); Assert.Equal(0, first.Physics.Engine.LandblockCount);
@ -465,7 +466,8 @@ public sealed class RuntimePhysicsStateTests
uint owner = Assert.Single(SealPrepared(physics, admission, prepared)); uint owner = Assert.Single(SealPrepared(physics, admission, prepared));
physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u); physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f)); Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
@ -561,6 +563,11 @@ public sealed class RuntimePhysicsStateTests
Assert.True(sealSteps > ownerCount); Assert.True(sealSteps > ownerCount);
Assert.True(workUnits > ownerCount); Assert.True(workUnits > ownerCount);
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
_ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit = RuntimeCollisionGenerationCommit commit =
@ -662,7 +669,8 @@ public sealed class RuntimePhysicsStateTests
} }
while (!seal.Completed); while (!seal.Completed);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f)); Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
@ -742,7 +750,8 @@ public sealed class RuntimePhysicsStateTests
break; break;
} }
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
ShadowEntry[] entries = physics.Engine.ShadowObjects ShadowEntry[] entries = physics.Engine.ShadowObjects
@ -841,7 +850,8 @@ public sealed class RuntimePhysicsStateTests
} }
while (!seal.Completed); while (!seal.Completed);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
ShadowEntry[] entries = physics.Engine.ShadowObjects ShadowEntry[] entries = physics.Engine.ShadowObjects
@ -885,6 +895,10 @@ public sealed class RuntimePhysicsStateTests
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects; ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
int notifications = 0; int notifications = 0;
physics.CollisionGenerationCommitted += _ => notifications++; physics.CollisionGenerationCommitted += _ => notifications++;
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
_ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit = RuntimeCollisionGenerationCommit commit =
@ -1049,6 +1063,10 @@ public sealed class RuntimePhysicsStateTests
PhysicsDataCache cacheFacade = physics.DataCache; PhysicsDataCache cacheFacade = physics.DataCache;
CellGraph graphFacade = physics.DataCache.CellGraph; CellGraph graphFacade = physics.DataCache.CellGraph;
ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects; ShadowObjectRegistry shadowFacade = physics.Engine.ShadowObjects;
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
_ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit = RuntimeCollisionGenerationCommit commit =
@ -1106,9 +1124,15 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, firstAdmission, first); _ = SealPrepared(physics, firstAdmission, first);
_ = SealPrepared(physics, secondAdmission, second); _ = SealPrepared(physics, secondAdmission, second);
Assert.False(physics.CommitCollisionGeneration(
firstAdmission,
first).Committed);
_ = SealPrepared(physics, firstAdmission, first);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long firstBefore = GC.GetAllocatedBytesForCurrentThread(); long firstBefore = GC.GetAllocatedBytesForCurrentThread();
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
firstAdmission, firstAdmission,
first).Committed); first).Committed);
long firstAllocated = long firstAllocated =
@ -1118,10 +1142,15 @@ public sealed class RuntimePhysicsStateTests
Assert.True(physics.Engine.IsLandblockTerrainResident(firstLandblock)); Assert.True(physics.Engine.IsLandblockTerrainResident(firstLandblock));
Assert.False(physics.Engine.IsLandblockTerrainResident(secondLandblock)); Assert.False(physics.Engine.IsLandblockTerrainResident(secondLandblock));
_ = SealPrepared(physics, secondAdmission, second);
Assert.False(physics.CommitCollisionGeneration(
secondAdmission,
second).Committed);
_ = SealPrepared(physics, secondAdmission, second); _ = SealPrepared(physics, secondAdmission, second);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long secondBefore = GC.GetAllocatedBytesForCurrentThread(); long secondBefore = GC.GetAllocatedBytesForCurrentThread();
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
secondAdmission, secondAdmission,
second).Committed); second).Committed);
long secondAllocated = long secondAllocated =
@ -1172,7 +1201,8 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, firstAdmission, first); _ = SealPrepared(physics, firstAdmission, first);
_ = SealPrepared(physics, secondAdmission, second); _ = SealPrepared(physics, secondAdmission, second);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
firstAdmission, firstAdmission,
first).Committed); first).Committed);
physics.Engine.ShadowObjects.UpdatePosition( physics.Engine.ShadowObjects.UpdatePosition(
@ -1185,7 +1215,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u); seedCellId: 0x01010001u);
_ = SealPrepared(physics, secondAdmission, second); _ = SealPrepared(physics, secondAdmission, second);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
secondAdmission, secondAdmission,
second).Committed); second).Committed);
Assert.Equal(12f, Assert.Single( Assert.Equal(12f, Assert.Single(
@ -1256,11 +1287,13 @@ public sealed class RuntimePhysicsStateTests
_ = SealPrepared(physics, northAdmission, northPrepared); _ = SealPrepared(physics, northAdmission, northPrepared);
_ = SealPrepared(physics, southAdmission, southPrepared); _ = SealPrepared(physics, southAdmission, southPrepared);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
northAdmission, northAdmission,
northPrepared).Committed); northPrepared).Committed);
_ = SealPrepared(physics, southAdmission, southPrepared); _ = SealPrepared(physics, southAdmission, southPrepared);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
southAdmission, southAdmission,
southPrepared).Committed); southPrepared).Committed);
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock( Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@ -1394,7 +1427,8 @@ public sealed class RuntimePhysicsStateTests
physics.Engine.UpdatePlayerCurrCell(secondCell); physics.Engine.UpdatePlayerCurrCell(secondCell);
ObjCell oldRootCell = physics.DataCache.CellGraph.CurrCell!; ObjCell oldRootCell = physics.DataCache.CellGraph.CurrCell!;
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
@ -1454,7 +1488,8 @@ public sealed class RuntimePhysicsStateTests
destination, destination,
seedCellId: 0x02020001u); seedCellId: 0x02020001u);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.False(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock( Assert.False(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@ -1521,7 +1556,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x02020001u, seedCellId: 0x02020001u,
isStatic: false); isStatic: false);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
ShadowEntry entry = Assert.Single( ShadowEntry entry = Assert.Single(
@ -1692,8 +1728,8 @@ public sealed class RuntimePhysicsStateTests
preparedThird, preparedThird,
CollisionAssets(replacement)); CollisionAssets(replacement));
Assert.True(physics.DemoteCollisionToTerrain(demoted).WasResident); Assert.True(CompleteDemotion(physics, demoted).WasResident);
Assert.True(physics.WithdrawCollision(withdrawn).WasResident); Assert.True(CompleteWithdrawal(physics, withdrawn).WasResident);
Assert.True(CommitPrepared( Assert.True(CommitPrepared(
physics, physics,
third, third,
@ -1745,7 +1781,8 @@ public sealed class RuntimePhysicsStateTests
} }
_ = SealPrepared(physics, retiredAdmission, retiredPrepared); _ = SealPrepared(physics, retiredAdmission, retiredPrepared);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
retiredAdmission, retiredAdmission,
retiredPrepared).Committed); retiredPrepared).Committed);
if (beginRebase) if (beginRebase)
@ -1762,9 +1799,9 @@ public sealed class RuntimePhysicsStateTests
} }
if (withdraw) if (withdraw)
Assert.True(physics.WithdrawCollision(retired).WasResident); Assert.True(CompleteWithdrawal(physics, retired).WasResident);
else else
Assert.True(physics.DemoteCollisionToTerrain(retired).WasResident); Assert.True(CompleteDemotion(physics, retired).WasResident);
Assert.True(CommitPrepared( Assert.True(CommitPrepared(
physics, physics,
@ -1986,6 +2023,11 @@ public sealed class RuntimePhysicsStateTests
while (!seal.Completed); while (!seal.Completed);
Assert.Equal(ownerCount, worked); Assert.Equal(ownerCount, worked);
Assert.False(physics.CommitCollisionGeneration(
admission,
prepared).Committed);
_ = SealPrepared(physics, admission, prepared);
_ = GC.GetAllocatedBytesForCurrentThread(); _ = GC.GetAllocatedBytesForCurrentThread();
long before = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread();
RuntimeCollisionGenerationCommit commit = RuntimeCollisionGenerationCommit commit =
@ -2050,7 +2092,8 @@ public sealed class RuntimePhysicsStateTests
survivingTarget, survivingTarget,
seedCellId: 0x02020001u, seedCellId: 0x02020001u,
isStatic: false); isStatic: false);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
survivingAdmission, survivingAdmission,
surviving).Committed); surviving).Committed);
Assert.Contains( Assert.Contains(
@ -2097,7 +2140,8 @@ public sealed class RuntimePhysicsStateTests
0f, 0f,
target, target,
seedCellId: 0x01010001u); seedCellId: 0x01010001u);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock( Assert.True(physics.Engine.ShadowObjects.HasOwnerRowsInLandblock(
@ -2146,7 +2190,8 @@ public sealed class RuntimePhysicsStateTests
outside, outside,
seedCellId: 0x02020001u); seedCellId: 0x02020001u);
physics.Engine.ShadowObjects.UpdatePhysicsState(ownerId, 0x55u); physics.Engine.ShadowObjects.UpdatePhysicsState(ownerId, 0x55u);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
ShadowEntry entry = Assert.Single( ShadowEntry entry = Assert.Single(
@ -2217,7 +2262,7 @@ public sealed class RuntimePhysicsStateTests
replacementAdmission, replacementAdmission,
replacementPrepared); replacementPrepared);
Assert.True(physics.WithdrawCollision(retired).WasResident); Assert.True(CompleteWithdrawal(physics, retired).WasResident);
Assert.False(physics.CommitCollisionGeneration( Assert.False(physics.CommitCollisionGeneration(
replacementAdmission, replacementAdmission,
replacementPrepared).Committed); replacementPrepared).Committed);
@ -2226,7 +2271,8 @@ public sealed class RuntimePhysicsStateTests
physics, physics,
replacementAdmission, replacementAdmission,
replacementPrepared); replacementPrepared);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
replacementAdmission, replacementAdmission,
replacementPrepared).Committed); replacementPrepared).Committed);
Assert.False(physics.Engine.IsLandblockTerrainResident(retired)); Assert.False(physics.Engine.IsLandblockTerrainResident(retired));
@ -2254,7 +2300,8 @@ public sealed class RuntimePhysicsStateTests
uint ordinal = index + 0x1000u; uint ordinal = index + 0x1000u;
uint x = ordinal & 0xFFu; uint x = ordinal & 0xFFu;
uint y = (ordinal >> 8) & 0xFFu; uint y = (ordinal >> 8) & 0xFFu;
_ = physics.WithdrawCollision( _ = CompleteWithdrawal(
physics,
(x << 24) | (y << 16) | 0xFFFFu); (x << 24) | (y << 16) | 0xFFFFu);
} }
Assert.False(physics.CommitCollisionGeneration( Assert.False(physics.CommitCollisionGeneration(
@ -2272,7 +2319,8 @@ public sealed class RuntimePhysicsStateTests
Assert.True(++steps < 100_000); Assert.True(++steps < 100_000);
} }
while (!seal.Completed); while (!seal.Completed);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
} }
@ -2380,7 +2428,8 @@ public sealed class RuntimePhysicsStateTests
Assert.InRange(seal.WorkUnits, 0, 1); Assert.InRange(seal.WorkUnits, 0, 1);
} }
while (!seal.Completed); while (!seal.Completed);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug()); Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
@ -2448,7 +2497,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u, seedCellId: 0x01010001u,
isStatic: false); isStatic: false);
physics.Engine.ShadowObjects.Deregister(77u); physics.Engine.ShadowObjects.Deregister(77u);
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug()); Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
@ -2484,7 +2534,8 @@ public sealed class RuntimePhysicsStateTests
seedCellId: 0x01010001u, seedCellId: 0x01010001u,
isStatic: false); isStatic: false);
} }
Assert.True(physics.CommitCollisionGeneration( Assert.True(CompleteSealedCommit(
physics,
admission, admission,
prepared).Committed); prepared).Committed);
Assert.Equal( Assert.Equal(
@ -2903,7 +2954,55 @@ public sealed class RuntimePhysicsStateTests
PreparedLandblockCollisionGeneration prepared) PreparedLandblockCollisionGeneration prepared)
{ {
_ = SealPrepared(physics, admission, prepared); _ = SealPrepared(physics, admission, prepared);
return physics.CommitCollisionGeneration(admission, prepared); return CompleteSealedCommit(physics, admission, prepared);
}
private static RuntimeCollisionGenerationCommit CompleteSealedCommit(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
for (int poll = 0; poll < 10_000; poll++)
{
RuntimeCollisionGenerationCommit commit =
physics.CommitCollisionGeneration(admission, prepared);
if (commit.Committed)
return commit;
if (!commit.EngineCommitted)
_ = SealPrepared(physics, admission, prepared);
}
throw new InvalidOperationException(
"Collision generation did not complete its Runtime mutation transaction.");
}
private static RuntimeCollisionMutationResult CompleteDemotion(
RuntimePhysicsState physics,
uint landblockId)
{
for (int poll = 0; poll < 10_000; poll++)
{
RuntimeCollisionMutationResult result =
physics.DemoteCollisionToTerrain(landblockId);
if (result.Completed)
return result;
}
throw new InvalidOperationException(
"Collision demotion did not complete its Runtime mutation transaction.");
}
private static RuntimeCollisionMutationResult CompleteWithdrawal(
RuntimePhysicsState physics,
uint landblockId)
{
for (int poll = 0; poll < 10_000; poll++)
{
RuntimeCollisionMutationResult result =
physics.WithdrawCollision(landblockId);
if (result.Completed)
return result;
}
throw new InvalidOperationException(
"Collision withdrawal did not complete its Runtime mutation transaction.");
} }
private static uint[] SealPrepared( private static uint[] SealPrepared(

View file

@ -1750,8 +1750,6 @@ public sealed class RuntimeSetPositionStateTests
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement; RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind); Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId); Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
placed.Token));
RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership(); RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership();
Assert.Equal(0, final.DeferredSetPositionBucketCount); Assert.Equal(0, final.DeferredSetPositionBucketCount);
Assert.Equal(0, final.UnboundDeferredSetPositionCellCount); Assert.Equal(0, final.UnboundDeferredSetPositionCellCount);
@ -1872,10 +1870,6 @@ public sealed class RuntimeSetPositionStateTests
Assert.All(observer.Deltas, delta => Assert.Equal( Assert.All(observer.Deltas, delta => Assert.Equal(
RuntimePlacementProjectionKind.Place, RuntimePlacementProjectionKind.Place,
delta.Placement.Kind)); delta.Placement.Kind));
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
observer.Deltas[0].Placement.Token));
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
observer.Deltas[1].Placement.Token));
} }
[Theory] [Theory]
@ -2425,32 +2419,48 @@ public sealed class RuntimeSetPositionStateTests
RuntimeCollisionAdmission admission, RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared) PreparedLandblockCollisionGeneration prepared)
{ {
while (true) bool engineCommitted = false;
for (int poll = 0; poll < 10_000; poll++)
{ {
while (!physics.AdvanceCollisionRetainedOwnerCapture( if (!engineCommitted)
admission,
prepared).Completed)
{ {
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
continue;
} }
foreach (uint ownerId in prepared.RetainedOwnerIds) RuntimeCollisionGenerationCommit result =
physics.CommitCollisionGeneration(admission, prepared);
if (result.Completed)
return result;
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{ {
physics.RefreshCollisionRetainedOwner( Assert.True(physics.SetPosition.AcknowledgeProjection(
admission, projection.Token));
prepared,
ownerId);
} }
RuntimeCollisionSealStep seal; engineCommitted = result.EngineCommitted;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (seal.Completed)
break;
} }
return physics.CommitCollisionGeneration(admission, prepared); throw new InvalidOperationException(
"Collision generation did not complete its Runtime mutation transaction.");
} }
private static void AddSyntheticCell( private static void AddSyntheticCell(