perf(physics): cut production traversal to flat assets
Make prepared flat BSP data authoritative for gameplay while retaining the parsed graph only as an exact sampled referee. Fail production publication when collision package data is genuinely absent, keep idempotent already-cached publication valid, and move cell membership, floor lookup, camera diagnostics, and live/static shape bounds onto the flat representation. Validated by 8,402 Release tests, a strict dense-Arwic connected gate with 46,309/46,309 exact referee matches, and graceful shutdown. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
d9446030e6
commit
068a06518d
13 changed files with 743 additions and 43 deletions
|
|
@ -13,9 +13,9 @@ public readonly record struct CollisionShadowStats(
|
|||
long Faults);
|
||||
|
||||
/// <summary>
|
||||
/// Slice I5 sampled graph/flat referee. The graph result always remains
|
||||
/// authoritative. One retained clone receives the flat query first; the
|
||||
/// canonical transition is then evaluated by the graph path and compared
|
||||
/// Slice I5/I6 sampled graph/flat referee. The authority is selected by
|
||||
/// <see cref="PhysicsDataCache.CollisionTraversalMode"/>. One retained clone
|
||||
/// receives the non-authoritative query and the two results are compared
|
||||
/// field-for-field with exact float bits.
|
||||
/// </summary>
|
||||
internal sealed class CollisionShadowVerifier
|
||||
|
|
@ -29,6 +29,7 @@ internal sealed class CollisionShadowVerifier
|
|||
private long _mismatches;
|
||||
private long _faults;
|
||||
private bool _flatPass;
|
||||
private bool _graphPass;
|
||||
|
||||
public CollisionShadowVerifier(
|
||||
int sampleEvery,
|
||||
|
|
@ -42,6 +43,7 @@ internal sealed class CollisionShadowVerifier
|
|||
}
|
||||
|
||||
internal bool IsFlatPass => _flatPass;
|
||||
internal bool IsGraphPass => _graphPass;
|
||||
|
||||
internal CollisionShadowStats Stats => new(
|
||||
_queries,
|
||||
|
|
@ -52,7 +54,7 @@ internal sealed class CollisionShadowVerifier
|
|||
|
||||
internal bool TrySample(out long sample)
|
||||
{
|
||||
if (_flatPass)
|
||||
if (_flatPass || _graphPass)
|
||||
{
|
||||
sample = 0;
|
||||
return false;
|
||||
|
|
@ -77,9 +79,9 @@ internal sealed class CollisionShadowVerifier
|
|||
|
||||
internal void BeginFlatPass()
|
||||
{
|
||||
if (_flatPass)
|
||||
if (_flatPass || _graphPass)
|
||||
throw new InvalidOperationException(
|
||||
"Collision shadow flat passes cannot overlap.");
|
||||
"Collision shadow passes cannot overlap.");
|
||||
_flatPass = true;
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +93,22 @@ internal sealed class CollisionShadowVerifier
|
|||
_flatPass = false;
|
||||
}
|
||||
|
||||
internal void BeginGraphPass()
|
||||
{
|
||||
if (_flatPass || _graphPass)
|
||||
throw new InvalidOperationException(
|
||||
"Collision shadow passes cannot overlap.");
|
||||
_graphPass = true;
|
||||
}
|
||||
|
||||
internal void EndGraphPass()
|
||||
{
|
||||
if (!_graphPass)
|
||||
throw new InvalidOperationException(
|
||||
"No collision shadow graph pass is active.");
|
||||
_graphPass = false;
|
||||
}
|
||||
|
||||
internal void RecordBoolean(
|
||||
long sample,
|
||||
string kind,
|
||||
|
|
@ -188,7 +206,8 @@ internal sealed class CollisionShadowVerifier
|
|||
string kind,
|
||||
uint sourceId,
|
||||
Exception fault,
|
||||
string input)
|
||||
string input,
|
||||
string authority = "graph")
|
||||
{
|
||||
_faults++;
|
||||
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||
|
|
@ -196,8 +215,8 @@ internal sealed class CollisionShadowVerifier
|
|||
sample,
|
||||
kind,
|
||||
$"0x{sourceId:X8}",
|
||||
"FlatFault",
|
||||
"graph-authoritative",
|
||||
authority == "flat" ? "GraphFault" : "FlatFault",
|
||||
$"{authority}-authoritative",
|
||||
$"{fault.GetType().FullName}: {fault.Message}",
|
||||
input));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ using System.Numerics;
|
|||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary Slice I referee mode. Graph remains production-authoritative;
|
||||
/// Flat is enabled only by differential tests until dual publication and the
|
||||
/// connected shadow gate complete.
|
||||
/// Temporary Slice I6 authority/referee selector. Gameplay uses
|
||||
/// <see cref="Flat"/>; <see cref="Graph"/> remains only for the old-oracle
|
||||
/// differential fixtures until the accepted cutover deletes it.
|
||||
/// </summary>
|
||||
internal enum CollisionTraversalMode
|
||||
{
|
||||
|
|
@ -14,8 +14,8 @@ internal enum CollisionTraversalMode
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Narrow representation switch used while the graph and flat collision
|
||||
/// worlds coexist. I6 removes the graph branch after acceptance.
|
||||
/// Narrow representation switch used while flat production and the graph
|
||||
/// referee coexist. I6 removes the graph branch after acceptance.
|
||||
/// </summary>
|
||||
internal static class CollisionTraversal
|
||||
{
|
||||
|
|
@ -27,7 +27,49 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
return flat.RootIndex >= 0;
|
||||
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
bool graphRefereeResult = false;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = cell.CellBSP?.Root is not null;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordBoolean(
|
||||
flatAuthoritySample,
|
||||
"HasCellContainment",
|
||||
cell.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"HasCellContainment",
|
||||
cell.SourceId,
|
||||
graphRefereeFault,
|
||||
string.Empty,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -82,7 +124,49 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
return flat.RootIndex >= 0;
|
||||
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
bool graphRefereeResult = false;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = cell.BSP?.Root is not null;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordBoolean(
|
||||
flatAuthoritySample,
|
||||
"HasCellPhysics",
|
||||
cell.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"HasCellPhysics",
|
||||
cell.SourceId,
|
||||
graphRefereeFault,
|
||||
string.Empty,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -137,7 +221,49 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics");
|
||||
return flat.RootIndex >= 0;
|
||||
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
bool graphRefereeResult = false;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = gfxObject.BSP.Root is not null;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordBoolean(
|
||||
flatAuthoritySample,
|
||||
"HasGfxObjPhysics",
|
||||
gfxObject.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"HasGfxObjPhysics",
|
||||
gfxObject.SourceId,
|
||||
graphRefereeFault,
|
||||
string.Empty,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -192,7 +318,43 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
return flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||
FlatCollisionSphere flatAuthorityResult =
|
||||
flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
try
|
||||
{
|
||||
flatShadow.BeginGraphPass();
|
||||
DatReaderWriter.Types.Sphere graphSphere =
|
||||
cell.BSP!.Root!.BoundingSphere;
|
||||
var graphRefereeResult =
|
||||
new FlatCollisionSphere(
|
||||
graphSphere.Origin,
|
||||
graphSphere.Radius);
|
||||
flatShadow.EndGraphPass();
|
||||
flatShadow.RecordSphere(
|
||||
flatAuthoritySample,
|
||||
"RootBoundingSphere",
|
||||
cell.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
if (flatShadow.IsGraphPass)
|
||||
flatShadow.EndGraphPass();
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"RootBoundingSphere",
|
||||
cell.SourceId,
|
||||
fault,
|
||||
string.Empty,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
||||
|
|
@ -240,7 +402,54 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||
bool flatAuthorityResult =
|
||||
FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
bool graphRefereeResult = false;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = BSPQuery.PointInsideCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localPoint);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||
localPoint,
|
||||
0f);
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordBoolean(
|
||||
flatAuthoritySample,
|
||||
"PointInsideCell",
|
||||
cell.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult,
|
||||
flatAuthorityInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"PointInsideCell",
|
||||
cell.SourceId,
|
||||
graphRefereeFault,
|
||||
flatAuthorityInput,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -305,10 +514,57 @@ internal static class CollisionTraversal
|
|||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
return FlatBspQuery.SphereIntersectsCellBsp(
|
||||
bool flatAuthorityResult = FlatBspQuery.SphereIntersectsCellBsp(
|
||||
flat,
|
||||
localCenter,
|
||||
radius);
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
if (flatShadow is null ||
|
||||
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||
return flatAuthorityResult;
|
||||
|
||||
bool graphRefereeResult = false;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = BSPQuery.SphereIntersectsCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localCenter,
|
||||
radius);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||
localCenter,
|
||||
radius);
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordBoolean(
|
||||
flatAuthoritySample,
|
||||
"SphereIntersectsCell",
|
||||
cell.SourceId,
|
||||
graphRefereeResult,
|
||||
flatAuthorityResult,
|
||||
flatAuthorityInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"SphereIntersectsCell",
|
||||
cell.SourceId,
|
||||
graphRefereeFault,
|
||||
flatAuthorityInput,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityResult;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -386,9 +642,17 @@ internal static class CollisionTraversal
|
|||
{
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
long flatAuthoritySample = 0;
|
||||
bool sampled =
|
||||
flatShadow is not null &&
|
||||
flatShadow.TrySample(out flatAuthoritySample);
|
||||
Transition? graphTransition = sampled
|
||||
? flatShadow!.PrepareShadow(transition)
|
||||
: null;
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
return FlatBspQuery.FindCollisions(
|
||||
TransitionState flatAuthorityState = FlatBspQuery.FindCollisions(
|
||||
flat,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
|
|
@ -402,6 +666,73 @@ internal static class CollisionTraversal
|
|||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
if (!sampled)
|
||||
return flatAuthorityState;
|
||||
|
||||
TransitionState graphRefereeState = TransitionState.Invalid;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow!.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeState = BSPQuery.FindCollisions(
|
||||
cell.BSP?.Root,
|
||||
cell.Resolved,
|
||||
graphTransition!,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
|
||||
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
worldOrigin);
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordTransition(
|
||||
flatAuthoritySample,
|
||||
"FindCellCollisions",
|
||||
cell.SourceId,
|
||||
graphRefereeState,
|
||||
flatAuthorityState,
|
||||
graphTransition!,
|
||||
transition,
|
||||
flatAuthorityInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"FindCellCollisions",
|
||||
cell.SourceId,
|
||||
graphRefereeFault,
|
||||
flatAuthorityInput,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityState;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -523,9 +854,17 @@ internal static class CollisionTraversal
|
|||
{
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||
long flatAuthoritySample = 0;
|
||||
bool sampled =
|
||||
flatShadow is not null &&
|
||||
flatShadow.TrySample(out flatAuthoritySample);
|
||||
Transition? graphTransition = sampled
|
||||
? flatShadow!.PrepareShadow(transition)
|
||||
: null;
|
||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics");
|
||||
return FlatBspQuery.FindCollisions(
|
||||
TransitionState flatAuthorityState = FlatBspQuery.FindCollisions(
|
||||
flat,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
|
|
@ -539,6 +878,73 @@ internal static class CollisionTraversal
|
|||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
if (!sampled)
|
||||
return flatAuthorityState;
|
||||
|
||||
TransitionState graphRefereeState = TransitionState.Invalid;
|
||||
Exception? graphRefereeFault = null;
|
||||
flatShadow!.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeState = BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.Resolved,
|
||||
graphTransition!,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
graphRefereeFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
flatShadow.EndGraphPass();
|
||||
}
|
||||
|
||||
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
worldOrigin);
|
||||
if (graphRefereeFault is null)
|
||||
{
|
||||
flatShadow.RecordTransition(
|
||||
flatAuthoritySample,
|
||||
"FindGfxObjCollisions",
|
||||
gfxObject.SourceId,
|
||||
graphRefereeState,
|
||||
flatAuthorityState,
|
||||
graphTransition!,
|
||||
transition,
|
||||
flatAuthorityInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
flatShadow.RecordFault(
|
||||
flatAuthoritySample,
|
||||
"FindGfxObjCollisions",
|
||||
gfxObject.SourceId,
|
||||
graphRefereeFault,
|
||||
flatAuthorityInput,
|
||||
"flat");
|
||||
}
|
||||
return flatAuthorityState;
|
||||
}
|
||||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
|
|
@ -642,9 +1048,15 @@ internal static class CollisionTraversal
|
|||
return graphState;
|
||||
}
|
||||
|
||||
private static bool UseFlat(PhysicsDataCache cache) =>
|
||||
cache.CollisionTraversalMode == CollisionTraversalMode.Flat ||
|
||||
cache.CollisionShadow?.IsFlatPass == true;
|
||||
private static bool UseFlat(PhysicsDataCache cache)
|
||||
{
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow?.IsGraphPass == true)
|
||||
return false;
|
||||
if (shadow?.IsFlatPass == true)
|
||||
return true;
|
||||
return cache.CollisionTraversalMode == CollisionTraversalMode.Flat;
|
||||
}
|
||||
|
||||
private static InvalidOperationException MissingFlat(string kind) =>
|
||||
new(
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@ public readonly record struct FlatPhysicsBspNode(
|
|||
|
||||
/// <summary>
|
||||
/// Immutable, deterministic pre-order representation of a retail physics BSP.
|
||||
/// It changes source storage only; traversal order and behavior remain defined
|
||||
/// by the graph oracle until Slice I6.
|
||||
/// It changes source storage only; traversal order and behavior remain the
|
||||
/// retail port pinned by the graph differential oracle.
|
||||
/// </summary>
|
||||
public sealed class FlatPhysicsBsp
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ 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 each graph/flat pair. ConcurrentDictionary keeps diagnostics and
|
||||
/// tooling reads safe without a global lock.
|
||||
/// installs prepared production records and, during the I6 acceptance window,
|
||||
/// their graph referee. ConcurrentDictionary keeps diagnostics and tooling
|
||||
/// reads safe without a global lock.
|
||||
/// </summary>
|
||||
public sealed class PhysicsDataCache
|
||||
{
|
||||
private readonly bool _requirePreparedCollision;
|
||||
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
||||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||
|
|
@ -32,7 +34,13 @@ public sealed class PhysicsDataCache
|
|||
_flatEnvCell = new();
|
||||
|
||||
public PhysicsDataCache()
|
||||
: this(requirePreparedCollision: false)
|
||||
{
|
||||
}
|
||||
|
||||
private PhysicsDataCache(bool requirePreparedCollision)
|
||||
{
|
||||
_requirePreparedCollision = requirePreparedCollision;
|
||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
CollisionShadow = new CollisionShadowVerifier(
|
||||
|
|
@ -41,15 +49,29 @@ 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.
|
||||
/// </summary>
|
||||
public static PhysicsDataCache CreateProduction()
|
||||
{
|
||||
var cache = new PhysicsDataCache(requirePreparedCollision: true)
|
||||
{
|
||||
CollisionTraversalMode = CollisionTraversalMode.Flat,
|
||||
};
|
||||
return cache;
|
||||
}
|
||||
|
||||
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||
|
||||
public CollisionShadowStats CollisionShadowStats =>
|
||||
CollisionShadow?.Stats ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Slice I graph/flat differential selector. Production remains on the
|
||||
/// graph oracle until I6; tests use the flat value to run the same complete
|
||||
/// resolver from cloned inputs.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
||||
CollisionTraversalMode.Graph;
|
||||
|
|
@ -84,6 +106,15 @@ public sealed class PhysicsDataCache
|
|||
GfxObj gfxObj,
|
||||
FlatGfxObjCollisionAsset? prepared = null)
|
||||
{
|
||||
if (_requirePreparedCollision &&
|
||||
gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics) &&
|
||||
gfxObj.PhysicsBSP?.Root is not null &&
|
||||
prepared?.PhysicsBsp is null &&
|
||||
!_flatGfxObj.ContainsKey(gfxObjId))
|
||||
{
|
||||
throw MissingPreparedCollision("GfxObj", gfxObjId);
|
||||
}
|
||||
|
||||
if (prepared is not null)
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
|
||||
|
|
@ -199,6 +230,11 @@ public sealed class PhysicsDataCache
|
|||
Setup setup,
|
||||
FlatSetupCollision? prepared = null)
|
||||
{
|
||||
if (_requirePreparedCollision &&
|
||||
prepared is null &&
|
||||
!_flatSetup.ContainsKey(setupId))
|
||||
throw MissingPreparedCollision("Setup", setupId);
|
||||
|
||||
if (prepared is not null)
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
|
||||
|
|
@ -234,6 +270,15 @@ public sealed class PhysicsDataCache
|
|||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||
FlatEnvCellTopology? preparedTopology = null)
|
||||
{
|
||||
if (_requirePreparedCollision &&
|
||||
preparedStructure is null &&
|
||||
!_flatCellStruct.ContainsKey(envCellId))
|
||||
throw MissingPreparedCollision("CellStruct", envCellId);
|
||||
if (_requirePreparedCollision &&
|
||||
preparedTopology is null &&
|
||||
!_flatEnvCell.ContainsKey(envCellId))
|
||||
throw MissingPreparedCollision("EnvCell topology", envCellId);
|
||||
|
||||
if (preparedStructure is not null)
|
||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||
if (preparedTopology is not null)
|
||||
|
|
@ -242,7 +287,14 @@ public sealed class PhysicsDataCache
|
|||
// 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))
|
||||
CellGraph.Add(UcgEnvCell.FromDat(envCellId, envCell, cellStruct, worldTransform));
|
||||
{
|
||||
CellGraph.Add(UcgEnvCell.FromDat(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
worldTransform,
|
||||
preparedStructure?.ContainmentBsp));
|
||||
}
|
||||
|
||||
if (_cellStruct.ContainsKey(envCellId)) return;
|
||||
if (cellStruct.PhysicsBSP?.Root is null) return;
|
||||
|
|
@ -455,6 +507,13 @@ public sealed class PhysicsDataCache
|
|||
return resolved;
|
||||
}
|
||||
|
||||
private static InvalidOperationException MissingPreparedCollision(
|
||||
string kind,
|
||||
uint sourceId) =>
|
||||
new(
|
||||
$"Production {kind} 0x{sourceId:X8} has no prepared collision asset. " +
|
||||
"Gameplay must not extract or fall back to a parsed DAT graph.");
|
||||
|
||||
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
||||
|
||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||
|
|
|
|||
|
|
@ -509,6 +509,39 @@ public sealed class PhysicsEngine
|
|||
|
||||
float? best = null;
|
||||
float bestDist = float.MaxValue;
|
||||
FlatPhysicsBsp? flat = cp.FlatPhysicsBsp;
|
||||
if (flat is not null)
|
||||
{
|
||||
FlatPolygonTable table = flat.PolygonTable;
|
||||
for (int i = 0; i < table.Polygons.Length; i++)
|
||||
{
|
||||
FlatCollisionPolygon poly = table.Polygons[i];
|
||||
Vector3 n = poly.Plane.Normal;
|
||||
if (n.Z < PhysicsGlobals.FloorZ) continue;
|
||||
if (!PointInPolygonXY(table, poly.VertexRange, local.X, local.Y))
|
||||
continue;
|
||||
float lz =
|
||||
-(n.X * local.X + n.Y * local.Y + poly.Plane.D) / n.Z;
|
||||
float wz = Vector3.Transform(
|
||||
new Vector3(local.X, local.Y, lz),
|
||||
cp.WorldTransform).Z;
|
||||
float dist = MathF.Abs(wz - referenceZ);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
best = wz;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
if (DataCache!.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Production CellStruct 0x{cellId:X8} has no prepared physics BSP.");
|
||||
}
|
||||
|
||||
// Explicit graph-oracle fixture path. Production returns above.
|
||||
foreach (var kv in cp.Resolved)
|
||||
{
|
||||
var poly = kv.Value;
|
||||
|
|
@ -524,6 +557,28 @@ public sealed class PhysicsEngine
|
|||
return best;
|
||||
}
|
||||
|
||||
private static bool PointInPolygonXY(
|
||||
FlatPolygonTable table,
|
||||
FlatIndexRange range,
|
||||
float x,
|
||||
float y)
|
||||
{
|
||||
bool inside = false;
|
||||
int end = range.EndExclusive;
|
||||
for (int i = range.Start, j = end - 1; i < end; j = i++)
|
||||
{
|
||||
Vector3 vi = table.Vertices[i];
|
||||
Vector3 vj = table.Vertices[j];
|
||||
if ((vi.Y > y) != (vj.Y > y)
|
||||
&& x < (vj.X - vi.X) * (y - vi.Y) /
|
||||
(vj.Y - vi.Y) + vi.X)
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/// <summary>Even-odd XY-projection point-in-polygon test (cell-local frame).</summary>
|
||||
private static bool PointInPolygonXY(IReadOnlyList<Vector3> verts, float x, float y)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -191,7 +191,11 @@ public static class ShadowShapeBuilder
|
|||
foreach (var meshRef in meshRefs)
|
||||
{
|
||||
var phys = getGfxObj(meshRef.GfxObjId);
|
||||
if (phys?.BSP?.Root is null) continue; // no physics BSP → nothing to collide
|
||||
if (phys is null) continue;
|
||||
FlatPhysicsBsp? flat = phys.FlatPhysicsBsp;
|
||||
bool hasFlat = flat is { RootIndex: >= 0 };
|
||||
if (!hasFlat && phys.BSP?.Root is null)
|
||||
continue; // graph-only fixture seam until I6 referee removal
|
||||
|
||||
// PartTransform is root-relative; decompose to local pos/rot/scale.
|
||||
if (!Matrix4x4.Decompose(meshRef.PartTransform,
|
||||
|
|
@ -205,7 +209,9 @@ public static class ShadowShapeBuilder
|
|||
}
|
||||
|
||||
float partScale = pScale.X > 0f ? pScale.X : 1f; // AC objects are uniformly scaled
|
||||
float localRadius = phys.BoundingSphere?.Radius ?? 1f;
|
||||
float localRadius = hasFlat
|
||||
? flat!.Nodes[flat.RootIndex].BoundingSphere.Radius
|
||||
: phys.BoundingSphere?.Radius ?? 1f;
|
||||
|
||||
shapes.Add(new ShadowShape(
|
||||
GfxObjId: meshRef.GfxObjId,
|
||||
|
|
|
|||
|
|
@ -13,19 +13,30 @@ public sealed class EnvCell : ObjCell
|
|||
/// <summary>Cell-containment BSP (retail CellStruct.CellBSP). Null => AABB fallback.</summary>
|
||||
public CellBSPTree? ContainmentBsp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared production containment tree. Slice I6 makes this path
|
||||
/// authoritative while <see cref="ContainmentBsp"/> remains only as the
|
||||
/// temporary graph referee/test fixture seam.
|
||||
/// </summary>
|
||||
public FlatCellContainmentBsp? FlatContainmentBsp { get; }
|
||||
|
||||
public EnvCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform,
|
||||
Vector3 localBoundsMin, Vector3 localBoundsMax,
|
||||
IReadOnlyList<CellPortal> portals, IReadOnlyList<uint> stabList,
|
||||
bool seenOutside, CellBSPTree? containmentBsp)
|
||||
bool seenOutside, CellBSPTree? containmentBsp,
|
||||
FlatCellContainmentBsp? flatContainmentBsp = null)
|
||||
: base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax,
|
||||
portals, stabList, seenOutside)
|
||||
{
|
||||
ContainmentBsp = containmentBsp;
|
||||
FlatContainmentBsp = flatContainmentBsp;
|
||||
}
|
||||
|
||||
public override bool PointInCell(Vector3 worldPoint)
|
||||
{
|
||||
var local = Vector3.Transform(worldPoint, InverseWorldTransform);
|
||||
if (FlatContainmentBsp is not null)
|
||||
return FlatBspQuery.PointInsideCellBsp(FlatContainmentBsp, local);
|
||||
if (ContainmentBsp?.Root is not null)
|
||||
return BSPQuery.PointInsideCellBsp(ContainmentBsp.Root, local); // BSPQuery.cs:1034
|
||||
return local.X >= LocalBoundsMin.X && local.X <= LocalBoundsMax.X
|
||||
|
|
@ -40,7 +51,8 @@ public sealed class EnvCell : ObjCell
|
|||
/// (no +2 cm render lift).
|
||||
/// </summary>
|
||||
public static EnvCell FromDat(uint id, DatReaderWriter.DBObjs.EnvCell datCell,
|
||||
CellStruct cellStruct, Matrix4x4 worldTransform)
|
||||
CellStruct cellStruct, Matrix4x4 worldTransform,
|
||||
FlatCellContainmentBsp? flatContainmentBsp = null)
|
||||
{
|
||||
Matrix4x4.Invert(worldTransform, out var inverse);
|
||||
|
||||
|
|
@ -72,7 +84,7 @@ public sealed class EnvCell : ObjCell
|
|||
bool seenOutside = datCell.Flags.HasFlag(EnvCellFlags.SeenOutside);
|
||||
|
||||
return new EnvCell(id, worldTransform, inverse, min, max, portals, stab,
|
||||
seenOutside, cellStruct.CellBSP);
|
||||
seenOutside, cellStruct.CellBSP, flatContainmentBsp);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Vector3> ResolvePortalPolygon(CellStruct cellStruct, ushort polygonId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue