feat(runtime): seal dormant SetPosition evaluations

This commit is contained in:
Erik 2026-08-01 11:31:58 +02:00
parent 22651c823d
commit 99f867f053
16 changed files with 2298 additions and 62 deletions

View file

@ -25,6 +25,14 @@ public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
private readonly List<uint> _order = new();
private readonly HashSet<uint> _seen = new();
/// <summary>
/// Optional append-only union target used by one retained SetPosition
/// transaction. Clearing this CELLARRAY must not clear the target: retail
/// can rebuild the working array repeatedly while every probed cell still
/// contributes to the transaction's collision-world authority footprint.
/// </summary>
internal CellArray? UnionTarget { get; set; }
public int Count => _order.Count;
public bool IsReadOnly => false;
@ -34,6 +42,11 @@ public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
/// <summary>Append <paramref name="id"/> iff not already present (retail add_cell dedup).</summary>
public void Add(uint id)
{
if (UnionTarget is { } target
&& !ReferenceEquals(target, this))
{
target.Add(id);
}
if (_seen.Add(id))
_order.Add(id);
}

View file

@ -169,6 +169,7 @@ public static class CellTransit
// Retail CEnvCell::find_transit_cells first asks the loaded
// neighbour cell whether the sphere intersects its CellBSP.
// The portal-plane side test is only the unloaded-cell load hint.
RecordUnionOnlyProbe(candidates, otherId);
var otherCell = cache.GetCellStruct(otherId);
if (otherCell is not null &&
CollisionTraversal.HasCellContainment(cache, otherCell))
@ -446,6 +447,7 @@ public static class CellTransit
if (portal.OtherPortalId < 0)
continue;
RecordUnionOnlyProbe(candidates, portal.OtherCellId);
var otherCell = cache.GetCellStruct(portal.OtherCellId);
if (otherCell is null ||
!CollisionTraversal.HasCellContainment(cache, otherCell))
@ -679,8 +681,13 @@ public static class CellTransit
/// </para>
/// </summary>
public static uint FindVisibleChildCell(
PhysicsDataCache cache, uint startCellId, Vector3 worldPoint, bool useStabList)
PhysicsDataCache cache,
uint startCellId,
Vector3 worldPoint,
bool useStabList,
ICollection<uint>? probedCells = null)
{
probedCells?.Add(startCellId);
var start = cache.GetCellStruct(startCellId);
if (start is null) return 0u;
@ -691,12 +698,17 @@ public static class CellTransit
{
// arg3 != 0 → iterate stab_list, GetVisible + point_in_cell (:311444-311465)
foreach (uint id in start.VisibleCellIds)
{
probedCells?.Add(id);
if (PointInCell(cache, cache.GetCellStruct(id), worldPoint)) return id;
}
}
else
{
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
foreach (var portal in start.Portals)
{
probedCells?.Add(portal.OtherCellId);
if (PointInCell(
cache,
cache.GetCellStruct(portal.OtherCellId),
@ -704,6 +716,7 @@ public static class CellTransit
{
return portal.OtherCellId;
}
}
}
return 0u;
@ -1068,7 +1081,11 @@ public static class CellTransit
sphereRadius))
{
uint recovered = FindVisibleChildCell(
cache, currentCellId, worldSphereCenter, useStabList: true);
cache,
currentCellId,
worldSphereCenter,
useStabList: true,
(candidates as CellArray)?.UnionTarget);
if (recovered != 0u && recovered != currentCellId)
return recovered;
}
@ -1078,6 +1095,14 @@ public static class CellTransit
return currentCellId;
}
private static void RecordUnionOnlyProbe(
ICollection<uint> candidates,
uint cellId)
{
if (candidates is CellArray { UnionTarget: { } queryFootprint })
queryFootprint.Add(cellId);
}
private static int EffectiveSphereCount(IReadOnlyList<Sphere> worldSpheres, int numSpheres)
{
if (numSpheres <= 0 || worldSpheres.Count == 0) return 0;

View file

@ -224,7 +224,22 @@ public sealed class PhysicsEngine
/// when the restriction weenie can't be resolved — so production MUST
/// wire this to the live table for the fix to actually admit anyone.
/// </summary>
public ClientObjectTable? Objects { get; set; }
private ClientObjectTable? _objects;
private ulong _objectsBindingRevision;
public ClientObjectTable? Objects
{
get => _objects;
set
{
if (ReferenceEquals(_objects, value))
return;
_objects = value;
_objectsBindingRevision = checked(_objectsBindingRevision + 1UL);
}
}
internal ulong ObjectsBindingRevision => _objectsBindingRevision;
internal sealed record LandblockPhysics(
TerrainSurface Terrain,
@ -1553,8 +1568,10 @@ public sealed class PhysicsEngine
private AdjustedSetPosition AdjustSetPosition(
uint seedCellId,
Vector3 cellLocalPosition,
Vector3 firstWorldSphereCenter)
Vector3 firstWorldSphereCenter,
CellArray queryFootprint)
{
queryFootprint.Add(seedCellId);
uint low = seedCellId & 0xFFFFu;
bool lowInRange = low is (>= 1u and <= 0x40u)
or (>= 0x0100u and <= 0xFFFDu)
@ -1582,7 +1599,8 @@ public sealed class PhysicsEngine
cache,
seedCellId,
firstWorldSphereCenter,
useStabList: true);
useStabList: true,
queryFootprint);
if (child != 0u)
{
return new AdjustedSetPosition(
@ -1606,6 +1624,7 @@ public sealed class PhysicsEngine
bool adjusted = LandDefs.AdjustToOutside(
ref adjustedCell,
ref adjustedLocal);
queryFootprint.Add(adjustedCell);
bool resident = adjusted
&& IsLandblockTerrainResident(adjustedCell);
return new AdjustedSetPosition(
@ -1635,33 +1654,54 @@ public sealed class PhysicsEngine
}
Transition transition = RentTransition();
CellArray queryFootprint =
transition.SpherePath.SetPositionQueryFootprint;
try
{
queryFootprint.Clear();
transition.SpherePath.CellCandidates.UnionTarget = queryFootprint;
InitializeSetPositionTransition(transition, request);
bool randomOnly = request.Flags.HasFlag(
PhysicsSetPositionFlags.RandomScatter);
PhysicsSetPositionResult result;
if (randomOnly)
{
return SetScatterPositionInternal(
result = SetScatterPositionInternal(
transition,
request,
handleCollisions);
handleCollisions,
queryFootprint);
}
else
{
result = SetPositionInternal(
transition,
request,
handleCollisions,
queryFootprint);
if (result.Error != PhysicsSetPositionError.Ok
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
{
result = SetScatterPositionInternal(
transition,
request,
handleCollisions,
queryFootprint);
}
}
PhysicsSetPositionResult result =
SetPositionInternal(transition, request, handleCollisions);
if (result.Error != PhysicsSetPositionError.Ok
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
// Scatter retains one append-only query union across every inner
// attempt. Materialize it exactly once at the public transaction
// boundary; copying it per attempt is quadratic at retail's
// maximum retry count.
return result with
{
return SetScatterPositionInternal(
transition,
request,
handleCollisions);
}
return result;
QueriedCellIds = queryFootprint.OrderedIds.ToImmutableArray(),
};
}
finally
{
transition.SpherePath.CellCandidates.UnionTarget = null;
ReturnTransition(transition);
}
}
@ -1686,7 +1726,8 @@ public sealed class PhysicsEngine
private PhysicsSetPositionResult SetScatterPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
CellArray queryFootprint)
{
PhysicsSetPositionResult result = ErrorResult(
request,
@ -1706,7 +1747,8 @@ public sealed class PhysicsEngine
result = SetPositionInternal(
transition,
scattered,
handleCollisions);
handleCollisions,
queryFootprint);
if (result.Error == PhysicsSetPositionError.Ok)
break;
}
@ -1716,7 +1758,8 @@ public sealed class PhysicsEngine
private PhysicsSetPositionResult SetPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
CellArray queryFootprint)
{
transition.SpherePath.CellCandidates.Clear();
transition.SpherePath.ClearWalkable();
@ -1731,7 +1774,8 @@ public sealed class PhysicsEngine
AdjustedSetPosition adjusted = AdjustSetPosition(
request.CellId,
request.CellLocalPosition,
firstWorldCenter);
firstWorldCenter,
queryFootprint);
if (!adjusted.Resident)
{
return new PhysicsSetPositionResult(
@ -1849,7 +1893,9 @@ public sealed class PhysicsEngine
}
if (spherePath.CurCellId == 0u)
{
return ErrorResult(request, PhysicsSetPositionError.NoCell);
return ErrorResult(
request,
PhysicsSetPositionError.NoCell);
}
bool inContact = collision.ContactPlaneValid;

View file

@ -145,7 +145,8 @@ internal readonly record struct PhysicsSetPositionResult(
bool CellChanged = false,
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
ImmutableArray<uint> CrossCellIds = default,
ImmutableArray<uint> CollidedObjectIds = default)
ImmutableArray<uint> CollidedObjectIds = default,
ImmutableArray<uint> QueriedCellIds = default)
{
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
internal bool IsCommitted =>

View file

@ -78,6 +78,7 @@ public sealed class ShadowObjectRegistry
_collisionWorld.Current.ShadowOwnerFreeSlots;
private readonly HashSet<uint> _prefixScratch = new();
private readonly List<uint> _removedPrefixScratch = new();
private ulong _mutationRevision;
internal event Action<uint, ulong>? OwnerMutated;
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
@ -101,6 +102,7 @@ public sealed class ShadowObjectRegistry
"A populated shadow registry cannot change collision roots.");
}
_collisionWorld = collisionWorld;
AdvanceMutationRevision();
}
internal sealed record RegistrationRecord(
@ -123,8 +125,20 @@ public sealed class ShadowObjectRegistry
? version
: 0UL;
/// <summary>
/// Monotonic authority for every logical mutation of the active shadow
/// collision world. SetPosition evaluation receipts seal this value so a
/// later owner insert, removal, move, state change, suspension, reflood,
/// or cell-row replacement cannot be committed against a different world.
/// </summary>
internal ulong MutationRevision => _mutationRevision;
private void AdvanceMutationRevision() =>
_mutationRevision = checked(_mutationRevision + 1UL);
private void BumpOwnerVersion(uint entityId)
{
AdvanceMutationRevision();
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
_ownerVersions[entityId] = version;
RefreshOwnerPrefixIndex(entityId);
@ -1308,6 +1322,7 @@ public sealed class ShadowObjectRegistry
DeregisterCore(entityId, publishMutation: false);
RemoveOwnerPrefixMembership(entityId);
_ownerVersions.Remove(entityId);
AdvanceMutationRevision();
return;
}
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
@ -1877,6 +1892,13 @@ public sealed class ShadowObjectRegistry
/// </summary>
public void Clear()
{
bool mutated = _cells.Count != 0
|| _entityToCells.Count != 0
|| _entityReg.Count != 0
|| _suspendedEntities.Count != 0
|| _suspendedEntityCells.Count != 0;
if (mutated)
AdvanceMutationRevision();
_cells.Clear();
_entityToCells.Clear();
_suspendedEntities.Clear();

View file

@ -719,6 +719,7 @@ public sealed class SpherePath
private Vector3[]? _walkableVertexStorage;
private Vector3[]? _lastWalkableVertexStorage;
internal readonly CellArray CellCandidates = new();
internal readonly CellArray SetPositionQueryFootprint = new();
internal readonly CellOrderScratchArena OrderedCellScratch = new();
internal Vector3[]? RetainedWalkableVertexStorage => _walkableVertexStorage;
@ -938,6 +939,8 @@ public sealed class SpherePath
if (_lastWalkableVertexStorage is not null)
System.Array.Clear(_lastWalkableVertexStorage);
CellCandidates.Clear();
CellCandidates.UnionTarget = null;
SetPositionQueryFootprint.Clear();
OrderedCellScratch.ResetForReuse();
}