feat(streaming): shadow-publish flat collision assets
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
f7ff9f4eea
commit
d9446030e6
32 changed files with 2777 additions and 92 deletions
|
|
@ -11,9 +11,10 @@ namespace AcDream.Core.Physics;
|
|||
|
||||
/// <summary>
|
||||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||
/// dat objects during streaming. Populated by the streaming worker thread;
|
||||
/// read by the physics engine on the game/render thread. ConcurrentDictionary
|
||||
/// makes cross-thread access safe without a global lock.
|
||||
/// dat objects during streaming and live-object materialization. Prepared
|
||||
/// payloads are built/read off the hot path; one update-thread publisher
|
||||
/// installs each graph/flat pair. ConcurrentDictionary keeps diagnostics and
|
||||
/// tooling reads safe without a global lock.
|
||||
/// </summary>
|
||||
public sealed class PhysicsDataCache
|
||||
{
|
||||
|
|
@ -21,6 +22,29 @@ public sealed class PhysicsDataCache
|
|||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatGfxObjCollisionAsset>
|
||||
_flatGfxObj = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatSetupCollision>
|
||||
_flatSetup = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
|
||||
_flatCellStruct = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatEnvCellTopology>
|
||||
_flatEnvCell = new();
|
||||
|
||||
public PhysicsDataCache()
|
||||
{
|
||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
CollisionShadow = new CollisionShadowVerifier(
|
||||
PhysicsDiagnostics.CollisionShadowSampleEvery,
|
||||
PhysicsDiagnostics.CollisionShadowArtifactDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||
|
||||
public CollisionShadowStats CollisionShadowStats =>
|
||||
CollisionShadow?.Stats ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Slice I graph/flat differential selector. Production remains on the
|
||||
|
|
@ -55,8 +79,14 @@ public sealed class PhysicsDataCache
|
|||
/// user collide with decorative meshes that don't have a CylSphere or
|
||||
/// per-part BSP.
|
||||
/// </summary>
|
||||
public void CacheGfxObj(uint gfxObjId, GfxObj gfxObj)
|
||||
public void CacheGfxObj(
|
||||
uint gfxObjId,
|
||||
GfxObj gfxObj,
|
||||
FlatGfxObjCollisionAsset? prepared = null)
|
||||
{
|
||||
if (prepared is not null)
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
|
||||
// Always cache a visual AABB from the mesh vertices — this is cheap
|
||||
// and fed by the mesh data that's already loaded. It serves as the
|
||||
// fallback collision shape for pure-visual entities.
|
||||
|
|
@ -65,18 +95,25 @@ public sealed class PhysicsDataCache
|
|||
_visualBounds[gfxObjId] = ComputeVisualBounds(gfxObj.VertexArray);
|
||||
}
|
||||
|
||||
if (_gfxObj.ContainsKey(gfxObjId)) return;
|
||||
if (_gfxObj.TryGetValue(gfxObjId, out GfxObjPhysics? existing))
|
||||
{
|
||||
if (prepared is not null)
|
||||
existing.FlatPhysicsBsp ??= prepared.PhysicsBsp;
|
||||
return;
|
||||
}
|
||||
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics)) return;
|
||||
if (gfxObj.PhysicsBSP?.Root is null) return;
|
||||
if (gfxObj.VertexArray is null) return;
|
||||
|
||||
var physics = new GfxObjPhysics
|
||||
{
|
||||
SourceId = gfxObjId,
|
||||
BSP = gfxObj.PhysicsBSP,
|
||||
PhysicsPolygons = gfxObj.PhysicsPolygons,
|
||||
BoundingSphere = gfxObj.PhysicsBSP.Root.BoundingSphere,
|
||||
Vertices = gfxObj.VertexArray,
|
||||
Resolved = ResolvePolygons(gfxObj.PhysicsPolygons, gfxObj.VertexArray),
|
||||
FlatPhysicsBsp = prepared?.PhysicsBsp,
|
||||
};
|
||||
_gfxObj[gfxObjId] = physics;
|
||||
|
||||
|
|
@ -157,17 +194,30 @@ public sealed class PhysicsDataCache
|
|||
/// Extract and cache the collision shape data from a Setup.
|
||||
/// No-ops if the id is already cached.
|
||||
/// </summary>
|
||||
public void CacheSetup(uint setupId, Setup setup)
|
||||
public void CacheSetup(
|
||||
uint setupId,
|
||||
Setup setup,
|
||||
FlatSetupCollision? prepared = null)
|
||||
{
|
||||
if (_setup.ContainsKey(setupId)) return;
|
||||
if (prepared is not null)
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
|
||||
if (_setup.TryGetValue(setupId, out SetupPhysics? existing))
|
||||
{
|
||||
if (prepared is not null)
|
||||
existing.FlatCollision ??= prepared;
|
||||
return;
|
||||
}
|
||||
_setup[setupId] = new SetupPhysics
|
||||
{
|
||||
SourceId = setupId,
|
||||
CylSpheres = setup.CylSpheres ?? new(),
|
||||
Spheres = setup.Spheres ?? new(),
|
||||
Height = setup.Height,
|
||||
Radius = setup.Radius,
|
||||
StepUpHeight = setup.StepUpHeight,
|
||||
StepDownHeight = setup.StepDownHeight,
|
||||
FlatCollision = prepared,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +226,19 @@ public sealed class PhysicsDataCache
|
|||
/// (indoor room geometry). No-ops if the id is already cached or the
|
||||
/// CellStruct has no physics BSP.
|
||||
/// </summary>
|
||||
public void CacheCellStruct(uint envCellId, DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
CellStruct cellStruct, Matrix4x4 worldTransform)
|
||||
public void CacheCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
CellStruct cellStruct,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||
FlatEnvCellTopology? preparedTopology = null)
|
||||
{
|
||||
if (preparedStructure is not null)
|
||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||
if (preparedTopology is not null)
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
// 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))
|
||||
|
|
@ -216,12 +276,17 @@ public sealed class PhysicsDataCache
|
|||
|
||||
var cellPhysics = new CellPhysics
|
||||
{
|
||||
SourceId = envCellId,
|
||||
BSP = cellStruct.PhysicsBSP,
|
||||
PhysicsPolygons = cellStruct.PhysicsPolygons,
|
||||
Vertices = cellStruct.VertexArray,
|
||||
WorldTransform = worldTransform,
|
||||
InverseWorldTransform = inverseTransform,
|
||||
Resolved = resolved,
|
||||
FlatPhysicsBsp = preparedStructure?.PhysicsBsp,
|
||||
FlatContainmentBsp = preparedStructure?.ContainmentBsp,
|
||||
FlatPortalPolygons = preparedStructure?.PortalPolygons,
|
||||
FlatTopology = preparedTopology,
|
||||
// ── Phase 2 portal fields ──
|
||||
CellBSP = cellStruct.CellBSP,
|
||||
Portals = portals,
|
||||
|
|
@ -394,9 +459,21 @@ public sealed class PhysicsDataCache
|
|||
|
||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
|
||||
_flatGfxObj.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatSetupCollision? GetFlatSetup(uint id) =>
|
||||
_flatSetup.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
|
||||
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
|
||||
_flatEnvCell.TryGetValue(id, out var value) ? value : null;
|
||||
public int GfxObjCount => _gfxObj.Count;
|
||||
public int SetupCount => _setup.Count;
|
||||
public int CellStructCount => _cellStruct.Count;
|
||||
public int FlatGfxObjCount => _flatGfxObj.Count;
|
||||
public int FlatSetupCount => _flatSetup.Count;
|
||||
public int FlatCellStructCount => _flatCellStruct.Count;
|
||||
public int FlatEnvCellCount => _flatEnvCell.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
||||
|
|
@ -483,6 +560,12 @@ public sealed class PhysicsDataCache
|
|||
foreach (var key in _cellStruct.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_cellStruct.TryRemove(key, out _);
|
||||
foreach (var key in _flatCellStruct.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_flatCellStruct.TryRemove(key, out _);
|
||||
foreach (var key in _flatEnvCell.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_flatEnvCell.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public BuildingPhysics? GetBuilding(uint landcellId)
|
||||
|
|
@ -539,6 +622,7 @@ public sealed class ResolvedPolygon
|
|||
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
||||
public sealed class GfxObjPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public required PhysicsBSPTree BSP { get; init; }
|
||||
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||
public Sphere? BoundingSphere { get; init; }
|
||||
|
|
@ -551,21 +635,24 @@ public sealed class GfxObjPhysics
|
|||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed shadow representation. Optional until Slice I5
|
||||
/// dual publication is complete.
|
||||
/// Prepared integer-indexed representation. Slice I5 guarantees this for
|
||||
/// production-published collision objects; graph-only test fixtures may
|
||||
/// omit it.
|
||||
/// </summary>
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
||||
public sealed class SetupPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public List<CylSphere> CylSpheres { get; init; } = new();
|
||||
public List<Sphere> Spheres { get; init; } = new();
|
||||
public float Height { get; init; }
|
||||
public float Radius { get; init; }
|
||||
public float StepUpHeight { get; init; }
|
||||
public float StepDownHeight { get; init; }
|
||||
public FlatSetupCollision? FlatCollision { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -575,6 +662,7 @@ public sealed class SetupPhysics
|
|||
/// </summary>
|
||||
public sealed class CellPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
/// <summary>
|
||||
/// The physics BSP tree for this cell. Nullable so that test fixtures
|
||||
/// can construct a <see cref="CellPhysics"/> from <see cref="Resolved"/>
|
||||
|
|
@ -593,7 +681,9 @@ public sealed class CellPhysics
|
|||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed physics BSP shadow. Optional until Slice I5.
|
||||
/// Prepared integer-indexed physics BSP. Slice I5 guarantees this for
|
||||
/// production-published collision cells; graph-only test fixtures may
|
||||
/// omit it.
|
||||
/// </summary>
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||
|
||||
|
|
@ -610,11 +700,23 @@ public sealed class CellPhysics
|
|||
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed cell-containment BSP shadow. Optional until
|
||||
/// Slice I5.
|
||||
/// Prepared integer-indexed cell-containment BSP. Slice I5 guarantees this
|
||||
/// for production-published cells; graph-only test fixtures may omit it.
|
||||
/// </summary>
|
||||
public FlatCellContainmentBsp? FlatContainmentBsp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared visible-polygon table addressed by
|
||||
/// <see cref="FlatEnvCellTopology.Portals"/>.
|
||||
/// </summary>
|
||||
public FlatPolygonTable? FlatPortalPolygons { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared per-EnvCell portal/visibility state. Placement remains on this
|
||||
/// runtime record's world transforms.
|
||||
/// </summary>
|
||||
public FlatEnvCellTopology? FlatTopology { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Portal connections to neighbouring cells, in cell-local space.
|
||||
/// Default: empty list. Source: <c>envCell.CellPortals</c>.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue