feat(runtime): commit dormant SetPosition activation

This commit is contained in:
Erik 2026-08-01 14:25:02 +02:00
parent 99f867f053
commit 5785a07b3e
13 changed files with 4674 additions and 111 deletions

View file

@ -79,6 +79,10 @@ public sealed class ShadowObjectRegistry
private readonly HashSet<uint> _prefixScratch = new();
private readonly List<uint> _removedPrefixScratch = new();
private ulong _mutationRevision;
private ulong _nextPreparedSetPositionCommitId;
private ulong _lastAppliedSetPositionCommitId;
private readonly HashSet<ulong> _pendingSetPositionDispatches = [];
private long _setPositionDispatchFailureCount;
internal event Action<uint, ulong>? OwnerMutated;
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
@ -783,6 +787,553 @@ public sealed class ShadowObjectRegistry
}
}
/// <summary>
/// Immutable owner-local shadow transaction prepared before Runtime's
/// SetPosition publication tail. The prepared rows are built against an
/// isolated registry; applying them never re-runs the flood oracle.
/// </summary>
internal sealed record PreparedSetPositionShadowCommit(
ulong CommitId,
uint EntityId,
ulong ExpectedMutationRevision,
ulong ExpectedOwnerVersion,
ulong FinalMutationRevision,
ulong FinalOwnerVersion,
bool ProvenShapeless,
PreparedShadowOwnerState? OwnerState,
PreparedShadowCellReplacement[] CellReplacements,
PreparedShadowPrefixReplacement[] PrefixReplacements,
HashSet<uint>? OwnerPrefixes,
uint[] ChangedPrefixes);
internal sealed record PreparedShadowCellReplacement(
uint CellId,
List<ShadowEntry> Entries);
internal sealed record PreparedShadowPrefixReplacement(
uint Prefix,
bool Remove,
List<uint>? Slots,
Dictionary<uint, int>? Indices,
Stack<int>? FreeSlots);
internal readonly record struct SetPositionShadowCommitReceipt(
ulong CommitId,
uint EntityId,
ulong OwnerVersion,
uint[] ChangedPrefixes,
bool Mutated)
{
internal bool IsValid => CommitId != 0UL && EntityId != 0u;
}
/// <summary>
/// Prepares the complete owner-row replacement without touching the active
/// collision world. A missing registration is accepted only when the
/// caller carries an explicit proven-shapeless disposition; absence alone
/// is not evidence because an authored BSP payload may still be pending.
/// </summary>
internal bool TryPrepareSetPosition(
uint entityId,
Vector3 worldPosition,
Quaternion worldRotation,
uint seedCellId,
float worldOffsetX,
float worldOffsetY,
PhysicsShadowCommitAction action,
System.Collections.Immutable.ImmutableArray<uint> crossCellIds,
bool provenShapeless,
bool suspendOwner,
out PreparedSetPositionShadowCommit? prepared)
{
prepared = null;
ulong expectedMutation = _mutationRevision;
ulong expectedOwner = GetOwnerVersion(entityId);
bool hasOwner = TryCaptureOwnerState(
entityId,
out PreparedShadowOwnerState? source);
if (!hasOwner)
{
if (!provenShapeless)
return false;
_pendingSetPositionDispatches.EnsureCapacity(
_pendingSetPositionDispatches.Count + 1);
prepared = new PreparedSetPositionShadowCommit(
checked(++_nextPreparedSetPositionCommitId),
entityId,
expectedMutation,
expectedOwner,
expectedMutation,
expectedOwner,
ProvenShapeless: true,
OwnerState: null,
CellReplacements: [],
PrefixReplacements: [],
OwnerPrefixes: null,
ChangedPrefixes: Array.Empty<uint>());
return _mutationRevision == expectedMutation
&& GetOwnerVersion(entityId) == expectedOwner
&& !HasLogicalOwner(entityId);
}
if (provenShapeless || source is null)
return false;
var staging = new ShadowObjectRegistry
{
DataCache = DataCache,
};
staging.InstallOwnerState(source);
if (suspendOwner)
{
if (!staging.Suspend(entityId))
return false;
}
else
{
staging.CommitSetPosition(
entityId,
worldPosition,
worldRotation,
seedCellId,
worldOffsetX,
worldOffsetY,
action,
crossCellIds);
}
if (!staging.TryCaptureOwnerState(
entityId,
out PreparedShadowOwnerState? replacement)
|| replacement is null)
{
return false;
}
uint[] changedPrefixes = CaptureChangedPrefixes(source, replacement);
PreparedShadowCellReplacement[] cellReplacements =
PrepareCellReplacements(entityId, source, replacement);
HashSet<uint> replacementPrefixes = CapturePrefixes(replacement);
PreparedShadowPrefixReplacement[] prefixReplacements =
PreparePrefixReplacements(
entityId,
CapturePrefixes(source),
replacementPrefixes,
changedPrefixes);
ulong finalMutation = checked(expectedMutation + 1UL);
ulong finalOwner = checked(expectedOwner + 1UL);
// Reserve dictionary capacity before the non-fallible publication
// suffix. Row/list payloads themselves were allocated in staging.
_cells.EnsureCapacity(_cells.Count + replacement.Rows.Count);
_entityToCells.EnsureCapacity(_entityToCells.Count + 1);
_entityReg.EnsureCapacity(_entityReg.Count + 1);
_entityShapes.EnsureCapacity(_entityShapes.Count + 1);
_suspendedEntityCells.EnsureCapacity(_suspendedEntityCells.Count + 1);
_withdrawnPrefixesByOwner.EnsureCapacity(
_withdrawnPrefixesByOwner.Count + 1);
_ownerVersions.EnsureCapacity(_ownerVersions.Count + 1);
_ownerPrefixes.EnsureCapacity(_ownerPrefixes.Count + 1);
_prefixOwnerSlots.EnsureCapacity(
_prefixOwnerSlots.Count + changedPrefixes.Length);
_prefixOwnerIndices.EnsureCapacity(
_prefixOwnerIndices.Count + changedPrefixes.Length);
_prefixFreeSlots.EnsureCapacity(
_prefixFreeSlots.Count + changedPrefixes.Length);
_suspendedEntities.EnsureCapacity(_suspendedEntities.Count + 1);
_pendingSetPositionDispatches.EnsureCapacity(
_pendingSetPositionDispatches.Count + 1);
prepared = new PreparedSetPositionShadowCommit(
checked(++_nextPreparedSetPositionCommitId),
entityId,
expectedMutation,
expectedOwner,
finalMutation,
finalOwner,
ProvenShapeless: false,
replacement,
cellReplacements,
prefixReplacements,
replacementPrefixes,
changedPrefixes);
return _mutationRevision == expectedMutation
&& GetOwnerVersion(entityId) == expectedOwner
&& HasLogicalOwner(entityId);
}
/// <summary>
/// Applies a previously prepared owner-local row swap with callbacks
/// suppressed. Runtime dispatches the returned exact notification only
/// after the complete SetPosition state suffix is visible.
/// </summary>
internal bool TryApplySetPosition(
PreparedSetPositionShadowCommit prepared,
out SetPositionShadowCommitReceipt receipt)
{
ArgumentNullException.ThrowIfNull(prepared);
receipt = default;
if (prepared.CommitId <= _lastAppliedSetPositionCommitId
|| _mutationRevision != prepared.ExpectedMutationRevision
|| GetOwnerVersion(prepared.EntityId)
!= prepared.ExpectedOwnerVersion
|| HasLogicalOwner(prepared.EntityId)
== prepared.ProvenShapeless)
{
return false;
}
if (prepared.ProvenShapeless)
{
receipt = new SetPositionShadowCommitReceipt(
prepared.CommitId,
prepared.EntityId,
prepared.ExpectedOwnerVersion,
Array.Empty<uint>(),
Mutated: false);
_lastAppliedSetPositionCommitId = prepared.CommitId;
_pendingSetPositionDispatches.Add(prepared.CommitId);
return true;
}
if (prepared.OwnerState is null)
return false;
for (int index = 0; index < prepared.CellReplacements.Length; index++)
{
PreparedShadowCellReplacement replacement =
prepared.CellReplacements[index];
_cells[replacement.CellId] = replacement.Entries;
}
PreparedShadowOwnerState state = prepared.OwnerState;
_entityReg[prepared.EntityId] = state.Registration;
ReplaceOwnerValue(_entityShapes, prepared.EntityId, state.Shapes);
if (state.Suspended)
_suspendedEntities.Add(prepared.EntityId);
else
_suspendedEntities.Remove(prepared.EntityId);
ReplaceOwnerValue(
_suspendedEntityCells,
prepared.EntityId,
state.SuspendedCellIds);
ReplaceOwnerValue(
_withdrawnPrefixesByOwner,
prepared.EntityId,
state.WithdrawnPrefixes);
ReplaceOwnerValue(
_entityToCells,
prepared.EntityId,
state.CellIds);
if (prepared.OwnerPrefixes is not null)
_ownerPrefixes[prepared.EntityId] = prepared.OwnerPrefixes;
for (int index = 0; index < prepared.PrefixReplacements.Length; index++)
{
PreparedShadowPrefixReplacement replacement =
prepared.PrefixReplacements[index];
if (replacement.Remove)
{
_prefixOwnerSlots.Remove(replacement.Prefix);
_prefixOwnerIndices.Remove(replacement.Prefix);
_prefixFreeSlots.Remove(replacement.Prefix);
continue;
}
_prefixOwnerSlots[replacement.Prefix] = replacement.Slots!;
_prefixOwnerIndices[replacement.Prefix] = replacement.Indices!;
_prefixFreeSlots[replacement.Prefix] = replacement.FreeSlots!;
}
_mutationRevision = prepared.FinalMutationRevision;
_ownerVersions[prepared.EntityId] = prepared.FinalOwnerVersion;
_lastAppliedSetPositionCommitId = prepared.CommitId;
_pendingSetPositionDispatches.Add(prepared.CommitId);
receipt = new SetPositionShadowCommitReceipt(
prepared.CommitId,
prepared.EntityId,
prepared.FinalOwnerVersion,
prepared.ChangedPrefixes,
Mutated: true);
return true;
}
internal bool IsPreparedSetPositionCurrent(
PreparedSetPositionShadowCommit prepared)
{
ArgumentNullException.ThrowIfNull(prepared);
return prepared.CommitId > _lastAppliedSetPositionCommitId
&& _mutationRevision == prepared.ExpectedMutationRevision
&& GetOwnerVersion(prepared.EntityId)
== prepared.ExpectedOwnerVersion
&& HasLogicalOwner(prepared.EntityId)
!= prepared.ProvenShapeless;
}
internal void DispatchSetPositionCommit(
in SetPositionShadowCommitReceipt receipt)
{
if (!receipt.IsValid
|| receipt.CommitId > _lastAppliedSetPositionCommitId
|| !_pendingSetPositionDispatches.Remove(receipt.CommitId))
return;
if (!receipt.Mutated)
return;
ulong currentOwnerVersion = GetOwnerVersion(receipt.EntityId);
if (!HasLogicalOwner(receipt.EntityId)
|| currentOwnerVersion != receipt.OwnerVersion)
{
return;
}
for (int index = 0; index < receipt.ChangedPrefixes.Length; index++)
{
if (!HasLogicalOwner(receipt.EntityId)
|| GetOwnerVersion(receipt.EntityId) != receipt.OwnerVersion)
{
return;
}
DispatchSetPositionPrefixObservers(
receipt.EntityId,
receipt.ChangedPrefixes[index]);
}
if (!HasLogicalOwner(receipt.EntityId))
return;
currentOwnerVersion = GetOwnerVersion(receipt.EntityId);
if (currentOwnerVersion != receipt.OwnerVersion)
return;
DispatchSetPositionOwnerObservers(
receipt.EntityId,
currentOwnerVersion);
}
internal bool DiscardSetPositionCommit(
in SetPositionShadowCommitReceipt receipt) =>
receipt.IsValid
&& _pendingSetPositionDispatches.Remove(receipt.CommitId);
internal int PendingSetPositionDispatchCount =>
_pendingSetPositionDispatches.Count;
internal long SetPositionDispatchFailureCount =>
_setPositionDispatchFailureCount;
private void DispatchSetPositionPrefixObservers(uint owner, uint prefix)
{
Action<uint, uint>? observers = OwnerPrefixMembershipChanged;
if (observers is null)
return;
foreach (Action<uint, uint> observer in observers.GetInvocationList())
{
try
{
observer(owner, prefix);
}
catch
{
_setPositionDispatchFailureCount++;
}
}
}
private void DispatchSetPositionOwnerObservers(uint owner, ulong version)
{
Action<uint, ulong>? observers = OwnerMutated;
if (observers is null)
return;
foreach (Action<uint, ulong> observer in observers.GetInvocationList())
{
try
{
observer(owner, version);
}
catch
{
_setPositionDispatchFailureCount++;
}
}
}
private static uint[] CaptureChangedPrefixes(
PreparedShadowOwnerState before,
PreparedShadowOwnerState after)
{
HashSet<uint> oldPrefixes = CapturePrefixes(before);
HashSet<uint> newPrefixes = CapturePrefixes(after);
var changed = new List<uint>();
foreach (uint prefix in oldPrefixes)
{
if (!newPrefixes.Contains(prefix))
changed.Add(prefix);
}
foreach (uint prefix in newPrefixes)
{
if (!oldPrefixes.Contains(prefix))
changed.Add(prefix);
}
changed.Sort();
return changed.ToArray();
}
private static HashSet<uint> CapturePrefixes(
PreparedShadowOwnerState state)
{
var prefixes = new HashSet<uint>
{
state.Registration.SeedCellId & 0xFFFF0000u,
};
if (state.CellIds is not null)
{
for (int index = 0; index < state.CellIds.Count; index++)
prefixes.Add(state.CellIds[index] & 0xFFFF0000u);
}
if (state.WithdrawnPrefixes is not null)
{
foreach (uint prefix in state.WithdrawnPrefixes)
prefixes.Add(prefix & 0xFFFF0000u);
}
return prefixes;
}
private PreparedShadowCellReplacement[] PrepareCellReplacements(
uint entityId,
PreparedShadowOwnerState before,
PreparedShadowOwnerState after)
{
var touched = new HashSet<uint>();
AddCells(touched, before.CellIds);
AddCells(touched, after.CellIds);
var afterRows = new Dictionary<uint, ShadowEntry[]>();
for (int index = 0; index < after.Rows.Count; index++)
{
PreparedShadowCellRows row = after.Rows[index];
touched.Add(row.CellId);
afterRows[row.CellId] = row.Entries;
}
for (int index = 0; index < before.Rows.Count; index++)
touched.Add(before.Rows[index].CellId);
uint[] ordered = touched.ToArray();
Array.Sort(ordered);
var result = new PreparedShadowCellReplacement[ordered.Length];
for (int index = 0; index < ordered.Length; index++)
{
uint cellId = ordered[index];
_cells.TryGetValue(cellId, out List<ShadowEntry>? active);
afterRows.TryGetValue(cellId, out ShadowEntry[]? ownerRows);
int retainedCount = 0;
if (active is not null)
{
for (int row = 0; row < active.Count; row++)
{
if (active[row].EntityId != entityId)
retainedCount++;
}
}
var replacement = new List<ShadowEntry>(
retainedCount + (ownerRows?.Length ?? 0));
if (active is not null)
{
for (int row = 0; row < active.Count; row++)
{
if (active[row].EntityId != entityId)
replacement.Add(active[row]);
}
}
if (ownerRows is not null)
replacement.AddRange(ownerRows);
result[index] = new PreparedShadowCellReplacement(
cellId,
replacement);
}
return result;
}
private PreparedShadowPrefixReplacement[] PreparePrefixReplacements(
uint entityId,
HashSet<uint> before,
HashSet<uint> after,
uint[] changedPrefixes)
{
var result = new PreparedShadowPrefixReplacement[
changedPrefixes.Length];
for (int index = 0; index < changedPrefixes.Length; index++)
{
uint prefix = changedPrefixes[index];
bool removeOwner = before.Contains(prefix)
&& !after.Contains(prefix);
_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? oldSlots);
_prefixOwnerIndices.TryGetValue(
prefix,
out Dictionary<uint, int>? oldIndices);
_prefixFreeSlots.TryGetValue(prefix, out Stack<int>? oldFree);
var slots = oldSlots is null ? [] : new List<uint>(oldSlots);
var indices = oldIndices is null
? new Dictionary<uint, int>()
: new Dictionary<uint, int>(oldIndices);
Stack<int> free = CloneStack(oldFree);
if (removeOwner)
{
if (indices.Remove(entityId, out int ownerSlot))
{
slots[ownerSlot] = 0u;
free.Push(ownerSlot);
}
result[index] = indices.Count == 0
? new PreparedShadowPrefixReplacement(
prefix,
Remove: true,
Slots: null,
Indices: null,
FreeSlots: null)
: new PreparedShadowPrefixReplacement(
prefix,
Remove: false,
slots,
indices,
free);
continue;
}
if (!indices.ContainsKey(entityId))
{
if (free.TryPop(out int freeIndex))
{
slots[freeIndex] = entityId;
indices[entityId] = freeIndex;
}
else
{
indices[entityId] = slots.Count;
slots.Add(entityId);
}
}
result[index] = new PreparedShadowPrefixReplacement(
prefix,
Remove: false,
slots,
indices,
free);
}
return result;
}
private static Stack<int> CloneStack(Stack<int>? source) =>
source is null
? new Stack<int>()
: new Stack<int>(source.Reverse());
private static void AddCells(HashSet<uint> destination, List<uint>? cells)
{
if (cells is null)
return;
for (int index = 0; index < cells.Count; index++)
destination.Add(cells[index]);
}
private static void ReplaceOwnerValue<T>(
Dictionary<uint, T> destination,
uint entityId,
T? value)
where T : class
{
if (value is null)
destination.Remove(entityId);
else
destination[entityId] = value;
}
private void RefreshPositionRows(
uint entityId,
RegistrationRecord registration,
@ -1896,7 +2447,9 @@ public sealed class ShadowObjectRegistry
|| _entityToCells.Count != 0
|| _entityReg.Count != 0
|| _suspendedEntities.Count != 0
|| _suspendedEntityCells.Count != 0;
|| _suspendedEntityCells.Count != 0
|| _nextPreparedSetPositionCommitId
!= _lastAppliedSetPositionCommitId;
if (mutated)
AdvanceMutationRevision();
_cells.Clear();
@ -1916,6 +2469,7 @@ public sealed class ShadowObjectRegistry
_ownerFreeSlots.Clear();
_prefixScratch.Clear();
_removedPrefixScratch.Clear();
_pendingSetPositionDispatches.Clear();
_fallback = null;
}