fix(physics): activate collision generations atomically
This commit is contained in:
parent
3e0f3b6206
commit
be94bc9b06
18 changed files with 1402 additions and 80 deletions
|
|
@ -51,8 +51,9 @@ public sealed class ShadowObjectRegistry
|
|||
/// is the streaming-side trigger.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
|
||||
private readonly Dictionary<uint, ulong> _ownerVersions = new();
|
||||
|
||||
private sealed record RegistrationRecord(
|
||||
internal sealed record RegistrationRecord(
|
||||
uint SeedCellId,
|
||||
Vector3 EntityWorldPos,
|
||||
Quaternion EntityWorldRot,
|
||||
|
|
@ -67,6 +68,16 @@ public sealed class ShadowObjectRegistry
|
|||
float CylHeight,
|
||||
float Scale);
|
||||
|
||||
internal ulong GetOwnerVersion(uint entityId) =>
|
||||
_ownerVersions.TryGetValue(entityId, out ulong version)
|
||||
? version
|
||||
: 0UL;
|
||||
|
||||
private void BumpOwnerVersion(uint entityId)
|
||||
{
|
||||
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flood's data source (cells, buildings, terrain origins). Wired by
|
||||
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
|
||||
|
|
@ -135,6 +146,7 @@ public sealed class ShadowObjectRegistry
|
|||
_entityReg[entityId] = new RegistrationRecord(
|
||||
seed, worldPos, rotation, state, flags, isStatic,
|
||||
IsMultiPart: false, gfxObjId, radius, collisionType, cylHeight, scale);
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -214,6 +226,7 @@ public sealed class ShadowObjectRegistry
|
|||
seed, entityWorldPos, entityWorldRot, state, flags, isStatic,
|
||||
IsMultiPart: true, GfxObjId: 0u, Radius: 0f,
|
||||
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -271,7 +284,10 @@ public sealed class ShadowObjectRegistry
|
|||
};
|
||||
|
||||
if (suspended || !_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||
{
|
||||
BumpOwnerVersion(entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (uint cellId in cells)
|
||||
{
|
||||
|
|
@ -300,6 +316,7 @@ public sealed class ShadowObjectRegistry
|
|||
foreach (uint cellId in cells)
|
||||
AddEntryToCell(entry, cellId);
|
||||
}
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -441,6 +458,7 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
|
||||
_suspendedEntities.Add(entityId);
|
||||
BumpOwnerVersion(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -624,11 +642,16 @@ public sealed class ShadowObjectRegistry
|
|||
|
||||
if (_entityReg.TryGetValue(entityId, out var reg))
|
||||
_entityReg[entityId] = reg with { State = newState };
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||
public void Deregister(uint entityId)
|
||||
{
|
||||
bool existed = _entityReg.ContainsKey(entityId)
|
||||
|| _entityToCells.ContainsKey(entityId)
|
||||
|| _entityShapes.ContainsKey(entityId)
|
||||
|| _suspendedEntities.Contains(entityId);
|
||||
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
{
|
||||
foreach (var cellId in cellIds)
|
||||
|
|
@ -642,6 +665,8 @@ public sealed class ShadowObjectRegistry
|
|||
_entityReg.Remove(entityId);
|
||||
_suspendedEntities.Remove(entityId);
|
||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||
if (existed)
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -708,11 +733,13 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
uint lbPrefix = landblockId & 0xFFFF0000u;
|
||||
var toRemove = new List<uint>();
|
||||
var touchedOwners = new HashSet<uint>();
|
||||
|
||||
foreach (var (entityId, cells) in _entityToCells)
|
||||
{
|
||||
if (!cells.Exists(cell => (cell & 0xFFFF0000u) == lbPrefix))
|
||||
continue;
|
||||
touchedOwners.Add(entityId);
|
||||
if (!_withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawn))
|
||||
{
|
||||
withdrawn = new HashSet<uint>();
|
||||
|
|
@ -753,6 +780,8 @@ public sealed class ShadowObjectRegistry
|
|||
_withdrawnPrefixesByOwner.Remove(eid);
|
||||
}
|
||||
}
|
||||
foreach (uint entityId in touchedOwners)
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -795,6 +824,315 @@ public sealed class ShadowObjectRegistry
|
|||
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
|
||||
public int SuspendedRegistrationCount => _suspendedEntities.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Copies the committed registry into an off-side collision generation.
|
||||
/// All mutable lists and sets are cloned; immutable registration and shape
|
||||
/// payloads may be shared.
|
||||
/// </summary>
|
||||
internal void CopyCollisionStateFrom(
|
||||
ShadowObjectRegistry source,
|
||||
PhysicsDataCache stagingCache)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(stagingCache);
|
||||
Clear();
|
||||
DataCache = stagingCache;
|
||||
foreach ((uint cellId, List<ShadowEntry> entries) in source._cells)
|
||||
_cells[cellId] = new List<ShadowEntry>(entries);
|
||||
foreach ((uint ownerId, List<uint> cells) in source._entityToCells)
|
||||
_entityToCells[ownerId] = new List<uint>(cells);
|
||||
foreach (uint ownerId in source._suspendedEntities)
|
||||
_suspendedEntities.Add(ownerId);
|
||||
foreach ((uint ownerId, HashSet<uint> prefixes) in
|
||||
source._withdrawnPrefixesByOwner)
|
||||
{
|
||||
_withdrawnPrefixesByOwner[ownerId] = new HashSet<uint>(prefixes);
|
||||
}
|
||||
foreach ((uint ownerId, IReadOnlyList<ShadowShape> shapes) in
|
||||
source._entityShapes)
|
||||
{
|
||||
_entityShapes[ownerId] = shapes;
|
||||
}
|
||||
foreach ((uint ownerId, RegistrationRecord registration) in
|
||||
source._entityReg)
|
||||
{
|
||||
_entityReg[ownerId] = registration;
|
||||
}
|
||||
foreach ((uint ownerId, ulong version) in source._ownerVersions)
|
||||
_ownerVersions[ownerId] = version;
|
||||
}
|
||||
|
||||
internal uint[] CaptureDynamicRefloodOwnersForLandblock(
|
||||
uint landblockId)
|
||||
{
|
||||
uint[] owners = CaptureRefloodOwnersForLandblock(landblockId);
|
||||
return owners.Where(ownerId =>
|
||||
_entityReg.TryGetValue(ownerId, out RegistrationRecord? record)
|
||||
&& !record.IsStatic)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes one staging owner from the exact active payload, then floods
|
||||
/// it against the staging generation's complete cell graph. The returned
|
||||
/// source version is the commit-time freshness token.
|
||||
/// </summary>
|
||||
internal bool RefreshDynamicOwnerFrom(
|
||||
ShadowObjectRegistry source,
|
||||
uint entityId,
|
||||
uint landblockId,
|
||||
out ulong sourceVersion)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
Deregister(entityId);
|
||||
sourceVersion = source.GetOwnerVersion(entityId);
|
||||
if (!source._entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration)
|
||||
|| registration.IsStatic
|
||||
|| source._suspendedEntities.Contains(entityId)
|
||||
|| !source.OwnerTouchesLandblock(entityId, landblockId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (registration.IsMultiPart
|
||||
&& source._entityShapes.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? shapes))
|
||||
{
|
||||
RegisterMultiPart(
|
||||
entityId,
|
||||
registration.EntityWorldPos,
|
||||
registration.EntityWorldRot,
|
||||
shapes,
|
||||
registration.State,
|
||||
registration.Flags,
|
||||
0f,
|
||||
0f,
|
||||
landblockId,
|
||||
registration.SeedCellId,
|
||||
isStatic: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Register(
|
||||
entityId,
|
||||
registration.GfxObjId,
|
||||
registration.EntityWorldPos,
|
||||
registration.EntityWorldRot,
|
||||
registration.Radius,
|
||||
0f,
|
||||
0f,
|
||||
landblockId,
|
||||
registration.CollisionType,
|
||||
registration.CylHeight,
|
||||
registration.Scale,
|
||||
registration.State,
|
||||
registration.Flags,
|
||||
registration.SeedCellId,
|
||||
isStatic: false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal uint[] FindDirtyDynamicOwners(
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expectedVersions)
|
||||
{
|
||||
var dirty = new HashSet<uint>(
|
||||
CaptureDynamicRefloodOwnersForLandblock(landblockId));
|
||||
dirty.UnionWith(expectedVersions.Keys);
|
||||
dirty.RemoveWhere(ownerId =>
|
||||
expectedVersions.TryGetValue(ownerId, out ulong expected)
|
||||
&& OwnerTouchesLandblock(ownerId, landblockId)
|
||||
&& GetOwnerVersion(ownerId) == expected);
|
||||
uint[] result = dirty.ToArray();
|
||||
Array.Sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal PreparedLandblockShadowReplacement PrepareLandblockReplacement(
|
||||
ShadowObjectRegistry staging,
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staging);
|
||||
uint[] dirty = FindDirtyDynamicOwners(
|
||||
landblockId,
|
||||
expectedDynamicVersions);
|
||||
if (dirty.Length != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow owners changed before collision generation sealing.");
|
||||
}
|
||||
|
||||
var owners = new HashSet<uint>(CaptureStaticOwnersForLandblock(landblockId));
|
||||
owners.UnionWith(staging.CaptureStaticOwnersForLandblock(landblockId));
|
||||
owners.UnionWith(expectedDynamicVersions.Keys);
|
||||
uint[] ownerIds = owners.ToArray();
|
||||
Array.Sort(ownerIds);
|
||||
var states = new List<PreparedShadowOwnerState>(ownerIds.Length);
|
||||
foreach (uint ownerId in ownerIds)
|
||||
{
|
||||
if (staging.TryCaptureOwnerState(ownerId, out PreparedShadowOwnerState? state)
|
||||
&& state is not null)
|
||||
states.Add(state);
|
||||
}
|
||||
return new PreparedLandblockShadowReplacement(
|
||||
landblockId & 0xFFFF0000u,
|
||||
ownerIds,
|
||||
states.ToArray(),
|
||||
expectedDynamicVersions.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value));
|
||||
}
|
||||
|
||||
internal bool ValidateLandblockReplacement(
|
||||
PreparedLandblockShadowReplacement replacement)
|
||||
{
|
||||
foreach ((uint ownerId, ulong version) in replacement.DynamicVersions)
|
||||
{
|
||||
if (GetOwnerVersion(ownerId) != version
|
||||
|| !OwnerTouchesLandblock(ownerId, replacement.LandblockPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return CaptureDynamicRefloodOwnersForLandblock(
|
||||
replacement.LandblockPrefix)
|
||||
.SequenceEqual(replacement.DynamicVersions.Keys.Order());
|
||||
}
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedLandblockShadowReplacement replacement)
|
||||
{
|
||||
if (!ValidateLandblockReplacement(replacement))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow owners changed before collision generation commit.");
|
||||
}
|
||||
|
||||
foreach (uint ownerId in replacement.OwnerIds)
|
||||
Deregister(ownerId);
|
||||
foreach (PreparedShadowOwnerState state in replacement.OwnerStates)
|
||||
InstallOwnerState(state);
|
||||
}
|
||||
|
||||
private bool OwnerTouchesLandblock(uint entityId, uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? record))
|
||||
return false;
|
||||
if ((record.SeedCellId & 0xFFFF0000u) == prefix)
|
||||
return true;
|
||||
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells)
|
||||
&& cells.Exists(cell => (cell & 0xFFFF0000u) == prefix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return _withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? withdrawn)
|
||||
&& withdrawn.Contains(prefix);
|
||||
}
|
||||
|
||||
private bool TryCaptureOwnerState(
|
||||
uint entityId,
|
||||
out PreparedShadowOwnerState? state)
|
||||
{
|
||||
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? registration))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
_entityToCells.TryGetValue(entityId, out List<uint>? cells);
|
||||
_entityShapes.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? shapes);
|
||||
_withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? withdrawn);
|
||||
var rows = new List<PreparedShadowCellRows>();
|
||||
if (cells is not null)
|
||||
{
|
||||
foreach (uint cellId in cells)
|
||||
{
|
||||
if (_cells.TryGetValue(cellId, out List<ShadowEntry>? entries))
|
||||
{
|
||||
rows.Add(new PreparedShadowCellRows(
|
||||
cellId,
|
||||
entries.Where(entry => entry.EntityId == entityId)
|
||||
.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
state = new PreparedShadowOwnerState(
|
||||
entityId,
|
||||
registration,
|
||||
shapes,
|
||||
cells?.ToArray() ?? Array.Empty<uint>(),
|
||||
rows.ToArray(),
|
||||
_suspendedEntities.Contains(entityId),
|
||||
withdrawn?.ToArray() ?? Array.Empty<uint>());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void InstallOwnerState(PreparedShadowOwnerState state)
|
||||
{
|
||||
_entityReg[state.EntityId] = state.Registration;
|
||||
if (state.Shapes is not null)
|
||||
_entityShapes[state.EntityId] = state.Shapes;
|
||||
if (state.Suspended)
|
||||
_suspendedEntities.Add(state.EntityId);
|
||||
if (state.WithdrawnPrefixes.Length != 0)
|
||||
{
|
||||
_withdrawnPrefixesByOwner[state.EntityId] =
|
||||
new HashSet<uint>(state.WithdrawnPrefixes);
|
||||
}
|
||||
if (state.CellIds.Length != 0)
|
||||
_entityToCells[state.EntityId] = new List<uint>(state.CellIds);
|
||||
foreach (PreparedShadowCellRows row in state.Rows)
|
||||
{
|
||||
foreach (ShadowEntry entry in row.Entries)
|
||||
AddEntryToCell(entry, row.CellId);
|
||||
}
|
||||
BumpOwnerVersion(state.EntityId);
|
||||
}
|
||||
|
||||
internal sealed class PreparedLandblockShadowReplacement
|
||||
{
|
||||
internal PreparedLandblockShadowReplacement(
|
||||
uint landblockPrefix,
|
||||
uint[] ownerIds,
|
||||
PreparedShadowOwnerState[] ownerStates,
|
||||
Dictionary<uint, ulong> dynamicVersions)
|
||||
{
|
||||
LandblockPrefix = landblockPrefix;
|
||||
OwnerIds = ownerIds;
|
||||
OwnerStates = ownerStates;
|
||||
DynamicVersions = dynamicVersions;
|
||||
}
|
||||
|
||||
internal uint LandblockPrefix { get; }
|
||||
internal uint[] OwnerIds { get; }
|
||||
internal PreparedShadowOwnerState[] OwnerStates { get; }
|
||||
internal Dictionary<uint, ulong> DynamicVersions { get; }
|
||||
}
|
||||
|
||||
internal sealed record PreparedShadowOwnerState(
|
||||
uint EntityId,
|
||||
RegistrationRecord Registration,
|
||||
IReadOnlyList<ShadowShape>? Shapes,
|
||||
uint[] CellIds,
|
||||
PreparedShadowCellRows[] Rows,
|
||||
bool Suspended,
|
||||
uint[] WithdrawnPrefixes);
|
||||
|
||||
internal sealed record PreparedShadowCellRows(
|
||||
uint CellId,
|
||||
ShadowEntry[] Entries);
|
||||
|
||||
/// <summary>
|
||||
/// Retires the complete logical registry at terminal physics-engine
|
||||
/// disposal, including suspended live registrations that own no cell row.
|
||||
|
|
@ -807,6 +1145,7 @@ public sealed class ShadowObjectRegistry
|
|||
_withdrawnPrefixesByOwner.Clear();
|
||||
_entityShapes.Clear();
|
||||
_entityReg.Clear();
|
||||
_ownerVersions.Clear();
|
||||
_fallback = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue