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

@ -18,8 +18,24 @@ namespace AcDream.Core.World.Cells;
/// </summary>
public sealed class CellGraph
{
private readonly ConcurrentDictionary<uint, EnvCell> _envCells = new();
private readonly ConcurrentDictionary<uint, CellGraphTerrain> _terrain = new();
private readonly CollisionWorldStateSlot _collisionWorld;
private ConcurrentDictionary<uint, EnvCell> _envCells =>
_collisionWorld.Current.EnvCells;
private ConcurrentDictionary<uint, CellGraphTerrain> _terrain =>
_collisionWorld.Current.Terrain;
private ConcurrentDictionary<uint, ObjCell> _outdoorCells =>
_collisionWorld.Current.OutdoorCells;
public CellGraph()
: this(new CollisionWorldStateSlot())
{
}
internal CellGraph(CollisionWorldStateSlot collisionWorld)
{
_collisionWorld = collisionWorld
?? throw new ArgumentNullException(nameof(collisionWorld));
}
/// <summary>The player's current cell — the render/lighting root. Written ONLY at the
/// player chokepoint <see cref="AcDream.Core.Physics.PhysicsEngine.UpdatePlayerCurrCell"/>
@ -34,8 +50,21 @@ public sealed class CellGraph
/// <param name="landblockPrefix">Any id in the cell's landblock; masked to (id &amp; 0xFFFF0000).</param>
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
=> _terrain[landblockPrefix & 0xFFFF0000u] =
new CellGraphTerrain(terrain, worldOrigin);
{
uint prefix = landblockPrefix & 0xFFFF0000u;
_terrain[prefix] = new CellGraphTerrain(terrain, worldOrigin);
for (uint low = 1u; low <= 0x40u; low++)
{
uint id = prefix | low;
int index = (int)(low - 1u);
_outdoorCells[id] = LandCell.Synthesize(
id,
terrain,
worldOrigin,
index / 8,
index % 8);
}
}
/// <summary>
/// World origin (SW corner) of the landblock containing <paramref name="id"/>,
@ -65,6 +94,8 @@ public sealed class CellGraph
CurrCell = null;
}
_terrain.TryRemove(lb, out _);
for (uint low = 1u; low <= 0x40u; low++)
_outdoorCells.TryRemove(lb | low, out _);
foreach (var id in new List<uint>(_envCells.Keys))
if ((id & 0xFFFF0000u) == lb) _envCells.TryRemove(id, out _);
}
@ -95,9 +126,9 @@ public sealed class CellGraph
uint low = id & 0xFFFFu;
if (low < 1u || low > 0x40u) return null;
if (!_terrain.TryGetValue(id & 0xFFFF0000u, out var t)) return null;
int idx = (int)(low - 1u);
return LandCell.Synthesize(id, t.Terrain, t.Origin, idx / 8, idx % 8);
return _outdoorCells.TryGetValue(id, out ObjCell? cell)
? cell
: null;
}
/// <summary>
@ -127,63 +158,10 @@ public sealed class CellGraph
return null;
}
/// <summary>
/// Creates an immutable-reference snapshot for collision-generation
/// preparation. EnvCell and TerrainSurface records are immutable after
/// publication, so copying the registries is sufficient; the active graph
/// remains untouched while the staging graph is rebuilt.
/// </summary>
internal CellGraph CreateCollisionStagingCopy()
{
var copy = new CellGraph { CurrCell = CurrCell };
foreach ((uint id, EnvCell cell) in _envCells)
copy._envCells.TryAdd(id, cell);
foreach ((uint id, CellGraphTerrain terrain) in _terrain)
{
copy._terrain.TryAdd(id, terrain);
}
return copy;
}
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
CellGraph staging,
uint landblockId) => new(this, staging, landblockId);
internal void CommitLandblockReplacement(
PreparedCellGraphLandblock replacement)
{
uint currentCellId = CurrCell?.Id ?? 0u;
for (int index = 0; index < replacement.EnvCellIdsToRemove.Count; index++)
_envCells.TryRemove(replacement.EnvCellIdsToRemove[index], out _);
if (replacement.HasTerrain)
{
_terrain[replacement.LandblockPrefix] = replacement.Terrain!;
}
else
{
_terrain.TryRemove(replacement.LandblockPrefix, out _);
}
for (int index = 0; index < replacement.EnvCells.Count; index++)
{
(uint id, EnvCell cell) = replacement.EnvCells[index];
_envCells[id] = cell;
}
uint desiredCurrentCellId =
(currentCellId & 0xFFFF0000u) == replacement.LandblockPrefix
? currentCellId
: currentCellId == 0u
&& (replacement.CurrentCellId & 0xFFFF0000u)
== replacement.LandblockPrefix
? replacement.CurrentCellId
: 0u;
if (desiredCurrentCellId != 0u)
CurrCell = GetVisible(desiredCurrentCellId);
else if ((currentCellId & 0xFFFF0000u)
== replacement.LandblockPrefix)
CurrCell = null;
}
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly CellGraph _active;
@ -249,8 +227,7 @@ public sealed class CellGraph
_removeIds,
_envCells,
hasTerrain,
terrain,
_staging.CurrCell?.Id ?? 0u);
terrain);
_phase = 2;
}
return true;
@ -271,8 +248,7 @@ internal sealed record PreparedCellGraphLandblock(
IReadOnlyList<uint> EnvCellIdsToRemove,
IReadOnlyList<KeyValuePair<uint, EnvCell>> EnvCells,
bool HasTerrain,
CellGraphTerrain? Terrain,
uint CurrentCellId);
CellGraphTerrain? Terrain);
internal sealed record CellGraphTerrain(
TerrainSurface Terrain,