refactor(physics): remove parsed collision graphs
Publish prepared GfxObj, Setup, CellStruct, and EnvCell collision records without retaining their parsed DAT BSP, polygon, vertex, or shape graphs. Strip temporary physics bundles at stable world commit, keep graph traversal only as an explicit test/tooling oracle, and report graph residency from actual retained fields. Validated by 115 focused collision/streaming tests, a zero-warning Release build, and 8,413 passing Release tests with five pre-existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
feb80a67d9
commit
82f8d4f82e
18 changed files with 923 additions and 104 deletions
|
|
@ -13,9 +13,10 @@ namespace AcDream.Core.Physics;
|
|||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||
/// dat objects during streaming and live-object materialization. Prepared
|
||||
/// payloads are built/read off the hot path; one update-thread publisher
|
||||
/// installs prepared production records and, during the I6 acceptance window,
|
||||
/// their graph referee. ConcurrentDictionary keeps diagnostics and tooling
|
||||
/// reads safe without a global lock.
|
||||
/// installs prepared production records without retaining the parsed DAT
|
||||
/// collision graph. The public constructor remains the graph-oracle seam for
|
||||
/// conformance fixtures and bake/equivalence tooling. ConcurrentDictionary
|
||||
/// keeps diagnostics and tooling reads safe without a global lock.
|
||||
/// </summary>
|
||||
public sealed class PhysicsDataCache
|
||||
{
|
||||
|
|
@ -41,7 +42,8 @@ public sealed class PhysicsDataCache
|
|||
private PhysicsDataCache(bool requirePreparedCollision)
|
||||
{
|
||||
_requirePreparedCollision = requirePreparedCollision;
|
||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
if (!requirePreparedCollision
|
||||
&& PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
CollisionShadow = new CollisionShadowVerifier(
|
||||
PhysicsDiagnostics.CollisionShadowSampleEvery,
|
||||
|
|
@ -52,7 +54,7 @@ public sealed class PhysicsDataCache
|
|||
/// <summary>
|
||||
/// Creates the gameplay cache with Slice I6's prepared flat collision
|
||||
/// representation authoritative. The public constructor deliberately
|
||||
/// remains the graph-oracle fixture seam until the I6 referee is removed.
|
||||
/// remains the graph-oracle fixture seam for conformance and bake tooling.
|
||||
/// </summary>
|
||||
public static PhysicsDataCache CreateProduction()
|
||||
{
|
||||
|
|
@ -69,9 +71,8 @@ public sealed class PhysicsDataCache
|
|||
CollisionShadow?.Stats ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Slice I graph/flat differential selector. Production is flat-authoritative
|
||||
/// from I6 onward. Graph remains an explicit fixture/oracle mode until the
|
||||
/// referee is removed after acceptance.
|
||||
/// Slice I graph/flat differential selector. Production is permanently
|
||||
/// flat-authoritative. Graph remains an explicit fixture/oracle mode.
|
||||
/// </summary>
|
||||
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
||||
CollisionTraversalMode.Graph;
|
||||
|
|
@ -106,6 +107,15 @@ public sealed class PhysicsDataCache
|
|||
GfxObj gfxObj,
|
||||
FlatGfxObjCollisionAsset? prepared = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfxObj);
|
||||
|
||||
if (_requirePreparedCollision
|
||||
&& prepared is null
|
||||
&& _flatGfxObj.TryGetValue(gfxObjId, out var retainedPrepared))
|
||||
{
|
||||
prepared = retainedPrepared;
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics) &&
|
||||
gfxObj.PhysicsBSP?.Root is not null &&
|
||||
|
|
@ -115,6 +125,13 @@ public sealed class PhysicsDataCache
|
|||
throw MissingPreparedCollision("GfxObj", gfxObjId);
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
if (prepared is not null)
|
||||
CachePreparedGfxObj(gfxObjId, prepared);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prepared is not null)
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
|
||||
|
|
@ -169,6 +186,53 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared GfxObj without retaining its parsed DAT
|
||||
/// BSP, polygon dictionary, or vertex array.
|
||||
/// </summary>
|
||||
public void CacheGfxObj(
|
||||
uint gfxObjId,
|
||||
FlatGfxObjCollisionAsset prepared)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prepared);
|
||||
CachePreparedGfxObj(gfxObjId, prepared);
|
||||
}
|
||||
|
||||
private void CachePreparedGfxObj(
|
||||
uint gfxObjId,
|
||||
FlatGfxObjCollisionAsset prepared)
|
||||
{
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
if (prepared.VisualBounds is { } bounds)
|
||||
{
|
||||
_visualBounds.TryAdd(gfxObjId, new GfxObjVisualBounds
|
||||
{
|
||||
Min = bounds.Min,
|
||||
Max = bounds.Max,
|
||||
Center = bounds.Center,
|
||||
Radius = bounds.Radius,
|
||||
HalfExtents = bounds.HalfExtents,
|
||||
});
|
||||
}
|
||||
|
||||
if (prepared.PhysicsBsp.RootIndex < 0)
|
||||
return;
|
||||
|
||||
FlatCollisionSphere root =
|
||||
prepared.PhysicsBsp.Nodes[prepared.PhysicsBsp.RootIndex]
|
||||
.BoundingSphere;
|
||||
_gfxObj.TryAdd(gfxObjId, new GfxObjPhysics
|
||||
{
|
||||
SourceId = gfxObjId,
|
||||
BoundingSphere = new Sphere
|
||||
{
|
||||
Origin = root.Origin,
|
||||
Radius = root.Radius,
|
||||
},
|
||||
FlatPhysicsBsp = prepared.PhysicsBsp,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the cached visual AABB for a GfxObj, or null if not cached.
|
||||
/// </summary>
|
||||
|
|
@ -230,11 +294,29 @@ public sealed class PhysicsDataCache
|
|||
Setup setup,
|
||||
FlatSetupCollision? prepared = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
|
||||
if (_requirePreparedCollision
|
||||
&& prepared is null
|
||||
&& _flatSetup.TryGetValue(setupId, out var retainedPrepared))
|
||||
{
|
||||
prepared = retainedPrepared;
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
prepared is null &&
|
||||
!_flatSetup.ContainsKey(setupId))
|
||||
throw MissingPreparedCollision("Setup", setupId);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
CachePreparedSetup(
|
||||
setupId,
|
||||
prepared
|
||||
?? throw MissingPreparedCollision("Setup", setupId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (prepared is not null)
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
|
||||
|
|
@ -257,6 +339,30 @@ public sealed class PhysicsDataCache
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared Setup without retaining parsed DBObj
|
||||
/// cylinder/sphere lists.
|
||||
/// </summary>
|
||||
public void CacheSetup(uint setupId, FlatSetupCollision prepared)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prepared);
|
||||
CachePreparedSetup(setupId, prepared);
|
||||
}
|
||||
|
||||
private void CachePreparedSetup(uint setupId, FlatSetupCollision prepared)
|
||||
{
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
_setup.TryAdd(setupId, new SetupPhysics
|
||||
{
|
||||
SourceId = setupId,
|
||||
Height = prepared.Height,
|
||||
Radius = prepared.Radius,
|
||||
StepUpHeight = prepared.StepUpHeight,
|
||||
StepDownHeight = prepared.StepDownHeight,
|
||||
FlatCollision = prepared,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract and cache the physics BSP + polygon data from a CellStruct
|
||||
/// (indoor room geometry). No-ops if the id is already cached or the
|
||||
|
|
@ -270,6 +376,17 @@ public sealed class PhysicsDataCache
|
|||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||
FlatEnvCellTopology? preparedTopology = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envCell);
|
||||
ArgumentNullException.ThrowIfNull(cellStruct);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
if (preparedStructure is null)
|
||||
_flatCellStruct.TryGetValue(envCellId, out preparedStructure);
|
||||
if (preparedTopology is null)
|
||||
_flatEnvCell.TryGetValue(envCellId, out preparedTopology);
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
preparedStructure is null &&
|
||||
!_flatCellStruct.ContainsKey(envCellId))
|
||||
|
|
@ -284,6 +401,19 @@ public sealed class PhysicsDataCache
|
|||
if (preparedTopology is not null)
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
CachePreparedCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
worldTransform,
|
||||
preparedStructure
|
||||
?? throw MissingPreparedCollision("CellStruct", envCellId),
|
||||
preparedTopology
|
||||
?? throw MissingPreparedCollision("EnvCell topology", envCellId));
|
||||
return;
|
||||
}
|
||||
|
||||
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
||||
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
||||
if (!CellGraph.Contains(envCellId))
|
||||
|
|
@ -450,6 +580,83 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared EnvCell without retaining its parsed DAT
|
||||
/// CellStruct, BSP trees, polygon dictionaries, or vertex arrays.
|
||||
/// </summary>
|
||||
public void CacheCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset preparedStructure,
|
||||
FlatEnvCellTopology preparedTopology)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envCell);
|
||||
ArgumentNullException.ThrowIfNull(preparedStructure);
|
||||
ArgumentNullException.ThrowIfNull(preparedTopology);
|
||||
CachePreparedCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
worldTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
}
|
||||
|
||||
private void CachePreparedCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset preparedStructure,
|
||||
FlatEnvCellTopology preparedTopology)
|
||||
{
|
||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
if (!CellGraph.Contains(envCellId))
|
||||
{
|
||||
CellGraph.Add(UcgEnvCell.FromPrepared(
|
||||
envCellId,
|
||||
worldTransform,
|
||||
preparedStructure,
|
||||
preparedTopology));
|
||||
}
|
||||
|
||||
// Preserve CacheCellStruct's existing distinction: BSP-less cells
|
||||
// participate in the cell graph but do not masquerade as hydrated
|
||||
// collision cells.
|
||||
if (preparedStructure.PhysicsBsp.RootIndex < 0)
|
||||
return;
|
||||
if (_cellStruct.ContainsKey(envCellId))
|
||||
return;
|
||||
|
||||
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverseTransform);
|
||||
var portals = new List<PortalInfo>(preparedTopology.Portals.Length);
|
||||
for (int i = 0; i < preparedTopology.Portals.Length; i++)
|
||||
{
|
||||
FlatEnvCellPortal portal = preparedTopology.Portals[i];
|
||||
portals.Add(new PortalInfo(
|
||||
portal.OtherCellId,
|
||||
portal.PolygonId,
|
||||
portal.Flags));
|
||||
}
|
||||
|
||||
_cellStruct.TryAdd(envCellId, new CellPhysics
|
||||
{
|
||||
SourceId = envCellId,
|
||||
WorldTransform = worldTransform,
|
||||
InverseWorldTransform = inverseTransform,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
FlatPhysicsBsp = preparedStructure.PhysicsBsp,
|
||||
FlatContainmentBsp = preparedStructure.ContainmentBsp,
|
||||
FlatPortalPolygons = preparedStructure.PortalPolygons,
|
||||
FlatTopology = preparedTopology,
|
||||
Portals = portals,
|
||||
VisibleCellIds = new HashSet<uint>(
|
||||
preparedTopology.VisibleCellIds),
|
||||
SeenOutside = preparedTopology.SeenOutside,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-resolve all physics polygons: lookup vertex positions from VertexArray
|
||||
/// and compute the face plane. Matches ACE's Polygon constructor which calls
|
||||
|
|
@ -533,6 +740,54 @@ public sealed class PhysicsDataCache
|
|||
public int FlatSetupCount => _flatSetup.Count;
|
||||
public int FlatCellStructCount => _flatCellStruct.Count;
|
||||
public int FlatEnvCellCount => _flatEnvCell.Count;
|
||||
public int GraphGfxObjCount => CountRetainedGfxObjGraphs();
|
||||
public int GraphSetupCount => CountRetainedSetupGraphs();
|
||||
public int GraphCellStructCount => CountRetainedCellGraphs();
|
||||
|
||||
private int CountRetainedGfxObjGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (GfxObjPhysics value in _gfxObj.Values)
|
||||
{
|
||||
if (value.BSP is not null
|
||||
|| value.PhysicsPolygons is not null
|
||||
|| value.Vertices is not null
|
||||
|| value.Resolved.Count != 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountRetainedSetupGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (SetupPhysics value in _setup.Values)
|
||||
{
|
||||
if (value.CylSpheres.Count != 0 || value.Spheres.Count != 0)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountRetainedCellGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (CellPhysics value in _cellStruct.Values)
|
||||
{
|
||||
if (value.BSP is not null
|
||||
|| value.CellBSP is not null
|
||||
|| value.PhysicsPolygons is not null
|
||||
|| value.Vertices is not null
|
||||
|| value.Resolved.Count != 0
|
||||
|| value.PortalPolygons is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
||||
|
|
@ -682,16 +937,16 @@ public sealed class ResolvedPolygon
|
|||
public sealed class GfxObjPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public required PhysicsBSPTree BSP { get; init; }
|
||||
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||
public PhysicsBSPTree? BSP { get; init; }
|
||||
public Dictionary<ushort, Polygon>? PhysicsPolygons { get; init; }
|
||||
public Sphere? BoundingSphere { get; init; }
|
||||
public required VertexArray Vertices { get; init; }
|
||||
public VertexArray? Vertices { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-resolved polygon data with vertex positions and computed planes.
|
||||
/// Populated once at cache time so BSP queries don't pay per-test lookup cost.
|
||||
/// </summary>
|
||||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||
public Dictionary<ushort, ResolvedPolygon> Resolved { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed representation. Slice I5 guarantees this for
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue