feat(runtime): own deferred set-position residence

This commit is contained in:
Erik 2026-07-31 22:32:49 +02:00
parent e84a388e6f
commit 4c02ac4259
18 changed files with 5557 additions and 34 deletions

View file

@ -9,6 +9,20 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
int SpatialRootCount,
int SpatialRemoteCount,
int SpatialProjectileCount,
int SetPositionOperationCount,
int AwaitingSetPositionPreparationCount,
int DeferredSetPositionCount,
int PendingSetPositionHostAcknowledgementCount,
int LostCellDeadlineCount,
int LostCellDeadlineNodeCount,
int LostCellDeadlineIndexCount,
int ExpiredLostCellCount,
int ExpiredLostCellIndexCount,
int DeferredSetPositionBucketCount,
int DeferredSetPositionBucketOrderCount,
int UnboundDeferredSetPositionCellCount,
int UnboundDeferredSetPositionCellOrderCount,
int PreparedSetPositionMoverCount,
int CollisionAdmissionCount,
int CollisionGenerationCount,
bool OwnsProductionDataCache,
@ -21,6 +35,20 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& SpatialRootCount == 0
&& SpatialRemoteCount == 0
&& SpatialProjectileCount == 0
&& SetPositionOperationCount == 0
&& AwaitingSetPositionPreparationCount == 0
&& DeferredSetPositionCount == 0
&& PendingSetPositionHostAcknowledgementCount == 0
&& LostCellDeadlineCount == 0
&& LostCellDeadlineNodeCount == 0
&& LostCellDeadlineIndexCount == 0
&& ExpiredLostCellCount == 0
&& ExpiredLostCellIndexCount == 0
&& DeferredSetPositionBucketCount == 0
&& DeferredSetPositionBucketOrderCount == 0
&& UnboundDeferredSetPositionCellCount == 0
&& UnboundDeferredSetPositionCellOrderCount == 0
&& PreparedSetPositionMoverCount == 0
&& CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0
&& OwnsProductionDataCache;
@ -979,6 +1007,7 @@ internal readonly record struct RuntimeCollisionSealStep(
public sealed class RuntimePhysicsState : IDisposable
{
private readonly TimeProvider _timeProvider;
private readonly IGameRuntimeClock? _gameClock;
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
_spatialRoots = new();
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
@ -1020,10 +1049,12 @@ public sealed class RuntimePhysicsState : IDisposable
internal RuntimePhysicsState(
RuntimeEntityDirectory entities,
PhysicsDataCache? dataCache = null,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGameRuntimeClock? gameClock = null)
{
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
_timeProvider = timeProvider ?? TimeProvider.System;
_gameClock = gameClock;
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
Engine = new PhysicsEngine
{
@ -1032,15 +1063,18 @@ public sealed class RuntimePhysicsState : IDisposable
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
OnCollisionOwnerPrefixMembershipChanged;
SetPosition = new RuntimeSetPositionState(this, Entities);
}
internal RuntimePhysicsState(
RuntimeEntityDirectory entities,
PhysicsEngine engine,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGameRuntimeClock? gameClock = null)
{
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
_timeProvider = timeProvider ?? TimeProvider.System;
_gameClock = gameClock;
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
DataCache = engine.DataCache
?? PhysicsDataCache.CreateProduction(engine.CollisionWorld);
@ -1048,11 +1082,13 @@ public sealed class RuntimePhysicsState : IDisposable
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
OnCollisionOwnerPrefixMembershipChanged;
SetPosition = new RuntimeSetPositionState(this, Entities);
}
internal RuntimeEntityDirectory Entities { get; }
public PhysicsEngine Engine { get; }
public PhysicsDataCache DataCache { get; }
internal RuntimeSetPositionState SetPosition { get; }
public int SpatialRootCount => _spatialRoots.Count;
public int SpatialRemoteCount => _spatialRemotes.Count;
public int SpatialProjectileCount => _spatialProjectiles.Count;
@ -1061,18 +1097,41 @@ public sealed class RuntimePhysicsState : IDisposable
internal double UtcNowSeconds =>
(_timeProvider.GetUtcNow() - DateTimeOffset.UnixEpoch)
.TotalSeconds;
internal double MonotonicNowSeconds =>
_timeProvider.GetTimestamp()
/ (double)_timeProvider.TimestampFrequency;
internal double PlacementSimulationTime(double fallback) =>
_gameClock?.SimulationTimeSeconds ?? fallback;
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
new(
public RuntimePhysicsOwnershipSnapshot CaptureOwnership()
{
RuntimeSetPositionOwnershipSnapshot setPosition =
SetPosition.CaptureOwnership();
return new(
Engine.LandblockCount,
Engine.ShadowObjects.RetainedRegistrationCount,
_spatialRoots.Count,
_spatialRemotes.Count,
_spatialProjectiles.Count,
setPosition.ActiveOperationCount,
setPosition.AwaitingPreparationCount,
setPosition.DeferredCellCount,
setPosition.PendingProjectionAcknowledgementCount,
setPosition.LostDeadlineCount,
setPosition.LostDeadlineNodeCount,
setPosition.LostDeadlineIndexCount,
setPosition.ExpiredLostCellCount,
setPosition.ExpiredLostCellIndexCount,
setPosition.DeferredBucketCount,
setPosition.DeferredBucketOrderCount,
setPosition.UnboundDeferredCellCount,
setPosition.UnboundDeferredCellOrderCount,
setPosition.PreparedMoverCount,
_collisionAdmissions.Count,
_collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache),
_disposed);
}
public void AcknowledgeSpatialProjection(
RuntimeEntityRecord record,
@ -1806,6 +1865,20 @@ public sealed class RuntimePhysicsState : IDisposable
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
if (_collisionAdmissions.Remove(
canonical,
out RuntimeCollisionAdmission? superseded))
{
SetPosition.CancelCollisionGeneration(
canonical,
superseded.Generation);
if (_preparedCollisionGenerations.Remove(
canonical,
out PreparedLandblockCollisionGeneration? prepared))
{
prepared.Dispose();
}
}
ulong generation = _collisionGenerations.TryGetValue(
canonical,
out ulong current)
@ -1817,6 +1890,7 @@ public sealed class RuntimePhysicsState : IDisposable
canonical,
generation);
_collisionAdmissions[canonical] = admission;
SetPosition.BeginCollisionGeneration(canonical, generation);
return admission;
}
@ -1901,6 +1975,9 @@ public sealed class RuntimePhysicsState : IDisposable
&& ReferenceEquals(current, admission))
{
_collisionAdmissions.Remove(admission.LandblockId);
SetPosition.CancelCollisionGeneration(
admission.LandblockId,
admission.Generation);
_collisionGenerations[admission.LandblockId] = checked(
admission.Generation + 1UL);
}
@ -2137,6 +2214,10 @@ public sealed class RuntimePhysicsState : IDisposable
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
SetPosition.CommitCollisionGeneration(
acknowledgement.LandblockId,
acknowledgement.Generation,
acknowledgement.Ready);
PublishCollisionGenerationCommitted(
new RuntimeCollisionGenerationCommitted(
acknowledgement.LandblockId,
@ -2221,6 +2302,7 @@ public sealed class RuntimePhysicsState : IDisposable
prepared.Dispose();
}
_preparedCollisionGenerations.Clear();
SetPosition.Dispose();
_collisionOwnerJournal.Clear();
_collisionOwnerSubscribers.Clear();
Engine.Clear();
@ -2380,12 +2462,22 @@ public sealed class RuntimePhysicsState : IDisposable
private void InvalidateCollisionAdmission(uint landblockId)
{
ulong generation = _collisionGenerations.TryGetValue(
ulong currentGeneration = _collisionGenerations.TryGetValue(
landblockId,
out ulong current)
? checked(current + 1UL)
: 1UL;
? 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,
@ -2396,6 +2488,70 @@ public sealed class RuntimePhysicsState : IDisposable
TrimCollisionOwnerJournal();
}
internal ulong ExpectedCollisionGeneration(uint exactCellId)
{
uint landblockId = CanonicalLandblock(exactCellId);
if (landblockId == 0u)
return 0UL;
if (_collisionAdmissions.TryGetValue(
landblockId,
out RuntimeCollisionAdmission? admission))
{
return admission.Generation;
}
return _collisionGenerations.TryGetValue(
landblockId,
out ulong generation)
? checked(generation + 1UL)
: 1UL;
}
internal bool HandleSetPositionCollisions(
RuntimeEntityRecord record,
ulong positionAuthorityVersion,
ulong spatialAuthorityVersion,
ulong velocityAuthorityVersion,
bool previousContact,
bool previousOnWalkable,
in PhysicsSetPositionCollisionReport report)
{
if (!Entities.IsCurrent(record)
|| record.PositionAuthorityVersion != positionAuthorityVersion
|| record.SpatialAuthorityVersion != spatialAuthorityVersion
|| (velocityAuthorityVersion != 0UL
&& record.VelocityAuthorityVersion
!= velocityAuthorityVersion)
|| record.PhysicsBody is not { } body)
{
return false;
}
body.FramesStationaryFall = report.FramesStationaryFall;
PhysicsObjUpdate.HandleAllCollisions(
body,
report.CollisionNormalValid,
report.CollisionNormal,
previousContact,
previousOnWalkable,
body.OnWalkable);
body.TransientState &= ~(TransientStateFlags.StationaryFall
| TransientStateFlags.StationaryStop
| TransientStateFlags.StationaryStuck);
body.TransientState |= report.FramesStationaryFall switch
{
1 => TransientStateFlags.StationaryFall,
2 => TransientStateFlags.StationaryStop,
3 => TransientStateFlags.StationaryStuck,
_ => TransientStateFlags.None,
};
// Retail returns the result of collision reporting, not a collision-
// presence guess. Runtime does not yet own the per-object report/
// tracking table required to reproduce that return value, so fail
// closed. This preserves ordinary placement rejection and leaves the
// already-registered reporting seam explicit for the 4B2 cutover.
return false;
}
private void OnCollisionOwnerMutated(uint ownerId, ulong version)
{
_ = version;

File diff suppressed because it is too large Load diff