docs(physics): pin Slice I collision oracle

This commit is contained in:
Erik 2026-07-25 06:27:09 +02:00
parent f1a8d36682
commit 624e1119ca
3 changed files with 827 additions and 0 deletions

View file

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Types;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Slice I0 evidence harness for the four production
/// <see cref="PhysicsEngine.ResolveWithTransition"/> call shapes. This is not a
/// budget assertion: I1 deliberately changes the expected allocation from the
/// recorded graph-path baseline to zero steady transition/query-scratch bytes.
/// The harness stays in-tree so the before/after figure is reproducible.
/// </summary>
public sealed class TransitionAllocationBaselineTests
{
private const int WarmupIterations = 256;
private const int MeasuredIterations = 4_096;
private const uint CellId = 0xA9B40001u;
private readonly ITestOutputHelper _output;
public TransitionAllocationBaselineTests(ITestOutputHelper output)
=> _output = output;
[Fact]
public void GraphPath_RecordsPerResolveAllocationForEveryMoverFamily()
{
var (root, resolved) = BSPStepUpFixtures.TallWall();
var engine = BuildEngine(root, resolved);
long player = Measure(() => ResolvePlayer(engine));
long remote = Measure(() => ResolveRemote(engine));
long projectile = Measure(() => ResolveProjectile(engine));
long camera = Measure(() => ResolveCamera(engine));
_output.WriteLine(
$"Slice I0 graph-path allocation baseline ({MeasuredIterations:N0} resolves/profile):");
_output.WriteLine($" player: {player,8:N0} B/resolve");
_output.WriteLine($" remote: {remote,8:N0} B/resolve");
_output.WriteLine($" projectile: {projectile,8:N0} B/resolve");
_output.WriteLine($" camera: {camera,8:N0} B/resolve");
// The current graph path constructs a Transition object graph and
// CollisionSphere helpers on every resolve. I1 replaces that lifetime
// shape; this lower bound merely proves the baseline capture actually
// observed those allocations instead of an optimized-away call.
Assert.All(
new[] { player, remote, projectile, camera },
bytes => Assert.True(
bytes > 0,
"The I0 baseline must observe the current per-resolve allocation."));
}
private static long Measure(Func<ResolveResult> resolve)
{
ResolveResult sink = default;
for (int i = 0; i < WarmupIterations; i++)
sink = resolve();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < MeasuredIterations; i++)
sink = resolve();
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
GC.KeepAlive(sink);
return allocated / MeasuredIterations;
}
private static ResolveResult ResolvePlayer(PhysicsEngine engine)
{
var body = Bodies.Player;
ResetBody(body, PhysicsStateFlags.Gravity);
return engine.ResolveWithTransition(
currentPos: new Vector3(0.10f, 0f, 0.20f),
targetPos: new Vector3(0.36f, 0f, 0.20f),
cellId: CellId,
sphereRadius: BSPStepUpFixtures.SphereRadius,
sphereHeight: 1.20f,
stepUpHeight: 0.60f,
stepDownHeight: 1.50f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x5000000Au);
}
private static ResolveResult ResolveRemote(PhysicsEngine engine)
{
var body = Bodies.Remote;
ResetBody(body, PhysicsStateFlags.Gravity);
return engine.ResolveWithTransition(
currentPos: new Vector3(0.10f, 0f, 0.20f),
targetPos: new Vector3(0.36f, 0f, 0.20f),
cellId: CellId,
sphereRadius: BSPStepUpFixtures.SphereRadius,
sphereHeight: 1.20f,
stepUpHeight: 0.60f,
stepDownHeight: 1.50f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.EdgeSlide,
movingEntityId: 0x80000001u);
}
private static ResolveResult ResolveProjectile(PhysicsEngine engine)
{
var body = Bodies.Projectile;
ResetBody(body, PhysicsStateFlags.Missile);
return engine.ResolveWithTransition(
currentPos: new Vector3(0.10f, 0f, 2.00f),
targetPos: new Vector3(0.36f, 0f, 2.00f),
cellId: CellId,
sphereRadius: 0.05f,
sphereHeight: 0f,
stepUpHeight: 0f,
stepDownHeight: 0f,
isOnGround: false,
body: body,
movingEntityId: 0x80000002u);
}
private static ResolveResult ResolveCamera(PhysicsEngine engine)
=> engine.ResolveWithTransition(
currentPos: new Vector3(0.10f, 0f, 2.00f),
targetPos: new Vector3(0.36f, 0f, 2.00f),
cellId: CellId,
sphereRadius: 0.30f,
sphereHeight: 0f,
stepUpHeight: 0f,
stepDownHeight: 0f,
isOnGround: false,
body: null,
moverFlags: ObjectInfoState.IsViewer
| ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate
| ObjectInfoState.PerfectClip);
private static void ResetBody(PhysicsBody body, PhysicsStateFlags state)
{
body.State = state;
body.TransientState = TransientStateFlags.Active;
body.ContactPlaneValid = false;
body.WalkablePolygonValid = false;
body.WalkableVertices = null;
body.SlidingNormal = Vector3.Zero;
body.FramesStationaryFall = 0;
}
private static PhysicsEngine BuildEngine(
PhysicsBSPNode root,
Dictionary<ushort, ResolvedPolygon> resolved)
{
var heights = new byte[81];
var heightTable = new float[256];
Array.Fill(heightTable, -50f);
var engine = new PhysicsEngine();
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
const uint gfxObjId = 0x0100F100u;
var cache = new PhysicsDataCache();
cache.RegisterGfxObjForTest(gfxObjId, new GfxObjPhysics
{
BSP = new PhysicsBSPTree { Root = root },
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
Vertices = new VertexArray(),
Resolved = resolved,
BoundingSphere = new Sphere
{
Origin = new Vector3(0f, 0f, 2.5f),
Radius = 10f,
},
});
engine.DataCache = cache;
engine.ShadowObjects.Register(
entityId: gfxObjId,
gfxObjId: gfxObjId,
worldPos: Vector3.Zero,
rotation: Quaternion.Identity,
radius: 10f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B4FFFFu,
collisionType: ShadowCollisionType.BSP,
scale: 1f);
return engine;
}
private static class Bodies
{
internal static readonly PhysicsBody Player = new();
internal static readonly PhysicsBody Remote = new();
internal static readonly PhysicsBody Projectile = new();
}
}