fix(physics): seal collision generations before activation

This commit is contained in:
Erik 2026-07-31 15:53:05 +02:00
parent be94bc9b06
commit d94145e6b8
15 changed files with 1556 additions and 410 deletions

View file

@ -21,6 +21,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
uint currentCellId,
BuildingInfo[] buildings,
uint[] priorStaticOwnerIds,
RuntimePhysicsState physics,
RuntimeCollisionAdmission collisionAdmission,
PreparedLandblockCollisionGeneration preparedGeneration)
{
@ -30,6 +31,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
CurrentCellId = currentCellId;
Buildings = buildings;
PriorStaticOwnerIds = priorStaticOwnerIds;
Physics = physics;
CollisionAdmission = collisionAdmission;
PreparedGeneration = preparedGeneration;
}
@ -39,6 +41,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
internal uint CurrentCellId { get; }
internal BuildingInfo[] Buildings { get; }
internal uint[] PriorStaticOwnerIds { get; }
internal RuntimePhysicsState Physics { get; }
internal RuntimeCollisionAdmission CollisionAdmission { get; }
internal PreparedLandblockCollisionGeneration PreparedGeneration { get; }
internal PhysicsDataCache StagingCache => PreparedGeneration.DataCache;
@ -63,16 +66,19 @@ public sealed class LandblockPhysicsPublication : IDisposable
internal int CylinderOwnerCount { get; set; }
internal int NoCollisionCount { get; set; }
internal int SceneryTried { get; set; }
internal uint[]? RefloodOwnerIds { get; set; }
internal IReadOnlyList<uint>? RefloodOwnerIds { get; set; }
internal int RefloodCursor { get; set; }
internal bool RefloodCommitted { get; set; }
internal bool SealCommitted { get; set; }
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
public void Dispose()
{
if (!CompletionCommitted)
PreparedGeneration.Dispose();
Physics.CancelCollisionGeneration(
CollisionAdmission,
PreparedGeneration);
}
public uint LandblockId => Build.Landblock.LandblockId;
@ -217,23 +223,34 @@ public sealed class LandblockPhysicsPublisher
?? Array.Empty<BuildingInfo>();
RuntimeCollisionAdmission collisionAdmission =
_physics.BeginCollisionAdmission(build.Landblock.LandblockId);
var publication = new LandblockPhysicsPublication(
_receiptOwner,
build,
origin,
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
buildings,
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
build.Landblock.LandblockId),
collisionAdmission,
_physics.PrepareCollisionGeneration(collisionAdmission));
publication.SetupObjectIds = build.Collisions is { } collisions
? [.. collisions.SetupIds]
: datBundle.Setups.Keys.Order().ToArray();
publication.PreparedGeneration.SetAssetClosure(
publication.GfxObjectIds,
publication.SetupObjectIds);
return publication;
PreparedLandblockCollisionGeneration? prepared = null;
try
{
prepared = _physics.PrepareCollisionGeneration(collisionAdmission);
var publication = new LandblockPhysicsPublication(
_receiptOwner,
build,
origin,
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
buildings,
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
build.Landblock.LandblockId),
_physics,
collisionAdmission,
prepared);
publication.SetupObjectIds = build.Collisions is { } collisions
? [.. collisions.SetupIds]
: datBundle.Setups.Keys.Order().ToArray();
publication.PreparedGeneration.SetAssetClosure(
publication.GfxObjectIds,
publication.SetupObjectIds);
return publication;
}
catch
{
_physics.CancelCollisionGeneration(collisionAdmission, prepared);
throw;
}
}
/// <summary>
@ -481,15 +498,20 @@ public sealed class LandblockPhysicsPublisher
}
else if (publication.RefloodOwnerIds is null)
{
publication.RefloodOwnerIds =
_physics.CaptureCollisionDynamicOwners(
RuntimeCollisionOwnerCaptureStep capture =
_physics.AdvanceCollisionRetainedOwnerCapture(
publication.CollisionAdmission,
publication.PreparedGeneration);
if (capture.Completed)
{
publication.RefloodOwnerIds =
publication.PreparedGeneration.RetainedOwnerIds;
}
}
else if (publication.RefloodCursor
< publication.RefloodOwnerIds.Length)
< publication.RefloodOwnerIds.Count)
{
_physics.RefreshCollisionDynamicOwner(
_physics.RefreshCollisionRetainedOwner(
publication.CollisionAdmission,
publication.PreparedGeneration,
publication.RefloodOwnerIds[publication.RefloodCursor]);
@ -507,6 +529,25 @@ public sealed class LandblockPhysicsPublisher
LogMissingSceneryBounds(landblock, publication.StagingCache);
publication.RefloodCommitted = true;
}
else if (!publication.SealCommitted)
{
RuntimeCollisionSealStep seal =
_physics.AdvanceCollisionGenerationSeal(
publication.CollisionAdmission,
publication.PreparedGeneration);
if (seal.WorkUnits > 1)
{
throw new InvalidOperationException(
"Collision seal exceeded its one-unit publication budget.");
}
publication.SealCommitted = seal.Completed;
if (seal.Restarted)
{
publication.RefloodOwnerIds = null;
publication.RefloodCursor = 0;
publication.RefloodCommitted = false;
}
}
else
{
RuntimeCollisionGenerationCommit commit =
@ -515,9 +556,13 @@ public sealed class LandblockPhysicsPublisher
publication.PreparedGeneration);
if (!commit.Committed)
{
publication.RefloodOwnerIds = commit.DirtyDynamicOwnerIds;
_physics.RestartCollisionRetainedOwnerCapture(
publication.CollisionAdmission,
publication.PreparedGeneration);
publication.RefloodOwnerIds = null;
publication.RefloodCursor = 0;
publication.RefloodCommitted = false;
publication.SealCommitted = false;
_completePublishTicks += Stopwatch.GetTimestamp() - started;
return false;
}
@ -1011,5 +1056,11 @@ public sealed class LandblockPhysicsPublisher
"The physics publication receipt belongs to another publisher.",
nameof(publication));
}
if (!publication.CompletionCommitted)
{
ObjectDisposedException.ThrowIf(
publication.PreparedGeneration.IsDisposed,
publication);
}
}
}

View file

@ -731,13 +731,15 @@ public sealed class LandblockPresentationPipeline
< transaction.Build.Landblock.Entities.Count
? 1
: transaction.PhysicsPublication
.RefloodOwnerIds is not null
&& transaction.PhysicsPublication
.RefloodOwnerIds is null
|| transaction.PhysicsPublication
.RefloodCursor
< transaction.PhysicsPublication
.RefloodOwnerIds.Length
? 1
: 0;
.RefloodOwnerIds.Count
|| !transaction.PhysicsPublication
.SealCommitted
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),

View file

@ -121,31 +121,24 @@ public sealed class PhysicsDataCache
return copy;
}
internal PreparedPhysicsDataCacheLandblock PrepareLandblockReplacement(
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
PhysicsDataCache staging,
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));
}
uint[] gfxObjectIds,
uint[] setupIds) => new(
this,
staging,
landblockId,
gfxObjectIds,
setupIds);
internal void CommitLandblockReplacement(
PreparedPhysicsDataCacheLandblock replacement)
{
RemoveCellsForLandblock(replacement.LandblockPrefix);
RemoveBuildingsForLandblock(replacement.LandblockPrefix);
RemoveEntries(_cellStruct, replacement.CellIdsToRemove);
RemoveEntries(_flatCellStruct, replacement.FlatCellIdsToRemove);
RemoveEntries(_flatEnvCell, replacement.FlatEnvCellIdsToRemove);
RemoveEntries(_buildings, replacement.BuildingIdsToRemove);
CommitEntries(_gfxObj, replacement.GfxObjects, replace: false);
CommitEntries(_visualBounds, replacement.VisualBounds, replace: false);
CommitEntries(_flatGfxObj, replacement.FlatGfxObjects, replace: false);
@ -166,34 +159,14 @@ public sealed class PhysicsDataCache
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,
IReadOnlyList<KeyValuePair<uint, T>> entries,
bool replace)
{
foreach ((uint id, T value) in entries)
for (int index = 0; index < entries.Count; index++)
{
(uint id, T value) = entries[index];
if (replace)
destination[id] = value;
else
@ -201,6 +174,14 @@ public sealed class PhysicsDataCache
}
}
private static void RemoveEntries<T>(
ConcurrentDictionary<uint, T> destination,
IReadOnlyList<uint> ids)
{
for (int index = 0; index < ids.Count; index++)
destination.TryRemove(ids[index], out _);
}
/// <summary>
/// Extract and cache the physics BSP + polygon data from a GfxObj,
/// PLUS always cache a visual AABB from the vertex data regardless of
@ -1043,19 +1024,268 @@ public sealed class PhysicsDataCache
/// <summary>Test helper, mirrors <see cref="RegisterCellStructForTest"/>.</summary>
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly PhysicsDataCache _active;
private readonly PhysicsDataCache _staging;
private readonly uint _prefix;
private readonly uint[] _gfxIds;
private readonly uint[] _setupIds;
private readonly List<KeyValuePair<uint, GfxObjPhysics>> _gfx = new();
private readonly List<KeyValuePair<uint, GfxObjVisualBounds>> _bounds = new();
private readonly List<KeyValuePair<uint, FlatGfxObjCollisionAsset>> _flatGfx = new();
private readonly List<KeyValuePair<uint, SetupPhysics>> _setups = new();
private readonly List<KeyValuePair<uint, FlatSetupCollision>> _flatSetups = new();
private readonly List<KeyValuePair<uint, CellPhysics>> _cells = new();
private readonly List<KeyValuePair<uint, FlatCellStructureCollisionAsset>> _flatCells = new();
private readonly List<KeyValuePair<uint, FlatEnvCellTopology>> _flatEnvCells = new();
private readonly List<KeyValuePair<uint, BuildingPhysics>> _buildings = new();
private readonly HashSet<uint> _cellIds = new();
private readonly HashSet<uint> _flatCellIds = new();
private readonly HashSet<uint> _flatEnvCellIds = new();
private readonly HashSet<uint> _buildingIds = new();
private readonly List<uint> _removeCells = new();
private readonly List<uint> _removeFlatCells = new();
private readonly List<uint> _removeFlatEnvCells = new();
private readonly List<uint> _removeBuildings = new();
private readonly UcgCellGraph.LandblockReplacementBuilder _cellGraph;
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvEnumerator;
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildingEnumerator;
private int _phase;
private int _cursor;
internal LandblockReplacementBuilder(
PhysicsDataCache active,
PhysicsDataCache staging,
uint landblockId,
uint[] gfxIds,
uint[] setupIds)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_gfxIds = gfxIds;
_setupIds = setupIds;
_cellGraph = active.CellGraph.CreateLandblockReplacementBuilder(
staging.CellGraph,
_prefix);
}
internal int WorkUnits { get; private set; }
internal PreparedPhysicsDataCacheLandblock? Prepared { get; private set; }
internal bool Advance()
{
switch (_phase)
{
case 0:
if (_cursor < _gfxIds.Length)
{
uint id = _gfxIds[_cursor++];
Capture(_staging._gfxObj, id, _gfx);
Capture(_staging._visualBounds, id, _bounds);
Capture(_staging._flatGfxObj, id, _flatGfx);
WorkUnits++;
return false;
}
_cursor = 0;
_phase++;
return false;
case 1:
if (_cursor < _setupIds.Length)
{
uint id = _setupIds[_cursor++];
Capture(_staging._setup, id, _setups);
Capture(_staging._flatSetup, id, _flatSetups);
WorkUnits++;
return false;
}
_phase++;
return false;
case 2:
_cellEnumerator ??= _staging._cellStruct.GetEnumerator();
if (CapturePrefixOne(_cellEnumerator, _prefix, _cells, _cellIds))
{
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
case 3:
_cellEnumerator ??= _active._cellStruct.GetEnumerator();
if (CaptureRemovalOne(_cellEnumerator, _prefix, _cellIds, _removeCells))
{
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
case 4:
_flatCellEnumerator ??= _staging._flatCellStruct.GetEnumerator();
if (CapturePrefixOne(_flatCellEnumerator, _prefix, _flatCells, _flatCellIds))
{
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
case 5:
_flatCellEnumerator ??= _active._flatCellStruct.GetEnumerator();
if (CaptureRemovalOne(_flatCellEnumerator, _prefix, _flatCellIds, _removeFlatCells))
{
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
case 6:
_flatEnvEnumerator ??= _staging._flatEnvCell.GetEnumerator();
if (CapturePrefixOne(_flatEnvEnumerator, _prefix, _flatEnvCells, _flatEnvCellIds))
{
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
case 7:
_flatEnvEnumerator ??= _active._flatEnvCell.GetEnumerator();
if (CaptureRemovalOne(_flatEnvEnumerator, _prefix, _flatEnvCellIds, _removeFlatEnvCells))
{
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
case 8:
_buildingEnumerator ??= _staging._buildings.GetEnumerator();
if (CapturePrefixOne(_buildingEnumerator, _prefix, _buildings, _buildingIds))
{
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
case 9:
_buildingEnumerator ??= _active._buildings.GetEnumerator();
if (CaptureRemovalOne(_buildingEnumerator, _prefix, _buildingIds, _removeBuildings))
{
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
case 10:
WorkUnits++;
if (!_cellGraph.Advance())
return false;
Prepared = new PreparedPhysicsDataCacheLandblock(
_prefix,
_gfx,
_bounds,
_flatGfx,
_setups,
_flatSetups,
_removeCells,
_cells,
_removeFlatCells,
_flatCells,
_removeFlatEnvCells,
_flatEnvCells,
_removeBuildings,
_buildings,
_cellGraph.Prepared!);
_phase++;
return true;
default:
return true;
}
}
private static void Capture<T>(
ConcurrentDictionary<uint, T> source,
uint id,
List<KeyValuePair<uint, T>> destination)
{
if (source.TryGetValue(id, out T? value))
destination.Add(new KeyValuePair<uint, T>(id, value));
}
private static bool CapturePrefixOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
List<KeyValuePair<uint, T>> destination,
HashSet<uint> ids)
{
if (!enumerator.MoveNext())
return false;
KeyValuePair<uint, T> pair = enumerator.Current;
if ((pair.Key & 0xFFFF0000u) == prefix)
{
destination.Add(pair);
ids.Add(pair.Key);
}
return true;
}
private static bool CaptureRemovalOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
HashSet<uint> retained,
List<uint> destination)
{
if (!enumerator.MoveNext())
return false;
uint id = enumerator.Current.Key;
if ((id & 0xFFFF0000u) == prefix && !retained.Contains(id))
destination.Add(id);
return true;
}
public void Dispose()
{
_cellEnumerator?.Dispose();
_flatCellEnumerator?.Dispose();
_flatEnvEnumerator?.Dispose();
_buildingEnumerator?.Dispose();
_cellGraph.Dispose();
}
}
}
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,
IReadOnlyList<KeyValuePair<uint, GfxObjPhysics>> GfxObjects,
IReadOnlyList<KeyValuePair<uint, GfxObjVisualBounds>> VisualBounds,
IReadOnlyList<KeyValuePair<uint, FlatGfxObjCollisionAsset>> FlatGfxObjects,
IReadOnlyList<KeyValuePair<uint, SetupPhysics>> Setups,
IReadOnlyList<KeyValuePair<uint, FlatSetupCollision>> FlatSetups,
IReadOnlyList<uint> CellIdsToRemove,
IReadOnlyList<KeyValuePair<uint, CellPhysics>> Cells,
IReadOnlyList<uint> FlatCellIdsToRemove,
IReadOnlyList<KeyValuePair<uint, FlatCellStructureCollisionAsset>> FlatCells,
IReadOnlyList<uint> FlatEnvCellIdsToRemove,
IReadOnlyList<KeyValuePair<uint, FlatEnvCellTopology>> FlatEnvCells,
IReadOnlyList<uint> BuildingIdsToRemove,
IReadOnlyList<KeyValuePair<uint, BuildingPhysics>> Buildings,
PreparedCellGraphLandblock CellGraph);
/// <summary>

View file

@ -182,12 +182,12 @@ public sealed class PhysicsEngine
return staging;
}
internal PreparedPhysicsEngineLandblock PrepareLandblockReplacement(
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
PhysicsEngine staging,
uint landblockId,
ReadOnlySpan<uint> gfxObjectIds,
ReadOnlySpan<uint> setupIds,
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
uint[] gfxObjectIds,
uint[] setupIds,
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions)
{
ArgumentNullException.ThrowIfNull(staging);
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
@ -201,31 +201,25 @@ public sealed class PhysicsEngine
PhysicsDataCache stagingCache = staging.DataCache
?? throw new InvalidOperationException(
"Staging collision engine has no data cache.");
return new PreparedPhysicsEngineLandblock(
return new LandblockReplacementBuilder(
canonical,
landblock,
stagingCache.PrepareLandblockReplacement(
(DataCache ?? throw new InvalidOperationException(
"Active collision engine has no data cache."))
.CreateLandblockReplacementBuilder(
stagingCache,
canonical,
gfxObjectIds,
setupIds),
ShadowObjects.PrepareLandblockReplacement(
ShadowObjects.CreateLandblockReplacementBuilder(
staging.ShadowObjects,
canonical,
expectedDynamicVersions));
expectedRetainedVersions));
}
internal bool ValidateLandblockReplacement(
PreparedPhysicsEngineLandblock replacement) =>
ShadowObjects.ValidateLandblockReplacement(replacement.Shadows);
internal void CommitLandblockReplacement(
PreparedPhysicsEngineLandblock replacement)
{
if (!ValidateLandblockReplacement(replacement))
{
throw new InvalidOperationException(
"Collision generation changed after it was sealed.");
}
(DataCache ?? throw new InvalidOperationException(
"Active collision engine has no data cache."))
.CommitLandblockReplacement(replacement.DataCache);
@ -253,6 +247,64 @@ public sealed class PhysicsEngine
internal ShadowObjectRegistry.PreparedLandblockShadowReplacement Shadows { get; }
}
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly uint _landblockId;
private readonly LandblockPhysics _landblock;
private readonly PhysicsDataCache.LandblockReplacementBuilder _data;
private readonly ShadowObjectRegistry.LandblockReplacementBuilder _shadows;
private int _phase;
internal LandblockReplacementBuilder(
uint landblockId,
LandblockPhysics landblock,
PhysicsDataCache.LandblockReplacementBuilder data,
ShadowObjectRegistry.LandblockReplacementBuilder shadows)
{
_landblockId = landblockId;
_landblock = landblock;
_data = data;
_shadows = shadows;
}
internal int WorkUnits => _data.WorkUnits + _shadows.WorkUnits;
internal bool IsStable => _shadows.IsStable;
internal PreparedPhysicsEngineLandblock? Prepared { get; private set; }
internal bool Advance()
{
if (_phase == 0)
{
if (!_data.Advance())
return false;
_phase++;
return false;
}
if (_phase == 1)
{
if (!_shadows.Advance())
return false;
if (IsStable && _data.Prepared is not null
&& _shadows.Prepared is not null)
{
Prepared = new PreparedPhysicsEngineLandblock(
_landblockId,
_landblock,
_data.Prepared,
_shadows.Prepared);
}
_phase++;
}
return true;
}
public void Dispose()
{
_data.Dispose();
_shadows.Dispose();
}
}
/// <summary>
/// Register a landblock with its terrain surface, indoor cells, portal
/// planes, and world-space origin offset.

View file

@ -52,6 +52,7 @@ public sealed class ShadowObjectRegistry
/// </summary>
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
private readonly Dictionary<uint, ulong> _ownerVersions = new();
private ulong _mutationVersion;
internal sealed record RegistrationRecord(
uint SeedCellId,
@ -76,8 +77,88 @@ public sealed class ShadowObjectRegistry
private void BumpOwnerVersion(uint entityId)
{
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
_mutationVersion = checked(_mutationVersion + 1UL);
}
internal ulong MutationVersion => _mutationVersion;
internal RetainedRefloodOwnerScan CreateRetainedRefloodOwnerScan(
uint landblockId) => new(this, landblockId & 0xFFFF0000u);
internal sealed class RetainedRefloodOwnerScan : IDisposable
{
private readonly ShadowObjectRegistry _owner;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
private Dictionary<uint, RegistrationRecord>.Enumerator _enumerator;
private bool _completed;
internal RetainedRefloodOwnerScan(
ShadowObjectRegistry owner,
uint prefix)
{
_owner = owner;
_prefix = prefix;
_sourceMutationVersion = owner.MutationVersion;
_enumerator = owner._entityReg.GetEnumerator();
}
internal RetainedRefloodOwnerScanStep Advance()
{
if (_completed)
{
return new RetainedRefloodOwnerScanStep(
Completed: true,
Stable: _owner.MutationVersion == _sourceMutationVersion,
HasOwner: false,
OwnerId: 0u,
SourceMutationVersion: _sourceMutationVersion);
}
if (_owner.MutationVersion != _sourceMutationVersion)
{
_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);
}
(uint ownerId, RegistrationRecord registration) =
_enumerator.Current;
bool retained = !_owner._suspendedEntities.Contains(ownerId)
&& (!registration.IsStatic
|| (registration.SeedCellId & 0xFFFF0000u) != _prefix)
&& _owner.OwnerTouchesLandblock(ownerId, _prefix);
return new RetainedRefloodOwnerScanStep(
Completed: false,
Stable: true,
HasOwner: retained,
OwnerId: retained ? ownerId : 0u,
SourceMutationVersion: _sourceMutationVersion);
}
public void Dispose() => _enumerator.Dispose();
}
internal readonly record struct RetainedRefloodOwnerScanStep(
bool Completed,
bool Stable,
bool HasOwner,
uint OwnerId,
ulong SourceMutationVersion);
/// <summary>
/// The flood's data source (cells, buildings, terrain origins). Wired by
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
@ -624,8 +705,14 @@ public sealed class ShadowObjectRegistry
{
// Suspended dynamic objects have no cell rows, but their retained
// registration must still receive authoritative state changes.
if (_entityReg.TryGetValue(entityId, out var retainedRegistration))
_entityReg[entityId] = retainedRegistration with { State = newState };
bool retained = _entityReg.TryGetValue(
entityId,
out RegistrationRecord? retainedRegistration);
if (retained)
{
_entityReg[entityId] = retainedRegistration! with { State = newState };
BumpOwnerVersion(entityId);
}
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
return; // not registered — no-op
@ -640,9 +727,6 @@ 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>
@ -657,7 +741,7 @@ public sealed class ShadowObjectRegistry
foreach (var cellId in cellIds)
{
if (_cells.TryGetValue(cellId, out var list))
list.RemoveAll(e => e.EntityId == entityId);
RemoveOwnerRows(list, entityId);
}
_entityToCells.Remove(entityId);
}
@ -669,6 +753,17 @@ public sealed class ShadowObjectRegistry
BumpOwnerVersion(entityId);
}
private static void RemoveOwnerRows(
List<ShadowEntry> entries,
uint entityId)
{
for (int index = entries.Count - 1; index >= 0; index--)
{
if (entries[index].EntityId == entityId)
entries.RemoveAt(index);
}
}
/// <summary>
/// Logically tear down every static object owned by a landblock, including
/// shadow rows flooded into adjacent landblocks. Dynamic/server-live owners
@ -824,6 +919,11 @@ public sealed class ShadowObjectRegistry
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
public int SuspendedRegistrationCount => _suspendedEntities.Count;
public bool HasOwnerRowsInLandblock(uint ownerId, uint landblockId) =>
_entityToCells.TryGetValue(ownerId, out List<uint>? cells)
&& cells.Exists(cell =>
(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
@ -860,16 +960,7 @@ public sealed class ShadowObjectRegistry
}
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();
_mutationVersion = source._mutationVersion;
}
/// <summary>
@ -877,7 +968,7 @@ public sealed class ShadowObjectRegistry
/// it against the staging generation's complete cell graph. The returned
/// source version is the commit-time freshness token.
/// </summary>
internal bool RefreshDynamicOwnerFrom(
internal bool RefreshRetainedOwnerFrom(
ShadowObjectRegistry source,
uint entityId,
uint landblockId,
@ -889,8 +980,10 @@ public sealed class ShadowObjectRegistry
if (!source._entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
|| registration.IsStatic
|| source._suspendedEntities.Contains(entityId)
|| (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u))
|| !source.OwnerTouchesLandblock(entityId, landblockId))
{
return false;
@ -912,7 +1005,7 @@ public sealed class ShadowObjectRegistry
0f,
landblockId,
registration.SeedCellId,
isStatic: false);
isStatic: registration.IsStatic);
}
else
{
@ -931,92 +1024,42 @@ public sealed class ShadowObjectRegistry
registration.State,
registration.Flags,
registration.SeedCellId,
isStatic: false);
isStatic: registration.IsStatic);
}
if (source._withdrawnPrefixesByOwner.TryGetValue(
entityId,
out HashSet<uint>? sourceWithdrawn))
{
var retainedWithdrawn = new HashSet<uint>(sourceWithdrawn);
uint prefix = landblockId & 0xFFFF0000u;
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells)
&& cells.Exists(cell => (cell & 0xFFFF0000u) == prefix))
{
retainedWithdrawn.Remove(prefix);
}
if (retainedWithdrawn.Count != 0)
_withdrawnPrefixesByOwner[entityId] = retainedWithdrawn;
}
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(
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
{
ArgumentNullException.ThrowIfNull(staging);
uint[] dirty = FindDirtyDynamicOwners(
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions) => new(
this,
staging,
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());
}
expectedRetainedVersions);
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);
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)
@ -1071,10 +1114,10 @@ public sealed class ShadowObjectRegistry
entityId,
registration,
shapes,
cells?.ToArray() ?? Array.Empty<uint>(),
rows.ToArray(),
cells is null ? null : new List<uint>(cells),
rows,
_suspendedEntities.Contains(entityId),
withdrawn?.ToArray() ?? Array.Empty<uint>());
withdrawn is null ? null : new HashSet<uint>(withdrawn));
return true;
}
@ -1085,49 +1128,194 @@ public sealed class ShadowObjectRegistry
_entityShapes[state.EntityId] = state.Shapes;
if (state.Suspended)
_suspendedEntities.Add(state.EntityId);
if (state.WithdrawnPrefixes.Length != 0)
if (state.WithdrawnPrefixes is not null)
{
_withdrawnPrefixesByOwner[state.EntityId] =
new HashSet<uint>(state.WithdrawnPrefixes);
_withdrawnPrefixesByOwner[state.EntityId] = state.WithdrawnPrefixes;
}
if (state.CellIds.Length != 0)
_entityToCells[state.EntityId] = new List<uint>(state.CellIds);
foreach (PreparedShadowCellRows row in state.Rows)
if (state.CellIds is not null)
_entityToCells[state.EntityId] = state.CellIds;
for (int rowIndex = 0; rowIndex < state.Rows.Count; rowIndex++)
{
foreach (ShadowEntry entry in row.Entries)
AddEntryToCell(entry, row.CellId);
PreparedShadowCellRows row = state.Rows[rowIndex];
for (int entryIndex = 0; entryIndex < row.Entries.Length; entryIndex++)
AddEntryToCell(row.Entries[entryIndex], row.CellId);
}
BumpOwnerVersion(state.EntityId);
}
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly ShadowObjectRegistry _active;
private readonly ShadowObjectRegistry _staging;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
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 int _phase;
internal LandblockReplacementBuilder(
ShadowObjectRegistry active,
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expected)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_sourceMutationVersion = active.MutationVersion;
_expectedEnumerator = expected.GetEnumerator();
}
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())
{
(uint ownerId, ulong version) = _expectedEnumerator.Current;
if (_active.GetOwnerVersion(ownerId) != version
|| !_active.IsRetainedRefloodOwner(ownerId, _prefix))
{
return true;
}
AddOwner(ownerId);
WorkUnits++;
return false;
}
_expectedEnumerator.Dispose();
_expectedEnumerator = null;
_registrationEnumerator = _active._entityReg.GetEnumerator();
_phase++;
return false;
case 1:
if (_registrationEnumerator.MoveNext())
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
}
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_registrationEnumerator = _staging._entityReg.GetEnumerator();
_phase++;
return false;
case 2:
if (_registrationEnumerator.MoveNext())
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
}
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_ownerEnumerator = _owners.GetEnumerator();
_phase++;
return false;
case 3:
if (_ownerEnumerator.MoveNext())
{
uint ownerId = _ownerEnumerator.Current;
if (_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state)
&& state is not null)
{
_states.Add(state);
}
WorkUnits++;
return false;
}
_ownerEnumerator.Dispose();
if (IsStable)
{
Prepared = new PreparedLandblockShadowReplacement(
_prefix,
_ownerIds,
_states);
}
_phase++;
return true;
default:
return true;
}
}
private void AddOwner(uint ownerId)
{
if (_owners.Add(ownerId))
_ownerIds.Add(ownerId);
}
public void Dispose()
{
_expectedEnumerator?.Dispose();
if (_phase is 1 or 2)
_registrationEnumerator.Dispose();
if (_phase == 3)
_ownerEnumerator.Dispose();
}
}
private bool IsRetainedRefloodOwner(uint ownerId, uint landblockId)
{
if (!_entityReg.TryGetValue(ownerId, out RegistrationRecord? registration)
|| _suspendedEntities.Contains(ownerId)
|| (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u)))
{
return false;
}
return OwnerTouchesLandblock(ownerId, landblockId);
}
internal sealed class PreparedLandblockShadowReplacement
{
internal PreparedLandblockShadowReplacement(
uint landblockPrefix,
uint[] ownerIds,
PreparedShadowOwnerState[] ownerStates,
Dictionary<uint, ulong> dynamicVersions)
IReadOnlyList<uint> ownerIds,
IReadOnlyList<PreparedShadowOwnerState> ownerStates)
{
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 IReadOnlyList<uint> OwnerIds { get; }
internal IReadOnlyList<PreparedShadowOwnerState> OwnerStates { get; }
}
internal sealed record PreparedShadowOwnerState(
uint EntityId,
RegistrationRecord Registration,
IReadOnlyList<ShadowShape>? Shapes,
uint[] CellIds,
PreparedShadowCellRows[] Rows,
List<uint>? CellIds,
IReadOnlyList<PreparedShadowCellRows> Rows,
bool Suspended,
uint[] WithdrawnPrefixes);
HashSet<uint>? WithdrawnPrefixes);
internal sealed record PreparedShadowCellRows(
uint CellId,
@ -1146,6 +1334,7 @@ public sealed class ShadowObjectRegistry
_entityShapes.Clear();
_entityReg.Clear();
_ownerVersions.Clear();
_mutationVersion = 0UL;
_fallback = null;
}

View file

@ -19,7 +19,7 @@ namespace AcDream.Core.World.Cells;
public sealed class CellGraph
{
private readonly ConcurrentDictionary<uint, EnvCell> _envCells = new();
private readonly ConcurrentDictionary<uint, (TerrainSurface Terrain, Vector3 Origin)> _terrain = new();
private readonly ConcurrentDictionary<uint, CellGraphTerrain> _terrain = new();
/// <summary>The player's current cell — the render/lighting root. Written ONLY at the
/// player chokepoint <see cref="AcDream.Core.Physics.PhysicsEngine.UpdatePlayerCurrCell"/>
@ -34,7 +34,8 @@ 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] = (terrain, worldOrigin);
=> _terrain[landblockPrefix & 0xFFFF0000u] =
new CellGraphTerrain(terrain, worldOrigin);
/// <summary>
/// World origin (SW corner) of the landblock containing <paramref name="id"/>,
@ -137,46 +138,36 @@ public sealed class CellGraph
var copy = new CellGraph { CurrCell = CurrCell };
foreach ((uint id, EnvCell cell) in _envCells)
copy._envCells.TryAdd(id, cell);
foreach ((uint id, (TerrainSurface Terrain, Vector3 Origin) terrain) in
_terrain)
foreach ((uint id, CellGraphTerrain terrain) in _terrain)
{
copy._terrain.TryAdd(id, terrain);
}
return copy;
}
internal PreparedCellGraphLandblock PrepareLandblockReplacement(
uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
KeyValuePair<uint, EnvCell>[] envCells = _envCells
.Where(static pair => (pair.Key & 0xFFFFu) >= 0x0100u)
.Where(pair => (pair.Key & 0xFFFF0000u) == prefix)
.OrderBy(static pair => pair.Key)
.ToArray();
bool hasTerrain = _terrain.TryGetValue(prefix, out var terrain);
return new PreparedCellGraphLandblock(
prefix,
envCells,
hasTerrain,
terrain.Terrain,
terrain.Origin,
CurrCell?.Id ?? 0u);
}
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
CellGraph staging,
uint landblockId) => new(this, staging, landblockId);
internal void CommitLandblockReplacement(
PreparedCellGraphLandblock replacement)
{
uint currentCellId = CurrCell?.Id ?? 0u;
RemoveLandblock(replacement.LandblockPrefix);
for (int index = 0; index < replacement.EnvCellIdsToRemove.Count; index++)
_envCells.TryRemove(replacement.EnvCellIdsToRemove[index], out _);
if (replacement.HasTerrain)
{
_terrain[replacement.LandblockPrefix] = (
replacement.Terrain!,
replacement.Origin);
_terrain[replacement.LandblockPrefix] = replacement.Terrain!;
}
foreach ((uint id, EnvCell cell) in replacement.EnvCells)
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
@ -188,13 +179,101 @@ public sealed class CellGraph
: 0u;
if (desiredCurrentCellId != 0u)
CurrCell = GetVisible(desiredCurrentCellId);
else if ((currentCellId & 0xFFFF0000u)
== replacement.LandblockPrefix)
CurrCell = null;
}
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly CellGraph _active;
private readonly CellGraph _staging;
private readonly uint _prefix;
private readonly List<KeyValuePair<uint, EnvCell>> _envCells = new();
private readonly HashSet<uint> _stagingIds = new();
private readonly List<uint> _removeIds = new();
private IEnumerator<KeyValuePair<uint, EnvCell>>? _enumerator;
private int _phase;
internal LandblockReplacementBuilder(
CellGraph active,
CellGraph staging,
uint landblockId)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_enumerator = staging._envCells.GetEnumerator();
}
internal bool Advance()
{
if (_phase == 0)
{
if (_enumerator!.MoveNext())
{
KeyValuePair<uint, EnvCell> pair = _enumerator.Current;
if ((pair.Key & 0xFFFF0000u) == _prefix
&& (pair.Key & 0xFFFFu) >= 0x0100u)
{
_envCells.Add(pair);
_stagingIds.Add(pair.Key);
}
return false;
}
_enumerator.Dispose();
_enumerator = _active._envCells.GetEnumerator();
_phase = 1;
return false;
}
if (_phase == 1)
{
if (_enumerator!.MoveNext())
{
uint id = _enumerator.Current.Key;
if ((id & 0xFFFF0000u) == _prefix
&& (id & 0xFFFFu) >= 0x0100u
&& !_stagingIds.Contains(id))
{
_removeIds.Add(id);
}
return false;
}
_enumerator.Dispose();
_enumerator = null;
bool hasTerrain = _staging._terrain.TryGetValue(
_prefix,
out var terrain);
Prepared = new PreparedCellGraphLandblock(
_prefix,
_removeIds,
_envCells,
hasTerrain,
terrain,
_staging.CurrCell?.Id ?? 0u);
_phase = 2;
}
return true;
}
internal PreparedCellGraphLandblock? Prepared { get; private set; }
public void Dispose()
{
_enumerator?.Dispose();
_enumerator = null;
}
}
}
internal sealed record PreparedCellGraphLandblock(
uint LandblockPrefix,
KeyValuePair<uint, EnvCell>[] EnvCells,
IReadOnlyList<uint> EnvCellIdsToRemove,
IReadOnlyList<KeyValuePair<uint, EnvCell>> EnvCells,
bool HasTerrain,
TerrainSurface? Terrain,
Vector3 Origin,
CellGraphTerrain? Terrain,
uint CurrentCellId);
internal sealed record CellGraphTerrain(
TerrainSurface Terrain,
Vector3 Origin);

View file

@ -18,6 +18,77 @@ internal interface IHeadlessCollisionNeighborhood
bool IsReady(uint fullCellId);
}
internal static class HeadlessCollisionGenerationTransaction
{
internal static RuntimeCollisionGenerationCommit Execute(
RuntimePhysicsState physics,
uint landblockId,
Action<RuntimeCollisionAdmission>? afterAdmission,
Action<RuntimeCollisionAdmission,
PreparedLandblockCollisionGeneration> stage)
{
ArgumentNullException.ThrowIfNull(physics);
ArgumentNullException.ThrowIfNull(stage);
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId);
PreparedLandblockCollisionGeneration? prepared = null;
bool committed = false;
try
{
// This hook exists so the exact post-admission/pre-prepare failure
// boundary remains covered. Production does not install one.
afterAdmission?.Invoke(admission);
prepared = physics.PrepareCollisionGeneration(admission);
stage(admission, prepared);
RuntimeCollisionOwnerCaptureStep ownerCapture;
do
{
ownerCapture = physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared);
}
while (!ownerCapture.Completed);
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
{
throw new InvalidOperationException(
"Headless collision owner set changed during synchronous sealing.");
}
RuntimeCollisionGenerationCommit result =
physics.CommitCollisionGeneration(admission, prepared);
if (!result.Committed)
{
throw new InvalidOperationException(
"Headless collision generation changed during synchronous publication.");
}
committed = true;
return result;
}
finally
{
if (!committed)
physics.CancelCollisionGeneration(admission, prepared);
}
}
}
/// <summary>
/// Per-session mutable collision publication over process-shared immutable
/// DAT and pak inputs. Every session retains its own engine, data cache,
@ -172,81 +243,58 @@ internal sealed class HeadlessCollisionNeighborhood
_content.PreparedCollision,
landblock);
RuntimePhysicsState physics =
_runtime.EntityObjects.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId);
using PreparedLandblockCollisionGeneration prepared =
physics.PrepareCollisionGeneration(admission);
prepared.SetAssetClosure(
[.. collisions.GfxObjIds],
[.. collisions.SetupIds]);
PhysicsDataCache cache = prepared.DataCache;
TerrainSurface terrain =
LandblockPhysicsContentBuilder.BuildTerrainSurface(
landblock,
_content.HeightTable.AsSpan());
var cellSurfaces = new List<CellSurface>();
var portalPlanes = new List<PortalPlane>();
LandblockPhysicsContentBuilder.PublishPreparedCells(
cache,
landblock,
collisions,
origin,
cellSurfaces,
portalPlanes);
LandblockPhysicsContentBuilder.CacheBuildings(
cache,
landblock,
terrain,
origin);
LandblockPhysicsContentBuilder.CachePreparedObjects(
cache,
collisions);
try
{
physics.StageCollisionAssets(
admission,
prepared,
new RuntimeLandblockCollisionAssets(
landblockId,
terrain,
cellSurfaces,
portalPlanes,
origin.X,
origin.Y,
currentCellId));
_ = LandblockPhysicsContentBuilder
.PublishStaticCollision(
prepared.Engine,
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
_ = HeadlessCollisionGenerationTransaction.Execute(
physics,
landblockId,
afterAdmission: null,
(admission, prepared) =>
{
prepared.SetAssetClosure(
[.. collisions.GfxObjIds],
[.. collisions.SetupIds]);
PhysicsDataCache cache = prepared.DataCache;
TerrainSurface terrain =
LandblockPhysicsContentBuilder.BuildTerrainSurface(
landblock,
_content.HeightTable.AsSpan());
var cellSurfaces = new List<CellSurface>();
var portalPlanes = new List<PortalPlane>();
LandblockPhysicsContentBuilder.PublishPreparedCells(
cache,
landblock,
collisions,
origin,
cellSurfaces,
portalPlanes);
LandblockPhysicsContentBuilder.CacheBuildings(
cache,
landblock,
terrain,
origin);
foreach (uint ownerId in physics.CaptureCollisionDynamicOwners(
admission,
prepared))
{
physics.RefreshCollisionDynamicOwner(
LandblockPhysicsContentBuilder.CachePreparedObjects(
cache,
collisions);
physics.StageCollisionAssets(
admission,
prepared,
ownerId);
}
RuntimeCollisionGenerationCommit commit =
physics.CommitCollisionGeneration(admission, prepared);
if (!commit.Committed)
{
throw new InvalidOperationException(
"Headless collision generation changed during synchronous publication.");
}
_resident.Add(CanonicalLandblock(landblockId));
}
catch
{
_ = physics.WithdrawCollision(landblockId);
throw;
}
new RuntimeLandblockCollisionAssets(
landblockId,
terrain,
cellSurfaces,
portalPlanes,
origin.X,
origin.Y,
currentCellId));
_ = LandblockPhysicsContentBuilder
.PublishStaticCollision(
prepared.Engine,
cache,
landblock,
collisions,
origin);
});
_resident.Add(CanonicalLandblock(landblockId));
}
private void RetireAll()

View file

@ -68,7 +68,7 @@ public readonly record struct RuntimeCollisionAcknowledgement(
public readonly record struct RuntimeCollisionGenerationCommit(
RuntimeCollisionAcknowledgement Acknowledgement,
uint[] DirtyDynamicOwnerIds)
uint[] DirtyRetainedOwnerIds)
{
public bool Committed => Acknowledgement.Ready;
}
@ -87,7 +87,11 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
{
private readonly RuntimePhysicsState _owner;
private readonly RuntimeCollisionAdmission _admission;
private readonly Dictionary<uint, ulong> _dynamicOwnerVersions = new();
private readonly Dictionary<uint, ulong> _retainedOwnerVersions = new();
private readonly List<uint> _retainedOwnerIds = new();
private ShadowObjectRegistry.RetainedRefloodOwnerScan? _retainedOwnerScan;
private PhysicsEngine.LandblockReplacementBuilder? _sealBuilder;
private PhysicsEngine.PreparedPhysicsEngineLandblock? _sealedReplacement;
private bool _disposed;
internal PreparedLandblockCollisionGeneration(
@ -106,9 +110,13 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
internal PhysicsEngine Engine { get; }
internal uint[] GfxObjectIds { get; private set; } = Array.Empty<uint>();
internal uint[] SetupIds { get; private set; } = Array.Empty<uint>();
internal IReadOnlyDictionary<uint, ulong> DynamicOwnerVersions =>
_dynamicOwnerVersions;
internal IReadOnlyDictionary<uint, ulong> RetainedOwnerVersions =>
_retainedOwnerVersions;
internal bool IsDisposed => _disposed;
internal bool RetainedOwnerCaptureComplete { get; private set; }
internal ulong RetainedOwnerCaptureMutationVersion { get; private set; }
internal bool IsSealed => _sealedReplacement is not null;
internal ulong SealedShadowMutationVersion { get; private set; }
internal bool Matches(
RuntimePhysicsState owner,
@ -123,26 +131,155 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
SetupIds = setupIds ?? throw new ArgumentNullException(nameof(setupIds));
}
internal void RefreshDynamicOwner(uint ownerId)
internal void RefreshRetainedOwner(uint ownerId)
{
EnsureUsable();
bool retained = Engine.ShadowObjects.RefreshDynamicOwnerFrom(
bool retained = Engine.ShadowObjects.RefreshRetainedOwnerFrom(
_owner.Engine.ShadowObjects,
ownerId,
_admission.LandblockId,
out ulong version);
if (retained)
_dynamicOwnerVersions[ownerId] = version;
_retainedOwnerVersions[ownerId] = version;
else
_dynamicOwnerVersions.Remove(ownerId);
_retainedOwnerVersions.Remove(ownerId);
}
internal uint[] FindDirtyDynamicOwners()
internal RuntimeCollisionOwnerCaptureStep AdvanceRetainedOwnerCapture()
{
EnsureUsable();
return _owner.Engine.ShadowObjects.FindDirtyDynamicOwners(
if (RetainedOwnerCaptureComplete)
{
return new RuntimeCollisionOwnerCaptureStep(
Completed: true,
Restarted: false,
HasOwner: false,
OwnerId: 0u);
}
_retainedOwnerScan ??= _owner.Engine.ShadowObjects
.CreateRetainedRefloodOwnerScan(_admission.LandblockId);
ShadowObjectRegistry.RetainedRefloodOwnerScanStep step =
_retainedOwnerScan.Advance();
if (step.Completed && !step.Stable)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionOwnerCaptureStep(
Completed: false,
Restarted: true,
HasOwner: false,
OwnerId: 0u);
}
if (step.HasOwner)
_retainedOwnerIds.Add(step.OwnerId);
if (step.Completed)
{
_retainedOwnerScan.Dispose();
_retainedOwnerScan = null;
RetainedOwnerCaptureMutationVersion = step.SourceMutationVersion;
RetainedOwnerCaptureComplete = true;
}
return new RuntimeCollisionOwnerCaptureStep(
RetainedOwnerCaptureComplete,
Restarted: false,
step.HasOwner,
step.OwnerId);
}
internal IReadOnlyList<uint> RetainedOwnerIds
{
get
{
EnsureUsable();
if (!RetainedOwnerCaptureComplete)
{
throw new InvalidOperationException(
"Retained collision-owner capture is incomplete.");
}
return _retainedOwnerIds;
}
}
internal void ResetRetainedOwnerCapture()
{
EnsureUsable();
_retainedOwnerScan?.Dispose();
_retainedOwnerScan = null;
_retainedOwnerIds.Clear();
_retainedOwnerVersions.Clear();
RetainedOwnerCaptureComplete = false;
RetainedOwnerCaptureMutationVersion = 0UL;
_sealedReplacement = null;
_sealBuilder?.Dispose();
_sealBuilder = null;
SealedShadowMutationVersion = 0UL;
}
internal RuntimeCollisionSealStep AdvanceSeal()
{
EnsureUsable();
if (!RetainedOwnerCaptureComplete)
{
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
WorkUnits: 0);
}
if (_owner.Engine.ShadowObjects.MutationVersion
!= RetainedOwnerCaptureMutationVersion)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
WorkUnits: 0);
}
if (_retainedOwnerVersions.Count != _retainedOwnerIds.Count)
{
throw new InvalidOperationException(
"Every retained collision owner must refresh before sealing.");
}
_sealBuilder ??= _owner.Engine.CreateLandblockReplacementBuilder(
Engine,
_admission.LandblockId,
_dynamicOwnerVersions);
GfxObjectIds,
SetupIds,
_retainedOwnerVersions);
int before = _sealBuilder.WorkUnits;
bool completed = _sealBuilder.Advance();
int workUnits = _sealBuilder.WorkUnits - before;
if (!completed)
{
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: false,
workUnits);
}
if (!_sealBuilder.IsStable || _sealBuilder.Prepared is null)
{
ResetRetainedOwnerCapture();
return new RuntimeCollisionSealStep(
Completed: false,
Restarted: true,
workUnits);
}
_sealedReplacement = _sealBuilder.Prepared;
_sealBuilder.Dispose();
_sealBuilder = null;
SealedShadowMutationVersion =
_owner.Engine.ShadowObjects.MutationVersion;
return new RuntimeCollisionSealStep(
Completed: true,
Restarted: false,
workUnits);
}
internal PhysicsEngine.PreparedPhysicsEngineLandblock TakeSealedReplacement()
{
EnsureUsable();
return _sealedReplacement
?? throw new InvalidOperationException(
"Collision generation must be sealed before activation.");
}
internal void MarkCommitted()
@ -156,7 +293,14 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
if (_disposed)
return;
Engine.Clear();
_dynamicOwnerVersions.Clear();
_retainedOwnerScan?.Dispose();
_retainedOwnerScan = null;
_retainedOwnerIds.Clear();
_retainedOwnerVersions.Clear();
_sealBuilder?.Dispose();
_sealBuilder = null;
_sealedReplacement = null;
SealedShadowMutationVersion = 0UL;
_disposed = true;
}
@ -167,6 +311,17 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
}
}
internal readonly record struct RuntimeCollisionOwnerCaptureStep(
bool Completed,
bool Restarted,
bool HasOwner,
uint OwnerId);
internal readonly record struct RuntimeCollisionSealStep(
bool Completed,
bool Restarted,
int WorkUnits);
/// <summary>
/// Presentation-free mutable physics world for one Runtime/session owner.
/// Immutable prepared collision inputs may be supplied by a graphical or
@ -1001,6 +1156,43 @@ public sealed class RuntimePhysicsState : IDisposable
stagingEngine);
}
/// <summary>
/// Cancels only the named unpublished generation. The currently active
/// collision world is never withdrawn. A stale receipt may dispose its
/// own staging storage but cannot invalidate a newer admission.
/// </summary>
internal void CancelCollisionGeneration(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration? prepared = null)
{
EnsureNotDisposed();
EnsureCollisionMutationThread();
ArgumentNullException.ThrowIfNull(admission);
if (!ReferenceEquals(admission.Owner, this))
{
throw new ArgumentException(
"Collision admission belongs to another Runtime.",
nameof(admission));
}
if (prepared is not null && !prepared.Matches(this, admission))
{
throw new ArgumentException(
"Prepared collision generation belongs to another admission.",
nameof(prepared));
}
prepared?.Dispose();
if (_collisionAdmissions.TryGetValue(
admission.LandblockId,
out RuntimeCollisionAdmission? current)
&& ReferenceEquals(current, admission))
{
_collisionAdmissions.Remove(admission.LandblockId);
_collisionGenerations[admission.LandblockId] = checked(
admission.Generation + 1UL);
}
}
internal void StageCollisionAssets(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared,
@ -1043,18 +1235,17 @@ public sealed class RuntimePhysicsState : IDisposable
admission.AssetsPrepared = true;
}
internal uint[] CaptureCollisionDynamicOwners(
internal RuntimeCollisionOwnerCaptureStep AdvanceCollisionRetainedOwnerCapture(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
return Engine.ShadowObjects.CaptureDynamicRefloodOwnersForLandblock(
admission.LandblockId);
return prepared.AdvanceRetainedOwnerCapture();
}
internal void RefreshCollisionDynamicOwner(
internal void RefreshCollisionRetainedOwner(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared,
uint ownerId)
@ -1062,7 +1253,32 @@ public sealed class RuntimePhysicsState : IDisposable
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
prepared.RefreshDynamicOwner(ownerId);
prepared.RefreshRetainedOwner(ownerId);
}
internal void RestartCollisionRetainedOwnerCapture(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
prepared.ResetRetainedOwnerCapture();
}
internal RuntimeCollisionSealStep AdvanceCollisionGenerationSeal(
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
ValidateAdmission(admission);
EnsureCollisionMutationThread();
ValidatePreparedGeneration(admission, prepared);
if (!admission.AssetsPrepared)
{
throw new InvalidOperationException(
"Collision generation cannot seal before its assets are prepared.");
}
return prepared.AdvanceSeal();
}
internal RuntimeCollisionGenerationCommit CommitCollisionGeneration(
@ -1083,38 +1299,25 @@ public sealed class RuntimePhysicsState : IDisposable
"Collision generation has already completed.");
}
uint[] dirtyOwners = prepared.FindDirtyDynamicOwners();
if (dirtyOwners.Length != 0)
if (!prepared.IsSealed)
{
throw new InvalidOperationException(
"Collision generation cannot activate before sealing.");
}
if (Engine.ShadowObjects.MutationVersion
!= prepared.SealedShadowMutationVersion)
{
prepared.ResetRetainedOwnerCapture();
return new RuntimeCollisionGenerationCommit(
new RuntimeCollisionAcknowledgement(
admission.LandblockId,
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
dirtyOwners);
Array.Empty<uint>());
}
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
Engine.PrepareLandblockReplacement(
prepared.Engine,
admission.LandblockId,
prepared.GfxObjectIds,
prepared.SetupIds,
prepared.DynamicOwnerVersions);
if (!Engine.ValidateLandblockReplacement(replacement))
{
dirtyOwners = prepared.FindDirtyDynamicOwners();
return new RuntimeCollisionGenerationCommit(
new RuntimeCollisionAcknowledgement(
admission.LandblockId,
admission.Generation,
Engine.IsLandblockTerrainResident(admission.LandblockId),
Ready: false),
dirtyOwners);
}
Engine.CommitLandblockReplacement(replacement);
Engine.CommitLandblockReplacement(prepared.TakeSealedReplacement());
admission.Completed = true;
_collisionAdmissions.Remove(admission.LandblockId);
prepared.MarkCommitted();