fix(physics): activate collision generations atomically

This commit is contained in:
Erik 2026-07-31 15:19:25 +02:00
parent 3e0f3b6206
commit be94bc9b06
18 changed files with 1402 additions and 80 deletions

View file

@ -12,7 +12,7 @@ namespace AcDream.App.Streaming;
/// publication. The render publisher commits buildings and EnvCells between
/// these stages without recomputing the captured origin.
/// </summary>
public sealed class LandblockPhysicsPublication
public sealed class LandblockPhysicsPublication : IDisposable
{
internal LandblockPhysicsPublication(
object owner,
@ -21,7 +21,8 @@ public sealed class LandblockPhysicsPublication
uint currentCellId,
BuildingInfo[] buildings,
uint[] priorStaticOwnerIds,
RuntimeCollisionAdmission collisionAdmission)
RuntimeCollisionAdmission collisionAdmission,
PreparedLandblockCollisionGeneration preparedGeneration)
{
Owner = owner;
Build = build;
@ -30,6 +31,7 @@ public sealed class LandblockPhysicsPublication
Buildings = buildings;
PriorStaticOwnerIds = priorStaticOwnerIds;
CollisionAdmission = collisionAdmission;
PreparedGeneration = preparedGeneration;
}
internal object Owner { get; }
@ -38,6 +40,9 @@ public sealed class LandblockPhysicsPublication
internal BuildingInfo[] Buildings { get; }
internal uint[] PriorStaticOwnerIds { get; }
internal RuntimeCollisionAdmission CollisionAdmission { get; }
internal PreparedLandblockCollisionGeneration PreparedGeneration { get; }
internal PhysicsDataCache StagingCache => PreparedGeneration.DataCache;
internal PhysicsEngine StagingEngine => PreparedGeneration.Engine;
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
internal int PreparationCursor { get; set; }
@ -64,6 +69,12 @@ public sealed class LandblockPhysicsPublication
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
public void Dispose()
{
if (!CompletionCommitted)
PreparedGeneration.Dispose();
}
public uint LandblockId => Build.Landblock.LandblockId;
public Vector3 Origin { get; }
}
@ -204,6 +215,8 @@ public sealed class LandblockPhysicsPublisher
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
?? Array.Empty<BuildingInfo>();
RuntimeCollisionAdmission collisionAdmission =
_physics.BeginCollisionAdmission(build.Landblock.LandblockId);
var publication = new LandblockPhysicsPublication(
_receiptOwner,
build,
@ -212,11 +225,14 @@ public sealed class LandblockPhysicsPublisher
buildings,
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
build.Landblock.LandblockId),
_physics.BeginCollisionAdmission(
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;
}
@ -237,6 +253,9 @@ public sealed class LandblockPhysicsPublisher
if (publication.Build.Collisions is { } collisions)
{
publication.GfxObjectIds = [.. collisions.GfxObjIds];
publication.PreparedGeneration.SetAssetClosure(
publication.GfxObjectIds,
publication.SetupObjectIds);
publication.PreparationCursor = entities.Count;
publication.PreparationCommitted = true;
return true;
@ -256,6 +275,9 @@ public sealed class LandblockPhysicsPublisher
}
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
publication.PreparedGeneration.SetAssetClosure(
publication.GfxObjectIds,
publication.SetupObjectIds);
publication.PreparationCommitted = true;
return true;
}
@ -299,9 +321,9 @@ public sealed class LandblockPhysicsPublisher
// CacheCellStruct/CacheBuilding use first-wins semantics within one
// publication, so the replacement pass starts with one exact
// landblock-scoped withdrawal.
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
publication.StagingCache.RemoveCellsForLandblock(landblock.LandblockId);
publication.StagingCache.RemoveBuildingsForLandblock(landblock.LandblockId);
publication.StagingCache.CellGraph.RemoveEnvCellsForLandblock(
landblock.LandblockId);
publication.PriorCacheRemoved = true;
}
@ -330,6 +352,7 @@ public sealed class LandblockPhysicsPublisher
PublishBuilding(
landblock,
datBundle,
publication.StagingCache,
publication.TerrainSurface,
origin,
publication.Buildings[publication.BuildingCursor]);
@ -337,8 +360,9 @@ public sealed class LandblockPhysicsPublisher
}
else if (!publication.BaseCommitted)
{
_physics.AdmitCollisionAssets(
_physics.StageCollisionAssets(
publication.CollisionAdmission,
publication.PreparedGeneration,
new RuntimeLandblockCollisionAssets(
landblock.LandblockId,
publication.TerrainSurface,
@ -409,7 +433,7 @@ public sealed class LandblockPhysicsPublisher
gfxObjectId,
out FlatGfxObjCollisionAsset? prepared) == true)
{
_physicsDataCache.CacheGfxObj(gfxObjectId, prepared);
publication.StagingCache.CacheGfxObj(gfxObjectId, prepared);
}
else if (datBundle.GfxObjs.TryGetValue(
gfxObjectId,
@ -417,7 +441,7 @@ public sealed class LandblockPhysicsPublisher
{
// Graph-oracle fixture seam. Production near builds always
// carry the strict prepared closure.
_physicsDataCache.CacheGfxObj(gfxObjectId, source);
publication.StagingCache.CacheGfxObj(gfxObjectId, source);
}
publication.GfxCursor++;
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
@ -430,19 +454,19 @@ public sealed class LandblockPhysicsPublisher
setupId,
out FlatSetupCollision? prepared) == true)
{
_physicsDataCache.CacheSetup(setupId, prepared);
publication.StagingCache.CacheSetup(setupId, prepared);
}
else if (datBundle.Setups.TryGetValue(setupId, out var source))
{
// Graph-oracle fixture seam only.
_physicsDataCache.CacheSetup(setupId, source);
publication.StagingCache.CacheSetup(setupId, source);
}
publication.SetupCursor++;
}
else if (publication.PriorStaticCursor
< publication.PriorStaticOwnerIds.Length)
{
_physicsEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
publication.StagingEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
publication.PriorStaticOwnerIds[
publication.PriorStaticCursor],
landblock.LandblockId);
@ -458,15 +482,17 @@ public sealed class LandblockPhysicsPublisher
else if (publication.RefloodOwnerIds is null)
{
publication.RefloodOwnerIds =
_physicsEngine.ShadowObjects
.CaptureRefloodOwnersForLandblock(landblock.LandblockId);
_physics.CaptureCollisionDynamicOwners(
publication.CollisionAdmission,
publication.PreparedGeneration);
}
else if (publication.RefloodCursor
< publication.RefloodOwnerIds.Length)
{
_physicsEngine.ShadowObjects.RefloodOwnerForLandblock(
publication.RefloodOwnerIds[publication.RefloodCursor],
landblock.LandblockId);
_physics.RefreshCollisionDynamicOwner(
publication.CollisionAdmission,
publication.PreparedGeneration,
publication.RefloodOwnerIds[publication.RefloodCursor]);
publication.RefloodCursor++;
}
else if (!publication.RefloodCommitted)
@ -478,14 +504,24 @@ public sealed class LandblockPhysicsPublisher
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
$"(outdoorNone={publication.NoCollisionCount})");
}
LogMissingSceneryBounds(landblock);
_refloodCount++;
LogMissingSceneryBounds(landblock, publication.StagingCache);
publication.RefloodCommitted = true;
}
else
{
_physics.CompleteCollisionAdmission(
publication.CollisionAdmission);
RuntimeCollisionGenerationCommit commit =
_physics.CommitCollisionGeneration(
publication.CollisionAdmission,
publication.PreparedGeneration);
if (!commit.Committed)
{
publication.RefloodOwnerIds = commit.DirtyDynamicOwnerIds;
publication.RefloodCursor = 0;
publication.RefloodCommitted = false;
_completePublishTicks += Stopwatch.GetTimestamp() - started;
return false;
}
_refloodCount++;
_staticBspOwnerCount += publication.BspOwnerCount;
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
publication.CompletionCommitted = true;
@ -545,7 +581,7 @@ public sealed class LandblockPhysicsPublisher
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(cellOriginWorld);
_physicsDataCache.CacheCellStruct(
publication.StagingCache.CacheCellStruct(
envCellId,
envCell,
physicsCellTransform,
@ -623,7 +659,7 @@ public sealed class LandblockPhysicsPublisher
Matrix4x4 physicsCellTransform =
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(cellOriginWorld);
_physicsDataCache.CacheCellStruct(
publication.StagingCache.CacheCellStruct(
envCellId,
envCell,
cellStruct,
@ -687,6 +723,7 @@ public sealed class LandblockPhysicsPublisher
private void PublishBuilding(
LoadedLandblock landblock,
PhysicsDatBundle datBundle,
PhysicsDataCache cache,
TerrainSurface terrainSurface,
Vector3 origin,
BuildingInfo building)
@ -720,7 +757,7 @@ public sealed class LandblockPhysicsPublisher
? setup.Parts[0]
: 0u;
}
_physicsDataCache.CacheBuilding(
cache.CacheBuilding(
landcellId,
portals,
buildingTransform,
@ -752,11 +789,11 @@ public sealed class LandblockPhysicsPublisher
ShadowShapeBuilder.FromLandblockBspParts(
entity.MeshRefs,
entity.IsBuildingShell,
_physicsDataCache.GetGfxObj);
publication.StagingCache.GetGfxObj);
entityBspCount = bspShapes.Count;
if (entityBspCount > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
publication.StagingEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
@ -772,9 +809,9 @@ public sealed class LandblockPhysicsPublisher
}
FlatSetupCollision? setup =
_physicsDataCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
publication.StagingCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
if (setup is null
&& _physicsDataCache.GetSetup(
&& publication.StagingCache.GetSetup(
entity.SourceGfxObjOrSetupId) is { } graphSetup)
{
// Graph-oracle fixture seam only. Production Setup publication is
@ -858,7 +895,7 @@ public sealed class LandblockPhysicsPublisher
if (setupShapes.Count > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
publication.StagingEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
@ -917,7 +954,9 @@ public sealed class LandblockPhysicsPublisher
}
}
private void LogMissingSceneryBounds(LoadedLandblock landblock)
private static void LogMissingSceneryBounds(
LoadedLandblock landblock,
PhysicsDataCache cache)
{
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
return;
@ -933,7 +972,7 @@ public sealed class LandblockPhysicsPublisher
foreach (MeshRef meshRef in entity.MeshRefs)
{
GfxObjVisualBounds? bounds =
_physicsDataCache.GetVisualBounds(meshRef.GfxObjId);
cache.GetVisualBounds(meshRef.GfxObjId);
if (bounds is not null && bounds.Radius > 0f)
{
hasBounds = true;

View file

@ -215,6 +215,18 @@ public sealed class LandblockPresentationPipeline
public IReadOnlyList<LandblockStreamResult> GetPendingPublicationResults() =>
_publications.Keys.ToArray();
/// <summary>
/// Cancels retained publication receipts during a generation reset. A
/// collision receipt owns only its private staging world until activation,
/// so cancellation cannot withdraw or partially replace the active world.
/// </summary>
internal void CancelPendingPublications()
{
foreach (PublicationTransaction transaction in _publications.Values)
transaction.PhysicsPublication?.Dispose();
_publications.Clear();
}
public void ResumePublication(LandblockStreamResult result)
{
ArgumentNullException.ThrowIfNull(result);

View file

@ -32,6 +32,7 @@ public sealed class StreamingController
public bool GenerationAdvanced;
public bool PendingLoadsCleared;
public bool CompletionQueueCleared;
public bool PendingPublicationsCleared;
public bool RegionCleared;
public bool SpatialGenerationDetached;
public bool PreparationCommitted;
@ -45,6 +46,7 @@ public sealed class StreamingController
public bool GenerationAdvanced;
public bool PendingLoadsCleared;
public bool CompletionQueueCleared;
public bool PendingPublicationsCleared;
public bool RegionCleared;
public List<uint>? ResidentIds;
public IEnumerator<uint>? ResidentEnumerator;
@ -1358,6 +1360,22 @@ public sealed class StreamingController
}
if (!transaction.RegionCleared)
{
if (!transaction.PendingPublicationsCleared)
{
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),
"recenter-cancel-publications",
() =>
{
_presentation.CancelPendingPublications();
transaction.PendingPublicationsCleared = true;
return true;
}))
{
return false;
}
}
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),
@ -1499,6 +1517,22 @@ public sealed class StreamingController
if (!transaction.RegionCleared)
{
if (!transaction.PendingPublicationsCleared)
{
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),
"reload-cancel-publications",
() =>
{
_presentation.CancelPendingPublications();
transaction.PendingPublicationsCleared = true;
return true;
}))
{
return false;
}
}
if (!TryRunStreamingWork(
meter,
new StreamingWorkCost(EntityOperations: 1),