fix(physics): activate collision generations atomically

This commit is contained in:
Erik 2026-07-31 15:19:25 +02:00
parent 3e0f3b6206
commit be94bc9b06
18 changed files with 1402 additions and 80 deletions

View file

@ -6,6 +6,7 @@ using DatReaderWriter.Types;
using Plane = System.Numerics.Plane;
using UcgEnvCell = AcDream.Core.World.Cells.EnvCell;
using UcgCellGraph = AcDream.Core.World.Cells.CellGraph;
using PreparedCellGraphLandblock = AcDream.Core.World.Cells.PreparedCellGraphLandblock;
namespace AcDream.Core.Physics;
@ -21,6 +22,7 @@ namespace AcDream.Core.Physics;
public sealed class PhysicsDataCache
{
private readonly bool _requirePreparedCollision;
private PhysicsDataCache? _readFallback;
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
@ -92,7 +94,112 @@ public sealed class PhysicsDataCache
/// (<c>TryGetTerrainOrigin</c>, read by <c>CellTransit</c>'s pick + transit
/// paths). No longer inert.
/// </summary>
public UcgCellGraph CellGraph { get; } = new();
public UcgCellGraph CellGraph { get; private set; } = new();
/// <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.
/// </summary>
internal PhysicsDataCache CreateCollisionStagingCopy()
{
var copy = new PhysicsDataCache(_requirePreparedCollision)
{
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 PreparedPhysicsDataCacheLandblock PrepareLandblockReplacement(
uint landblockId,
ReadOnlySpan<uint> gfxObjectIds,
ReadOnlySpan<uint> setupIds)
{
uint prefix = landblockId & 0xFFFF0000u;
return new PreparedPhysicsDataCacheLandblock(
prefix,
CaptureRequested(_gfxObj, gfxObjectIds),
CaptureRequested(_visualBounds, gfxObjectIds),
CaptureRequested(_flatGfxObj, gfxObjectIds),
CaptureRequested(_setup, setupIds),
CaptureRequested(_flatSetup, setupIds),
CapturePrefix(_cellStruct, prefix),
CapturePrefix(_flatCellStruct, prefix),
CapturePrefix(_flatEnvCell, prefix),
CapturePrefix(_buildings, prefix),
CellGraph.PrepareLandblockReplacement(prefix));
}
internal void CommitLandblockReplacement(
PreparedPhysicsDataCacheLandblock replacement)
{
RemoveCellsForLandblock(replacement.LandblockPrefix);
RemoveBuildingsForLandblock(replacement.LandblockPrefix);
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 KeyValuePair<uint, T>[] CaptureRequested<T>(
ConcurrentDictionary<uint, T> source,
ReadOnlySpan<uint> ids)
{
var result = new List<KeyValuePair<uint, T>>(ids.Length);
for (int index = 0; index < ids.Length; index++)
{
uint id = ids[index];
if (source.TryGetValue(id, out T? value))
result.Add(new KeyValuePair<uint, T>(id, value));
}
return result.ToArray();
}
private static KeyValuePair<uint, T>[] CapturePrefix<T>(
ConcurrentDictionary<uint, T> source,
uint prefix) => source
.Where(pair => (pair.Key & 0xFFFF0000u) == prefix)
.OrderBy(static pair => pair.Key)
.ToArray();
private static void CommitEntries<T>(
ConcurrentDictionary<uint, T> destination,
KeyValuePair<uint, T>[] entries,
bool replace)
{
foreach ((uint id, T value) in entries)
{
if (replace)
destination[id] = value;
else
destination.TryAdd(id, value);
}
}
/// <summary>
/// Extract and cache the physics BSP + polygon data from a GfxObj,
@ -237,7 +344,9 @@ public sealed class PhysicsDataCache
/// Get the cached visual AABB for a GfxObj, or null if not cached.
/// </summary>
public GfxObjVisualBounds? GetVisualBounds(uint gfxObjId) =>
_visualBounds.TryGetValue(gfxObjId, out var vb) ? vb : null;
_visualBounds.TryGetValue(gfxObjId, out var vb)
? vb
: _readFallback?.GetVisualBounds(gfxObjId);
/// <summary>
/// Compute a tight axis-aligned bounding box over all vertices in the mesh.
@ -756,14 +865,24 @@ public sealed class PhysicsDataCache
$"Production {kind} 0x{sourceId:X8} has no prepared collision asset. " +
"Gameplay must not extract or fall back to a parsed DAT graph.");
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
public GfxObjPhysics? GetGfxObj(uint id) =>
_gfxObj.TryGetValue(id, out var p)
? p
: _readFallback?.GetGfxObj(id);
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
public SetupPhysics? GetSetup(uint id) =>
_setup.TryGetValue(id, out var p)
? p
: _readFallback?.GetSetup(id);
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
_flatGfxObj.TryGetValue(id, out var value) ? value : null;
_flatGfxObj.TryGetValue(id, out var value)
? value
: _readFallback?.GetFlatGfxObj(id);
public FlatSetupCollision? GetFlatSetup(uint id) =>
_flatSetup.TryGetValue(id, out var value) ? value : null;
_flatSetup.TryGetValue(id, out var value)
? value
: _readFallback?.GetFlatSetup(id);
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
@ -926,6 +1045,19 @@ public sealed class PhysicsDataCache
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
}
internal sealed record PreparedPhysicsDataCacheLandblock(
uint LandblockPrefix,
KeyValuePair<uint, GfxObjPhysics>[] GfxObjects,
KeyValuePair<uint, GfxObjVisualBounds>[] VisualBounds,
KeyValuePair<uint, FlatGfxObjCollisionAsset>[] FlatGfxObjects,
KeyValuePair<uint, SetupPhysics>[] Setups,
KeyValuePair<uint, FlatSetupCollision>[] FlatSetups,
KeyValuePair<uint, CellPhysics>[] Cells,
KeyValuePair<uint, FlatCellStructureCollisionAsset>[] FlatCells,
KeyValuePair<uint, FlatEnvCellTopology>[] FlatEnvCells,
KeyValuePair<uint, BuildingPhysics>[] Buildings,
PreparedCellGraphLandblock CellGraph);
/// <summary>
/// Visual AABB of a GfxObj mesh — populated for every cached GfxObj regardless
/// of whether it has physics data. Used as a collision fallback shape for