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

@ -26,20 +26,26 @@ namespace AcDream.Core.Physics;
/// </summary>
public sealed class ShadowObjectRegistry
{
private readonly Dictionary<uint, List<ShadowEntry>> _cells = new();
private readonly Dictionary<uint, List<uint>> _entityToCells = new(); // for deregistration
private readonly HashSet<uint> _suspendedEntities = new();
private CollisionWorldStateSlot _collisionWorld;
private Dictionary<uint, List<ShadowEntry>> _cells =>
_collisionWorld.Current.ShadowCells;
private Dictionary<uint, List<uint>> _entityToCells =>
_collisionWorld.Current.ShadowEntityCells; // for deregistration
private HashSet<uint> _suspendedEntities =>
_collisionWorld.Current.SuspendedShadowEntities;
// Rows withdrawn because a touched landblock streamed out. The owner may
// be seeded in an adjacent still-resident landblock, so its remaining rows
// cannot by themselves tell RefloodLandblock that this prefix needs repair.
private readonly Dictionary<uint, HashSet<uint>> _withdrawnPrefixesByOwner = new();
private Dictionary<uint, HashSet<uint>> _withdrawnPrefixesByOwner =>
_collisionWorld.Current.WithdrawnPrefixesByOwner;
/// <summary>
/// A6.P4 door fix (2026-05-24): per-entity original shape list, used by
/// <see cref="UpdatePosition"/> to recompose part world-transforms when
/// the entity moves. Cleared by <see cref="Deregister"/>.
/// </summary>
private readonly Dictionary<uint, System.Collections.Generic.IReadOnlyList<ShadowShape>> _entityShapes = new();
private Dictionary<uint, System.Collections.Generic.IReadOnlyList<ShadowShape>> _entityShapes =>
_collisionWorld.Current.ShadowEntityShapes;
/// <summary>
/// BR-7: per-entity registration arguments, kept so a registration can be
@ -50,9 +56,50 @@ public sealed class ShadowObjectRegistry
/// gets its cell set recomputed afterwards. <see cref="RefloodLandblock"/>
/// is the streaming-side trigger.
/// </summary>
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
private readonly Dictionary<uint, ulong> _ownerVersions = new();
private ulong _mutationVersion;
private Dictionary<uint, RegistrationRecord> _entityReg =>
_collisionWorld.Current.ShadowEntityRegistrations;
private Dictionary<uint, ulong> _ownerVersions =>
_collisionWorld.Current.ShadowOwnerVersions;
private Dictionary<uint, HashSet<uint>> _ownerPrefixes =>
_collisionWorld.Current.ShadowOwnerPrefixes;
private Dictionary<uint, List<uint>> _prefixOwnerSlots =>
_collisionWorld.Current.ShadowPrefixOwnerSlots;
private Dictionary<uint, Dictionary<uint, int>> _prefixOwnerIndices =>
_collisionWorld.Current.ShadowPrefixOwnerIndices;
private Dictionary<uint, Stack<int>> _prefixFreeSlots =>
_collisionWorld.Current.ShadowPrefixFreeSlots;
private List<uint> _ownerSlots =>
_collisionWorld.Current.ShadowOwnerSlots;
private Dictionary<uint, int> _ownerIndices =>
_collisionWorld.Current.ShadowOwnerIndices;
private Stack<int> _ownerFreeSlots =>
_collisionWorld.Current.ShadowOwnerFreeSlots;
private readonly HashSet<uint> _prefixScratch = new();
private readonly List<uint> _removedPrefixScratch = new();
internal event Action<uint, ulong>? OwnerMutated;
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
public ShadowObjectRegistry()
: this(new CollisionWorldStateSlot())
{
}
internal ShadowObjectRegistry(CollisionWorldStateSlot collisionWorld)
{
_collisionWorld = collisionWorld
?? throw new ArgumentNullException(nameof(collisionWorld));
}
internal void AttachCollisionWorld(CollisionWorldStateSlot collisionWorld)
{
ArgumentNullException.ThrowIfNull(collisionWorld);
if (_cells.Count != 0 || _entityReg.Count != 0)
{
throw new InvalidOperationException(
"A populated shadow registry cannot change collision roots.");
}
_collisionWorld = collisionWorld;
}
internal sealed record RegistrationRecord(
uint SeedCellId,
@ -76,31 +123,38 @@ public sealed class ShadowObjectRegistry
private void BumpOwnerVersion(uint entityId)
{
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
_mutationVersion = checked(_mutationVersion + 1UL);
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
_ownerVersions[entityId] = version;
RefreshOwnerPrefixIndex(entityId);
OwnerMutated?.Invoke(entityId, version);
}
internal ulong MutationVersion => _mutationVersion;
internal RetainedRefloodOwnerScan CreateRetainedRefloodOwnerScan(
uint landblockId) => new(this, landblockId & 0xFFFF0000u);
uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? slots);
return new RetainedRefloodOwnerScan(this, prefix, slots);
}
internal sealed class RetainedRefloodOwnerScan : IDisposable
{
private readonly ShadowObjectRegistry _owner;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
private Dictionary<uint, RegistrationRecord>.Enumerator _enumerator;
private readonly List<uint>? _slots;
private readonly int _limit;
private int _index;
private bool _completed;
internal RetainedRefloodOwnerScan(
ShadowObjectRegistry owner,
uint prefix)
uint prefix,
List<uint>? slots)
{
_owner = owner;
_prefix = prefix;
_sourceMutationVersion = owner.MutationVersion;
_enumerator = owner._entityReg.GetEnumerator();
_slots = slots;
_limit = slots?.Count ?? 0;
}
internal RetainedRefloodOwnerScanStep Advance()
@ -109,55 +163,169 @@ public sealed class ShadowObjectRegistry
{
return new RetainedRefloodOwnerScanStep(
Completed: true,
Stable: _owner.MutationVersion == _sourceMutationVersion,
HasOwner: false,
OwnerId: 0u,
SourceMutationVersion: _sourceMutationVersion);
OwnerId: 0u);
}
if (_owner.MutationVersion != _sourceMutationVersion)
if (_slots is null || _index >= _limit)
{
_completed = true;
return new RetainedRefloodOwnerScanStep(
Completed: true,
Stable: false,
HasOwner: false,
OwnerId: 0u,
SourceMutationVersion: _sourceMutationVersion);
}
if (!_enumerator.MoveNext())
{
_completed = true;
return new RetainedRefloodOwnerScanStep(
Completed: true,
Stable: true,
HasOwner: false,
OwnerId: 0u,
SourceMutationVersion: _sourceMutationVersion);
OwnerId: 0u);
}
(uint ownerId, RegistrationRecord registration) =
_enumerator.Current;
bool retained = !_owner._suspendedEntities.Contains(ownerId)
&& (!registration.IsStatic
|| (registration.SeedCellId & 0xFFFF0000u) != _prefix)
&& _owner.OwnerTouchesLandblock(ownerId, _prefix);
uint ownerId = _slots[_index++];
bool retained = _owner.IsRetainedRefloodOwner(ownerId, _prefix);
return new RetainedRefloodOwnerScanStep(
Completed: false,
Stable: true,
HasOwner: retained,
OwnerId: retained ? ownerId : 0u,
SourceMutationVersion: _sourceMutationVersion);
OwnerId: retained ? ownerId : 0u);
}
public void Dispose() => _enumerator.Dispose();
public void Dispose() { }
}
private void RefreshOwnerPrefixIndex(uint entityId)
{
if (!_entityReg.ContainsKey(entityId))
{
RemoveOwnerPrefixMembership(entityId);
return;
}
EnsureOwnerSlot(entityId);
_prefixScratch.Clear();
if (_entityReg.TryGetValue(entityId, out RegistrationRecord? registration))
_prefixScratch.Add(registration.SeedCellId & 0xFFFF0000u);
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells))
{
for (int index = 0; index < cells.Count; index++)
_prefixScratch.Add(cells[index] & 0xFFFF0000u);
}
if (_withdrawnPrefixesByOwner.TryGetValue(
entityId,
out HashSet<uint>? withdrawn))
{
foreach (uint prefix in withdrawn)
_prefixScratch.Add(prefix & 0xFFFF0000u);
}
if (!_ownerPrefixes.TryGetValue(entityId, out HashSet<uint>? current))
{
current = new HashSet<uint>();
_ownerPrefixes[entityId] = current;
}
_removedPrefixScratch.Clear();
foreach (uint prefix in current)
{
if (!_prefixScratch.Contains(prefix))
_removedPrefixScratch.Add(prefix);
}
for (int index = 0; index < _removedPrefixScratch.Count; index++)
{
uint prefix = _removedPrefixScratch[index];
current.Remove(prefix);
if (_prefixOwnerIndices.TryGetValue(
prefix,
out Dictionary<uint, int>? indices)
&& indices.Remove(entityId, out int slotIndex))
{
_prefixOwnerSlots[prefix][slotIndex] = 0u;
_prefixFreeSlots[prefix].Push(slotIndex);
ReleaseEmptyPrefixContainer(prefix, indices);
}
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
}
foreach (uint prefix in _prefixScratch)
{
if (!current.Add(prefix))
continue;
if (!_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? slots))
{
slots = new List<uint>();
_prefixOwnerSlots[prefix] = slots;
_prefixOwnerIndices[prefix] = new Dictionary<uint, int>();
_prefixFreeSlots[prefix] = new Stack<int>();
}
Dictionary<uint, int> indices = _prefixOwnerIndices[prefix];
if (indices.ContainsKey(entityId))
continue;
Stack<int> free = _prefixFreeSlots[prefix];
if (free.TryPop(out int freeIndex))
{
slots[freeIndex] = entityId;
indices[entityId] = freeIndex;
}
else
{
indices[entityId] = slots.Count;
slots.Add(entityId);
}
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
}
}
private void RemoveOwnerPrefixMembership(uint entityId)
{
if (_ownerPrefixes.Remove(entityId, out HashSet<uint>? prefixes))
{
foreach (uint prefix in prefixes)
{
if (!_prefixOwnerIndices.TryGetValue(
prefix,
out Dictionary<uint, int>? indices)
|| !indices.Remove(entityId, out int slotIndex))
{
continue;
}
_prefixOwnerSlots[prefix][slotIndex] = 0u;
_prefixFreeSlots[prefix].Push(slotIndex);
ReleaseEmptyPrefixContainer(prefix, indices);
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
}
}
if (_ownerIndices.Remove(entityId, out int ownerSlot))
{
_ownerSlots[ownerSlot] = 0u;
_ownerFreeSlots.Push(ownerSlot);
}
}
private void EnsureOwnerSlot(uint entityId)
{
if (_ownerIndices.ContainsKey(entityId))
return;
if (_ownerFreeSlots.TryPop(out int freeIndex))
{
_ownerSlots[freeIndex] = entityId;
_ownerIndices[entityId] = freeIndex;
return;
}
_ownerIndices[entityId] = _ownerSlots.Count;
_ownerSlots.Add(entityId);
}
private void ReleaseEmptyPrefixContainer(
uint prefix,
Dictionary<uint, int> indices)
{
if (indices.Count != 0)
return;
// An in-flight scan retains its captured List reference and observes
// only tombstones. A future owner gets a fresh compact container.
_prefixOwnerSlots.Remove(prefix);
_prefixOwnerIndices.Remove(prefix);
_prefixFreeSlots.Remove(prefix);
}
internal readonly record struct RetainedRefloodOwnerScanStep(
bool Completed,
bool Stable,
bool HasOwner,
uint OwnerId,
ulong SourceMutationVersion);
uint OwnerId);
/// <summary>
/// The flood's data source (cells, buildings, terrain origins). Wired by
@ -193,7 +361,8 @@ public sealed class ShadowObjectRegistry
uint state = 0u,
EntityCollisionFlags flags = EntityCollisionFlags.None,
uint seedCellId = 0u,
bool isStatic = true)
bool isStatic = true,
bool publishMutation = true)
{
// Flood FIRST: retail keeps the previous shadows when the new cell
// array would be empty (SetPositionInternal num_cells gate,
@ -211,7 +380,7 @@ public sealed class ShadowObjectRegistry
FloodCache, seed, spheres, spheres.Length, isStatic);
if (cellSet.Count == 0) return;
Deregister(entityId);
DeregisterCore(entityId, publishMutation: false);
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius,
collisionType, cylHeight, scale, state, flags);
@ -227,7 +396,10 @@ public sealed class ShadowObjectRegistry
_entityReg[entityId] = new RegistrationRecord(
seed, worldPos, rotation, state, flags, isStatic,
IsMultiPart: false, gfxObjId, radius, collisionType, cylHeight, scale);
BumpOwnerVersion(entityId);
if (publishMutation)
BumpOwnerVersion(entityId);
else
RefreshOwnerPrefixIndex(entityId);
}
/// <summary>
@ -256,7 +428,8 @@ public sealed class ShadowObjectRegistry
EntityCollisionFlags flags,
float worldOffsetX, float worldOffsetY, uint landblockId,
uint seedCellId = 0u,
bool isStatic = false)
bool isStatic = false,
bool publishMutation = true)
{
if (shapes.Count == 0) { Deregister(entityId); return; }
@ -271,7 +444,7 @@ public sealed class ShadowObjectRegistry
FloodCache, seed, floodSpheres, floodSpheres.Count, isStatic);
if (cellSet.Count == 0) return;
Deregister(entityId);
DeregisterCore(entityId, publishMutation: false);
_entityShapes[entityId] = shapes;
var allCells = new List<uint>(cellSet.Count);
@ -307,7 +480,10 @@ 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);
if (publishMutation)
BumpOwnerVersion(entityId);
else
RefreshOwnerPrefixIndex(entityId);
}
/// <summary>
@ -635,7 +811,8 @@ public sealed class ShadowObjectRegistry
0f,
lbPrefix,
reg.SeedCellId,
reg.IsStatic);
reg.IsStatic,
publishMutation: false);
}
else
{
@ -654,7 +831,8 @@ public sealed class ShadowObjectRegistry
reg.State,
reg.Flags,
reg.SeedCellId,
reg.IsStatic);
reg.IsStatic,
publishMutation: false);
}
// Register is also the authoritative movement/replacement API and
@ -674,6 +852,7 @@ public sealed class ShadowObjectRegistry
if (withdrawn.Count == 0)
_withdrawnPrefixesByOwner.Remove(entityId);
}
BumpOwnerVersion(entityId);
}
/// <summary>
@ -711,12 +890,16 @@ public sealed class ShadowObjectRegistry
if (retained)
{
_entityReg[entityId] = retainedRegistration! with { State = newState };
BumpOwnerVersion(entityId);
}
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
{
if (retained)
BumpOwnerVersion(entityId);
return; // not registered — no-op
}
foreach (var cellId in cellIds)
{
if (!_cells.TryGetValue(cellId, out var list)) continue;
@ -727,10 +910,16 @@ public sealed class ShadowObjectRegistry
}
}
if (retained)
BumpOwnerVersion(entityId);
}
/// <summary>Remove an entity from all cells it was registered in.</summary>
public void Deregister(uint entityId)
=> DeregisterCore(entityId, publishMutation: true);
private void DeregisterCore(uint entityId, bool publishMutation)
{
bool existed = _entityReg.ContainsKey(entityId)
|| _entityToCells.ContainsKey(entityId)
@ -749,8 +938,12 @@ public sealed class ShadowObjectRegistry
_entityReg.Remove(entityId);
_suspendedEntities.Remove(entityId);
_withdrawnPrefixesByOwner.Remove(entityId);
if (existed)
if (existed && publishMutation)
{
BumpOwnerVersion(entityId);
RemoveOwnerPrefixMembership(entityId);
_ownerVersions.Remove(entityId);
}
}
private static void RemoveOwnerRows(
@ -879,6 +1072,59 @@ public sealed class ShadowObjectRegistry
BumpOwnerVersion(entityId);
}
/// <summary>
/// Retires one logical owner's rows from a streamed-out prefix. This is
/// the owner-granular form used by the collision-generation retirement
/// cursor; it preserves the same static/dynamic lifetime rules as
/// <see cref="RemoveLandblock"/> without scanning the complete registry.
/// </summary>
internal void RetireOwnerFromLandblock(uint entityId, uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
if (_entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == prefix)
{
DeregisterCore(entityId, publishMutation: false);
RemoveOwnerPrefixMembership(entityId);
_ownerVersions.Remove(entityId);
return;
}
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
return;
bool touched = false;
for (int index = cells.Count - 1; index >= 0; index--)
{
uint cellId = cells[index];
if ((cellId & 0xFFFF0000u) != prefix)
continue;
touched = true;
cells.RemoveAt(index);
if (_cells.TryGetValue(cellId, out List<ShadowEntry>? entries))
{
RemoveOwnerRows(entries, entityId);
if (entries.Count == 0)
_cells.Remove(cellId);
}
}
if (!touched)
return;
if (!_withdrawnPrefixesByOwner.TryGetValue(
entityId,
out HashSet<uint>? withdrawn))
{
withdrawn = new HashSet<uint>();
_withdrawnPrefixesByOwner[entityId] = withdrawn;
}
withdrawn.Add(prefix);
if (cells.Count == 0)
_entityToCells.Remove(entityId);
BumpOwnerVersion(entityId);
}
/// <summary>
/// All objects registered in a specific cell — retail
/// <c>CObjCell::find_obj_collisions</c> iterating only
@ -925,44 +1171,35 @@ public sealed class ShadowObjectRegistry
(cell & 0xFFFF0000u) == (landblockId & 0xFFFF0000u));
/// <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.
/// Mirrors one ordinary active-world mutation into an off-side generation.
/// Target-prefix owners may subsequently be reflooded against the staged
/// topology; unrelated owners retain these exact active rows.
/// </summary>
internal void CopyCollisionStateFrom(
internal void MirrorOwnerFrom(
ShadowObjectRegistry source,
PhysicsDataCache stagingCache)
uint entityId)
{
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)
DeregisterCore(entityId, publishMutation: false);
if (source.TryCaptureOwnerState(
entityId,
out PreparedShadowOwnerState? state)
&& state is not null)
{
_withdrawnPrefixesByOwner[ownerId] = new HashSet<uint>(prefixes);
InstallOwnerState(state);
_ownerVersions[entityId] = source.GetOwnerVersion(entityId);
}
foreach ((uint ownerId, IReadOnlyList<ShadowShape> shapes) in
source._entityShapes)
else
{
_entityShapes[ownerId] = shapes;
RemoveOwnerPrefixMembership(entityId);
_ownerVersions.Remove(entityId);
}
foreach ((uint ownerId, RegistrationRecord registration) in
source._entityReg)
{
_entityReg[ownerId] = registration;
}
foreach ((uint ownerId, ulong version) in source._ownerVersions)
_ownerVersions[ownerId] = version;
_mutationVersion = source._mutationVersion;
}
internal int CaptureOwnerSlotLimit() => _ownerSlots.Count;
internal uint GetOwnerSlot(int index) => _ownerSlots[index];
/// <summary>
/// Refreshes one staging owner from the exact active payload, then floods
/// it against the staging generation's complete cell graph. The returned
@ -975,19 +1212,27 @@ public sealed class ShadowObjectRegistry
out ulong sourceVersion)
{
ArgumentNullException.ThrowIfNull(source);
Deregister(entityId);
sourceVersion = source.GetOwnerVersion(entityId);
if (!source._entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
|| source._suspendedEntities.Contains(entityId)
|| (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u))
|| !source.OwnerTouchesLandblock(entityId, landblockId))
{
// A target-local refresh is not a global owner deletion. Preserve
// the exact active rows when the live owner has moved elsewhere.
MirrorOwnerFrom(source, entityId);
return false;
}
if (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u))
{
// Target statics come from the staged landblock itself.
return false;
}
DeregisterCore(entityId, publishMutation: false);
if (registration.IsMultiPart
&& source._entityShapes.TryGetValue(
@ -1005,7 +1250,8 @@ public sealed class ShadowObjectRegistry
0f,
landblockId,
registration.SeedCellId,
isStatic: registration.IsStatic);
isStatic: registration.IsStatic,
publishMutation: false);
}
else
{
@ -1024,7 +1270,8 @@ public sealed class ShadowObjectRegistry
registration.State,
registration.Flags,
registration.SeedCellId,
isStatic: registration.IsStatic);
isStatic: registration.IsStatic,
publishMutation: false);
}
if (source._withdrawnPrefixesByOwner.TryGetValue(
@ -1041,28 +1288,21 @@ public sealed class ShadowObjectRegistry
if (retainedWithdrawn.Count != 0)
_withdrawnPrefixesByOwner[entityId] = retainedWithdrawn;
}
RefreshOwnerPrefixIndex(entityId);
_ownerVersions[entityId] = sourceVersion;
return true;
}
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions) => new(
IReadOnlyList<uint> expectedRetainedOwners) => new(
this,
staging,
landblockId,
expectedRetainedVersions);
expectedRetainedOwners);
internal void CommitLandblockReplacement(
PreparedLandblockShadowReplacement replacement)
{
for (int index = 0; index < replacement.OwnerIds.Count; index++)
Deregister(replacement.OwnerIds[index]);
for (int index = 0; index < replacement.OwnerStates.Count; index++)
InstallOwnerState(replacement.OwnerStates[index]);
}
private bool OwnerTouchesLandblock(uint entityId, uint landblockId)
internal bool OwnerTouchesLandblock(uint entityId, uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? record))
@ -1080,6 +1320,43 @@ public sealed class ShadowObjectRegistry
&& withdrawn.Contains(prefix);
}
internal bool IsStaticOwnerRootedIn(uint entityId, uint landblockId) =>
_entityReg.TryGetValue(entityId, out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u);
internal bool TryGetStaticOwnerRootPrefix(
uint entityId,
out uint landblockPrefix)
{
if (_entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
&& registration.IsStatic)
{
landblockPrefix = registration.SeedCellId & 0xFFFF0000u;
return true;
}
landblockPrefix = 0u;
return false;
}
internal bool HasLogicalOwner(uint entityId) =>
_entityReg.ContainsKey(entityId);
public int PrefixOwnerSlotCapacityForDiagnostics(uint landblockId) =>
_prefixOwnerSlots.TryGetValue(
landblockId & 0xFFFF0000u,
out List<uint>? slots)
? slots.Count
: 0;
public int OwnerVersionCountForDiagnostics => _ownerVersions.Count;
public int PrefixOwnerContainerCountForDiagnostics =>
_prefixOwnerSlots.Count;
private bool TryCaptureOwnerState(
uint entityId,
out PreparedShadowOwnerState? state)
@ -1148,63 +1425,65 @@ public sealed class ShadowObjectRegistry
private readonly ShadowObjectRegistry _active;
private readonly ShadowObjectRegistry _staging;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
private readonly IReadOnlyList<uint> _expected;
private readonly List<uint>? _activeSlots;
private readonly List<uint>? _stagingSlots;
private readonly int _activeSlotLimit;
private readonly int _stagingSlotLimit;
private readonly HashSet<uint> _owners = new();
private readonly List<uint> _ownerIds = new();
private readonly List<PreparedShadowOwnerState> _states = new();
private IEnumerator<KeyValuePair<uint, ulong>>? _expectedEnumerator;
private Dictionary<uint, RegistrationRecord>.Enumerator _registrationEnumerator;
private HashSet<uint>.Enumerator _ownerEnumerator;
private readonly List<PreparedShadowOwnerSlot> _states = new();
private readonly Dictionary<uint, int> _stateIndex = new();
private int _expectedIndex;
private int _activeSlotIndex;
private int _stagingSlotIndex;
private int _ownerIndex;
private int _phase;
internal LandblockReplacementBuilder(
ShadowObjectRegistry active,
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expected)
IReadOnlyList<uint> expected)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_sourceMutationVersion = active.MutationVersion;
_expectedEnumerator = expected.GetEnumerator();
_expected = expected;
active._prefixOwnerSlots.TryGetValue(
_prefix,
out _activeSlots);
staging._prefixOwnerSlots.TryGetValue(
_prefix,
out _stagingSlots);
_activeSlotLimit = _activeSlots?.Count ?? 0;
_stagingSlotLimit = _stagingSlots?.Count ?? 0;
}
internal int WorkUnits { get; private set; }
internal bool IsStable =>
_active.MutationVersion == _sourceMutationVersion;
internal PreparedLandblockShadowReplacement? Prepared { get; private set; }
internal bool Advance()
{
if (!IsStable)
return true;
switch (_phase)
{
case 0:
if (_expectedEnumerator!.MoveNext())
if (_expectedIndex < _expected.Count)
{
(uint ownerId, ulong version) = _expectedEnumerator.Current;
if (_active.GetOwnerVersion(ownerId) != version
|| !_active.IsRetainedRefloodOwner(ownerId, _prefix))
{
return true;
}
AddOwner(ownerId);
AddOwner(_expected[_expectedIndex++]);
WorkUnits++;
return false;
}
_expectedEnumerator.Dispose();
_expectedEnumerator = null;
_registrationEnumerator = _active._entityReg.GetEnumerator();
_phase++;
return false;
case 1:
if (_registrationEnumerator.MoveNext())
if (_activeSlotIndex < _activeSlotLimit)
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
uint ownerId = _activeSlots![_activeSlotIndex++];
if (_active._entityReg.TryGetValue(
ownerId,
out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
@ -1212,16 +1491,16 @@ public sealed class ShadowObjectRegistry
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_registrationEnumerator = _staging._entityReg.GetEnumerator();
_phase++;
return false;
case 2:
if (_registrationEnumerator.MoveNext())
if (_stagingSlotIndex < _stagingSlotLimit)
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
uint ownerId = _stagingSlots![_stagingSlotIndex++];
if (_staging._entityReg.TryGetValue(
ownerId,
out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
@ -1229,32 +1508,24 @@ public sealed class ShadowObjectRegistry
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_ownerEnumerator = _owners.GetEnumerator();
_phase++;
return false;
case 3:
if (_ownerEnumerator.MoveNext())
if (_ownerIndex < _ownerIds.Count)
{
uint ownerId = _ownerEnumerator.Current;
if (_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state)
&& state is not null)
{
_states.Add(state);
}
uint ownerId = _ownerIds[_ownerIndex++];
_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state);
_stateIndex[ownerId] = _states.Count;
_states.Add(new PreparedShadowOwnerSlot(ownerId, state));
WorkUnits++;
return false;
}
_ownerEnumerator.Dispose();
if (IsStable)
{
Prepared = new PreparedLandblockShadowReplacement(
_prefix,
_ownerIds,
_states);
}
Prepared = new PreparedLandblockShadowReplacement(
_prefix,
_ownerIds,
_states);
_phase++;
return true;
default:
@ -1262,20 +1533,34 @@ public sealed class ShadowObjectRegistry
}
}
private void AddOwner(uint ownerId)
internal void AddOwner(uint ownerId)
{
if (_owners.Add(ownerId))
_ownerIds.Add(ownerId);
}
public void Dispose()
internal void RefreshOwner(uint ownerId)
{
_expectedEnumerator?.Dispose();
if (_phase is 1 or 2)
_registrationEnumerator.Dispose();
if (_phase == 3)
_ownerEnumerator.Dispose();
AddOwner(ownerId);
if (_stateIndex.TryGetValue(ownerId, out int index))
{
_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state);
_states[index].State = state;
return;
}
if (_phase > 3)
{
_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state);
_stateIndex[ownerId] = _states.Count;
_states.Add(new PreparedShadowOwnerSlot(ownerId, state));
}
}
public void Dispose() { }
}
private bool IsRetainedRefloodOwner(uint ownerId, uint landblockId)
@ -1296,7 +1581,7 @@ public sealed class ShadowObjectRegistry
internal PreparedLandblockShadowReplacement(
uint landblockPrefix,
IReadOnlyList<uint> ownerIds,
IReadOnlyList<PreparedShadowOwnerState> ownerStates)
IReadOnlyList<PreparedShadowOwnerSlot> ownerStates)
{
LandblockPrefix = landblockPrefix;
OwnerIds = ownerIds;
@ -1305,7 +1590,21 @@ public sealed class ShadowObjectRegistry
internal uint LandblockPrefix { get; }
internal IReadOnlyList<uint> OwnerIds { get; }
internal IReadOnlyList<PreparedShadowOwnerState> OwnerStates { get; }
internal IReadOnlyList<PreparedShadowOwnerSlot> OwnerStates { get; }
}
internal sealed class PreparedShadowOwnerSlot
{
internal PreparedShadowOwnerSlot(
uint entityId,
PreparedShadowOwnerState? state)
{
EntityId = entityId;
State = state;
}
internal uint EntityId { get; }
internal PreparedShadowOwnerState? State { get; set; }
}
internal sealed record PreparedShadowOwnerState(
@ -1334,7 +1633,15 @@ public sealed class ShadowObjectRegistry
_entityShapes.Clear();
_entityReg.Clear();
_ownerVersions.Clear();
_mutationVersion = 0UL;
_ownerPrefixes.Clear();
_prefixOwnerSlots.Clear();
_prefixOwnerIndices.Clear();
_prefixFreeSlots.Clear();
_ownerSlots.Clear();
_ownerIndices.Clear();
_ownerFreeSlots.Clear();
_prefixScratch.Clear();
_removedPrefixScratch.Clear();
_fallback = null;
}