fix(physics): make collision activation starvation-free

This commit is contained in:
Erik 2026-07-31 18:34:46 +02:00
parent d94145e6b8
commit 6b28ff999c
14 changed files with 4637 additions and 474 deletions

View file

@ -23,18 +23,20 @@ public sealed class PhysicsDataCache
{
private readonly bool _requirePreparedCollision;
private PhysicsDataCache? _readFallback;
private readonly CollisionWorldStateSlot _collisionWorld;
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
private ConcurrentDictionary<uint, CellPhysics> _cellStruct =>
_collisionWorld.Current.CellStruct;
private readonly ConcurrentDictionary<uint, FlatGfxObjCollisionAsset>
_flatGfxObj = new();
private readonly ConcurrentDictionary<uint, FlatSetupCollision>
_flatSetup = new();
private readonly ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
_flatCellStruct = new();
private readonly ConcurrentDictionary<uint, FlatEnvCellTopology>
_flatEnvCell = new();
private ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
_flatCellStruct => _collisionWorld.Current.FlatCellStruct;
private ConcurrentDictionary<uint, FlatEnvCellTopology>
_flatEnvCell => _collisionWorld.Current.FlatEnvCell;
public PhysicsDataCache()
: this(requirePreparedCollision: false)
@ -42,8 +44,18 @@ public sealed class PhysicsDataCache
}
private PhysicsDataCache(bool requirePreparedCollision)
: this(requirePreparedCollision, new CollisionWorldStateSlot())
{
}
private PhysicsDataCache(
bool requirePreparedCollision,
CollisionWorldStateSlot collisionWorld)
{
_requirePreparedCollision = requirePreparedCollision;
_collisionWorld = collisionWorld
?? throw new ArgumentNullException(nameof(collisionWorld));
CellGraph = new UcgCellGraph(_collisionWorld);
if (!requirePreparedCollision
&& PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
{
@ -67,6 +79,18 @@ public sealed class PhysicsDataCache
return cache;
}
internal static PhysicsDataCache CreateProduction(
CollisionWorldStateSlot collisionWorld)
{
var cache = new PhysicsDataCache(
requirePreparedCollision: true,
collisionWorld)
{
CollisionTraversalMode = CollisionTraversalMode.Flat,
};
return cache;
}
internal CollisionShadowVerifier? CollisionShadow { get; set; }
public CollisionShadowStats CollisionShadowStats =>
@ -80,7 +104,8 @@ public sealed class PhysicsDataCache
CollisionTraversalMode.Graph;
// ── Phase 2: building portal cache for outdoor→indoor entry ───────────
private readonly ConcurrentDictionary<uint, BuildingPhysics> _buildings = new();
private ConcurrentDictionary<uint, BuildingPhysics> _buildings =>
_collisionWorld.Current.Buildings;
/// <summary>
/// The unified cell graph (UCG): the active id-&gt;cell resolver and registry.
@ -94,31 +119,23 @@ public sealed class PhysicsDataCache
/// (<c>TryGetTerrainOrigin</c>, read by <c>CellTransit</c>'s pick + transit
/// paths). No longer inert.
/// </summary>
public UcgCellGraph CellGraph { get; private set; } = new();
public UcgCellGraph CellGraph { get; }
internal CollisionWorldStateSlot CollisionWorld => _collisionWorld;
/// <summary>
/// Copies the currently committed immutable collision records into an
/// off-side cache. Streaming may replace one landblock in this copy over
/// many frames without exposing a partially withdrawn cell graph to live
/// physics queries.
/// Creates the empty off-side facade used by Runtime's metered root
/// materializer. Global immutable catalogs fall through to this cache;
/// mutable world topology is installed one leaf per host step.
/// </summary>
internal PhysicsDataCache CreateCollisionStagingCopy()
internal PhysicsDataCache CreateEmptyCollisionStaging(
CollisionWorldStateSlot collisionWorld)
{
var copy = new PhysicsDataCache(_requirePreparedCollision)
return new PhysicsDataCache(_requirePreparedCollision, collisionWorld)
{
CollisionTraversalMode = CollisionTraversalMode,
CellGraph = CellGraph.CreateCollisionStagingCopy(),
_readFallback = this,
};
// Global immutable GfxObj/Setup records are not copied wholesale.
// The accepted build's exact closure is staged cursor-by-cursor below;
// copying the process-retained asset catalog here would turn every
// landblock publication into an unbounded frame spike.
CopyDictionary(_cellStruct, copy._cellStruct);
CopyDictionary(_flatCellStruct, copy._flatCellStruct);
CopyDictionary(_flatEnvCell, copy._flatEnvCell);
CopyDictionary(_buildings, copy._buildings);
return copy;
}
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
@ -132,56 +149,6 @@ public sealed class PhysicsDataCache
gfxObjectIds,
setupIds);
internal void CommitLandblockReplacement(
PreparedPhysicsDataCacheLandblock replacement)
{
RemoveEntries(_cellStruct, replacement.CellIdsToRemove);
RemoveEntries(_flatCellStruct, replacement.FlatCellIdsToRemove);
RemoveEntries(_flatEnvCell, replacement.FlatEnvCellIdsToRemove);
RemoveEntries(_buildings, replacement.BuildingIdsToRemove);
CommitEntries(_gfxObj, replacement.GfxObjects, replace: false);
CommitEntries(_visualBounds, replacement.VisualBounds, replace: false);
CommitEntries(_flatGfxObj, replacement.FlatGfxObjects, replace: false);
CommitEntries(_setup, replacement.Setups, replace: false);
CommitEntries(_flatSetup, replacement.FlatSetups, replace: false);
CommitEntries(_cellStruct, replacement.Cells, replace: true);
CommitEntries(_flatCellStruct, replacement.FlatCells, replace: true);
CommitEntries(_flatEnvCell, replacement.FlatEnvCells, replace: true);
CommitEntries(_buildings, replacement.Buildings, replace: true);
CellGraph.CommitLandblockReplacement(replacement.CellGraph);
}
private static void CopyDictionary<T>(
ConcurrentDictionary<uint, T> source,
ConcurrentDictionary<uint, T> destination)
{
foreach ((uint id, T value) in source)
destination.TryAdd(id, value);
}
private static void CommitEntries<T>(
ConcurrentDictionary<uint, T> destination,
IReadOnlyList<KeyValuePair<uint, T>> entries,
bool replace)
{
for (int index = 0; index < entries.Count; index++)
{
(uint id, T value) = entries[index];
if (replace)
destination[id] = value;
else
destination.TryAdd(id, value);
}
}
private static void RemoveEntries<T>(
ConcurrentDictionary<uint, T> destination,
IReadOnlyList<uint> ids)
{
for (int index = 0; index < ids.Count; index++)
destination.TryRemove(ids[index], out _);
}
/// <summary>
/// Extract and cache the physics BSP + polygon data from a GfxObj,
/// PLUS always cache a visual AABB from the vertex data regardless of
@ -1085,6 +1052,9 @@ public sealed class PhysicsDataCache
if (_cursor < _gfxIds.Length)
{
uint id = _gfxIds[_cursor++];
Preinstall(_staging._gfxObj, _active._gfxObj, id);
Preinstall(_staging._visualBounds, _active._visualBounds, id);
Preinstall(_staging._flatGfxObj, _active._flatGfxObj, id);
Capture(_staging._gfxObj, id, _gfx);
Capture(_staging._visualBounds, id, _bounds);
Capture(_staging._flatGfxObj, id, _flatGfx);
@ -1098,6 +1068,8 @@ public sealed class PhysicsDataCache
if (_cursor < _setupIds.Length)
{
uint id = _setupIds[_cursor++];
Preinstall(_staging._setup, _active._setup, id);
Preinstall(_staging._flatSetup, _active._flatSetup, id);
Capture(_staging._setup, id, _setups);
Capture(_staging._flatSetup, id, _flatSetups);
WorkUnits++;
@ -1229,6 +1201,15 @@ public sealed class PhysicsDataCache
destination.Add(new KeyValuePair<uint, T>(id, value));
}
private static void Preinstall<T>(
ConcurrentDictionary<uint, T> source,
ConcurrentDictionary<uint, T> destination,
uint id)
{
if (source.TryGetValue(id, out T? value))
destination.TryAdd(id, value);
}
private static bool CapturePrefixOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,