feat(runtime): atomically replace collision generations
This commit is contained in:
parent
99bf1751bb
commit
9b0f59bd1b
21 changed files with 2435 additions and 408 deletions
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue