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 bool RefloodCommitted { 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 CompletionCommitted { get; set; }
internal bool CancellationRequested { get; private set; }
public void Dispose()
{
if (!CompletionCommitted)
Physics.CancelCollisionGeneration(
CollisionAdmission,
PreparedGeneration);
if (!TryCancel())
{
throw new InvalidOperationException(
"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;
@ -246,9 +277,14 @@ public sealed class LandblockPhysicsPublisher
publication.SetupObjectIds);
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;
}
}
@ -416,7 +452,7 @@ public sealed class LandblockPhysicsPublisher
/// presentation pipeline supplies the static-presentation owner callback
/// that preserves per-entity light-before-collision order.
/// </summary>
public void CompletePublication(
public bool CompletePublication(
LandblockPhysicsPublication publication,
Action<WorldEntity>? beforeStaticCollision = null)
{
@ -426,7 +462,29 @@ public sealed class LandblockPhysicsPublisher
"Physics publication cannot complete before its prefix commits.");
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>
@ -438,11 +496,15 @@ public sealed class LandblockPhysicsPublisher
Action<WorldEntity>? beforeStaticCollision = null)
{
ValidateReceipt(publication);
if (publication.CancellationRequested)
throw new InvalidOperationException(
"A cancelled collision publication cannot resume.");
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return true;
publication.RuntimeMutationPending = false;
long started = Stopwatch.GetTimestamp();
LoadedLandblock landblock = publication.Build.Landblock;
@ -567,12 +629,20 @@ public sealed class LandblockPhysicsPublisher
_physics.CommitCollisionGeneration(
publication.CollisionAdmission,
publication.PreparedGeneration);
if (!commit.Committed)
publication.EngineMutationCommitted |= commit.EngineCommitted;
if (!commit.Completed)
{
// 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;
publication.RuntimeMutationPending = true;
if (!commit.EngineCommitted)
{
// 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;
return false;
}
@ -587,16 +657,54 @@ public sealed class LandblockPhysicsPublisher
return publication.CompletionCommitted;
}
public void DemoteToTerrain(uint landblockId)
public bool DemoteToTerrain(uint landblockId)
{
_physics.DemoteCollisionToTerrain(landblockId);
_demotionCount++;
for (int poll = 0; poll < 2; poll++)
{
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++;
return true;
}
private void PublishCell(
@ -1066,7 +1174,8 @@ public sealed class LandblockPhysicsPublisher
"The physics publication receipt belongs to another publisher.",
nameof(publication));
}
if (!publication.CompletionCommitted)
if (!publication.CompletionCommitted
&& !publication.EngineMutationCommitted)
{
ObjectDisposedException.ThrowIf(
publication.PreparedGeneration.IsDisposed,

View file

@ -217,14 +217,34 @@ public sealed class LandblockPresentationPipeline
/// <summary>
/// Cancels retained publication receipts during a generation reset. A
/// collision receipt owns only its private staging world until activation,
/// so cancellation cannot withdraw or partially replace the active world.
/// pre-engine receipt restores its exact prior generation. A receipt that
/// already transferred the engine generation remains retained until its
/// canonical post-engine placement acknowledgement suffix completes; the
/// committed transfer is never rolled back.
/// </summary>
internal void CancelPendingPublications()
internal bool CancelPendingPublications()
{
foreach (PublicationTransaction transaction in _publications.Values)
transaction.PhysicsPublication?.Dispose();
_publications.Clear();
if (_publications.Count == 0)
return true;
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)
@ -247,10 +267,15 @@ public sealed class LandblockPresentationPipeline
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);
}
internal void AdvancePriorityRetirement(
uint landblockId,
@ -261,14 +286,26 @@ public sealed class LandblockPresentationPipeline
{
_retirements.BeginFull(landblockId);
if (_retirements.UsesBudgetedSteps)
{
_retirements.Advance();
if (_retirements.IsPending(landblockId)
&& (_physicsPublisher?.CanContinueMutationSynchronously()
?? true))
_retirements.Advance();
}
}
public void BeginNearLayerRetirement(uint landblockId)
{
_retirements.BeginNearLayer(landblockId);
if (_retirements.UsesBudgetedSteps)
{
_retirements.Advance();
if (_retirements.IsPending(landblockId)
&& (_physicsPublisher?.CanContinueMutationSynchronously()
?? true))
_retirements.Advance();
}
}
internal void EnqueueFullRetirement(uint landblockId) =>
@ -754,6 +791,17 @@ public sealed class LandblockPresentationPipeline
{
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)
{

View file

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

View file

@ -12,8 +12,8 @@ public enum LandblockRetirementStage : ushort
EntityLighting = 1 << 3,
EntityTranslucency = 1 << 4,
PluginProjection = 1 << 5,
Terrain = 1 << 6,
Physics = 1 << 7,
Physics = 1 << 6,
Terrain = 1 << 7,
CellVisibility = 1 << 8,
BuildingRegistry = 1 << 9,
EnvironmentCells = 1 << 10,
@ -24,6 +24,7 @@ internal enum LandblockRetirementOperationResult : byte
{
NoWork,
Progressed,
Pending,
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(
LandblockRetirementStage stage,
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(
LandblockRetirementStage stage,
Func<WorldEntity, bool> predicate,
@ -815,6 +863,8 @@ public sealed class LandblockRetirementCoordinator
}
meter.Complete();
if (result == LandblockRetirementOperationResult.Pending)
return BudgetedAdvanceResult.Yielded;
return BudgetedAdvanceResult.Progressed;
}

View file

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

View file

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

View file

@ -18,9 +18,37 @@ internal interface IHeadlessCollisionNeighborhood
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,
uint landblockId,
Action<RuntimeCollisionAdmission>? afterAdmission,
@ -33,60 +61,103 @@ internal static class HeadlessCollisionGenerationTransaction
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId);
PreparedLandblockCollisionGeneration? prepared = null;
bool committed = false;
try
{
// This hook exists so the exact post-admission/pre-prepare failure
// boundary remains covered. Production does not install one.
afterAdmission?.Invoke(admission);
prepared = physics.PrepareCollisionGeneration(admission);
stage(admission, prepared);
RuntimeCollisionOwnerCaptureStep ownerCapture;
do
{
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;
return new HeadlessCollisionGenerationTransaction(
physics,
admission,
prepared);
}
finally
catch (Exception publicationError)
{
if (!committed)
physics.CancelCollisionGeneration(admission, prepared);
if (!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>
@ -97,10 +168,23 @@ internal static class HeadlessCollisionGenerationTransaction
internal sealed class HeadlessCollisionNeighborhood
: IHeadlessCollisionNeighborhood
{
private readonly record struct PublicationSpec(
uint LandblockId,
Vector3 Origin,
bool Required);
private readonly GameRuntime _runtime;
private readonly HeadlessProcessContentOwner
.HeadlessProcessContentLease _content;
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;
internal HeadlessCollisionNeighborhood(
@ -122,6 +206,21 @@ internal sealed class HeadlessCollisionNeighborhood
nameof(fullCellId),
"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
&& _resident.Contains(center)
&& _runtime.EntityObjects.Physics.Engine
@ -131,55 +230,17 @@ internal sealed class HeadlessCollisionNeighborhood
.UpdatePlayerCurrCell(fullCellId);
return;
}
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;
}
AdvanceWork();
}
public bool IsReady(uint fullCellId)
{
uint center = CanonicalLandblock(fullCellId);
if (_requestedCenterLandblock == center)
{
_requestedFullCell = fullCellId;
AdvanceWork();
}
if (_centerLandblock != center
|| !_resident.Contains(center)
|| !_runtime.EntityObjects.Physics.Engine
@ -192,7 +253,7 @@ internal sealed class HeadlessCollisionNeighborhood
.GetCellStruct(fullCellId) is not null;
}
private void PublishOne(
private HeadlessCollisionGenerationTransaction? CreatePublication(
uint landblockId,
Vector3 origin,
uint currentCellId,
@ -207,7 +268,7 @@ internal sealed class HeadlessCollisionNeighborhood
throw new InvalidDataException(
$"Required headless landblock 0x{landblockId:X8} is missing.");
}
return;
return null;
}
IReadOnlyList<WorldEntity> staticEntities =
@ -244,7 +305,7 @@ internal sealed class HeadlessCollisionNeighborhood
landblock);
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
_ = HeadlessCollisionGenerationTransaction.Execute(
return HeadlessCollisionGenerationTransaction.Begin(
physics,
landblockId,
afterAdmission: null,
@ -294,22 +355,113 @@ internal sealed class HeadlessCollisionNeighborhood
collisions,
origin);
});
_resident.Add(CanonicalLandblock(landblockId));
}
private void RetireAll()
private void AdvanceWork()
{
if (_resident.Count == 0)
if (_pendingPublicationCancellation
&& _pendingPublication is { } cancelling)
{
_centerLandblock = 0u;
return;
bool wasCommitted = cancelling.EngineMutationCommitted;
if (!cancelling.TryCancel())
return;
if (wasCommitted || cancelling.EngineMutationCommitted)
_resident.Add(cancelling.LandblockId);
_pendingPublication = null;
_pendingPublicationCancellation = false;
}
uint[] retiring = [.. _resident];
_resident.Clear();
_centerLandblock = 0u;
foreach (uint landblock in retiring)
_ = _runtime.EntityObjects.Physics.WithdrawCollision(landblock);
if (_resetRequired)
{
_retirementQueue.Clear();
foreach (uint landblock in _resident)
_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) =>

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,13 @@ using System.Collections.Immutable;
using System.Numerics;
using System.Reflection;
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
@ -19,6 +23,91 @@ public sealed class LandblockPhysicsPublisherTests
private static readonly float[] HeightTable =
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]
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
{
@ -821,6 +910,151 @@ public sealed class LandblockPhysicsPublisherTests
LandblockBuild 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)
{
var publisher = new LandblockRenderPublisher(

View file

@ -9,6 +9,106 @@ namespace AcDream.App.Tests.Streaming;
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]
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
{

View file

@ -1,6 +1,8 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Net;
using System.Numerics;
using System.Reflection;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@ -605,7 +607,7 @@ public sealed class HeadlessSessionHostTests
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
_ = HeadlessCollisionGenerationTransaction.Execute(
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@ -616,7 +618,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() =>
HeadlessCollisionGenerationTransaction.Execute(
CompleteCollisionGeneration(
physics,
landblockId,
_ => throw new FixtureCollisionPublicationException(),
@ -629,13 +631,137 @@ public sealed class HeadlessSessionHostTests
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]
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimePhysicsState physics = lifetime.Physics;
const uint landblockId = 0xA9B4FFFFu;
_ = HeadlessCollisionGenerationTransaction.Execute(
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@ -646,7 +772,7 @@ public sealed class HeadlessSessionHostTests
CollisionAssets(landblockId, 10f)));
Assert.Throws<FixtureCollisionPublicationException>(() =>
HeadlessCollisionGenerationTransaction.Execute(
CompleteCollisionGeneration(
physics,
landblockId,
afterAdmission: null,
@ -714,6 +840,107 @@ public sealed class HeadlessSessionHostTests
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(
uint landblockId,
float terrainHeight)

View file

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

View file

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