perf(physics): reuse reset-complete transition scratch

Mirror retail's ten-deep LIFO transition lifetime, retain all query scratch with complete reset contracts, and remove Tier-0 enum boxing without changing collision decisions. Fresh and retained engines are bit-identical across the expanded oracle, while measured transition profiles now allocate 0 bytes per resolve.
This commit is contained in:
Erik 2026-07-25 14:37:02 +02:00
parent 2effba5127
commit 16d182c2f0
19 changed files with 3037 additions and 1228 deletions

View file

@ -9,11 +9,11 @@ using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Slice I0 evidence harness for the four production
/// Slice I allocation gate 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.
/// broad frame-allocation budget: it isolates transition/query scratch and
/// requires zero steady bytes after all retained buffers and tiered code paths
/// have warmed.
/// </summary>
public sealed class TransitionAllocationBaselineTests
{
@ -27,32 +27,33 @@ public sealed class TransitionAllocationBaselineTests
=> _output = output;
[Fact]
public void GraphPath_RecordsPerResolveAllocationForEveryMoverFamily()
public void ReusedScratch_AllocatesZeroBytesPerResolveForEveryMoverFamily()
{
var (root, resolved) = BSPStepUpFixtures.TallWall();
var engine = BuildEngine(root, resolved);
var groundEngine = BuildGroundEngine();
long player = Measure(() => ResolvePlayer(engine));
long groundedPublication = Measure(
() => ResolveGroundedPublication(groundEngine));
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):");
$"Slice I1 steady scratch gate ({MeasuredIterations:N0} resolves/profile):");
_output.WriteLine($" player: {player,8:N0} B/resolve");
_output.WriteLine(
$" grounded: {groundedPublication,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."));
Assert.Equal(0, player);
Assert.Equal(0, groundedPublication);
Assert.Equal(0, remote);
Assert.Equal(0, projectile);
Assert.Equal(0, camera);
}
private static long Measure(Func<ResolveResult> resolve)
@ -106,6 +107,24 @@ public sealed class TransitionAllocationBaselineTests
movingEntityId: 0x80000001u);
}
private static ResolveResult ResolveGroundedPublication(PhysicsEngine engine)
{
var body = Bodies.Grounded;
ResetBody(body, PhysicsStateFlags.Gravity);
return engine.ResolveWithTransition(
currentPos: new Vector3(8f, 8f, 0f),
targetPos: new Vector3(8.12f, 8f, 0f),
cellId: CellId,
sphereRadius: BSPStepUpFixtures.SphereRadius,
sphereHeight: 1.20f,
stepUpHeight: 0.40f,
stepDownHeight: 1.50f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x5000000Bu);
}
private static ResolveResult ResolveProjectile(PhysicsEngine engine)
{
var body = Bodies.Projectile;
@ -197,9 +216,25 @@ public sealed class TransitionAllocationBaselineTests
return engine;
}
private static PhysicsEngine BuildGroundEngine()
{
var heights = new byte[81];
var heightTable = new float[256];
var engine = new PhysicsEngine();
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
return engine;
}
private static class Bodies
{
internal static readonly PhysicsBody Player = new();
internal static readonly PhysicsBody Grounded = new();
internal static readonly PhysicsBody Remote = new();
internal static readonly PhysicsBody Projectile = new();
}