probe(physics): ACDREAM_DUMP_TRANSIT_FAIL — self-selecting transition-phase trace for #345
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

Fires only on the stuck-tick predicate (>=1mm XY requested, <=0.1mm
achieved), buffering per-tick phase outcomes cheaply and flushing only
on a stuck tick: per-insert-attempt phase/state/normal/source, step-up
enter/exit verdicts, every ValidateWalkable branch with dist/waterDepth
and both SetCollisionNormal guards evaluated, and the tick's final
AdjustOffset pair. Zero cost when off (flag before any allocation — the
I1 zero-alloc gate stays green), mover id on every line, [ThreadStatic]
buffer per the referee-safety rule. Two tests: fires on a synthetic
wall-stuck tick, silent on ordinary movement.

Diagnostics only; no behavioral change. Suite 11,271 / 6 / 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-07 10:48:46 +02:00
parent 6c6664a685
commit e761761aa3
4 changed files with 622 additions and 3 deletions

View file

@ -2775,6 +2775,310 @@ public static class PhysicsDiagnostics
_cylToiUnverifiedAnnounced = 0;
}
// ── [transit-fail-*] — #345 stuck-tick transition-phase trace ─────────
//
// docs/research/2026-08-08-345-mechanism-contract.md "probe" section:
// walking angled into a too-steep slope eats 100% of the requested XY
// displacement while the resolve reports collN=(0,0,1) — straight up —
// with the output position byte-identical to the input. Naming WHICH
// mechanism writes that UP normal on a completely stalled tick needs a
// per-tick TRANSITION PHASE TRACE, but printing one on every one of the
// ~30 resolves/sec a normal walk takes would drown the signal. So the
// trace is captured into a cheap per-thread buffer during the tick and
// only flushed to the console when ResolveWithTransition's own tail
// confirms the tick requested a nonzero XY move and delivered zero (the
// self-selecting stuck-tick predicate — a healthy session prints
// nothing at all).
//
// Four capture families, one flush:
// [transit-fail-insert] — one line per FindPrimaryCellCollisions call
// (TransitionalInsert's per-attempt env->building->objects
// channel), naming which phase halted the attempt and, on Collided,
// the colliding polygon's normal and which of the three channels
// wrote it.
// [transit-fail-stepup] — DoStepUp entry/exit, mirroring the existing
// ACDREAM_DUMP_STEPUP probe's own input-normal-and-verdict content
// so a stuck-tick capture carries the step chain without running a
// second flag.
// [transit-fail-walk] — every ValidateWalkable outcome: the signed
// distance, waterDepth, which of the four code branches fired, and
// the two flag guards (oi.Contact, sp.StepDown) gating its
// SetCollisionNormal call — the collN=(0,0,1) fingerprint most
// plausibly comes from exactly this site.
// [transit-fail-adjust] — overwritten on every AdjustOffset call, so
// only the LAST one before the tick ends survives to the flush:
// the final per-tick input/output offset pair.
//
// A stuck tick's flush order is: the [transit-fail] summary line
// itself, then every buffered [transit-fail-insert]/
// [transit-fail-stepup]/[transit-fail-walk] line in call order, then
// the one [transit-fail-adjust] line.
//
// All four families are diagnostics-only: nothing here reads back into
// any production decision. [ThreadStatic] because a headless host ticks
// several sessions in parallel and physics is synchronous within each.
// Every capture method tests DumpTransitFailEnabled BEFORE touching any
// buffer, building any string, or reading collision state beyond the
// value-type arguments the caller already had in hand — zero allocation
// when off (Slice I1's TransitionAllocationBaselineTests gate).
// TEMPORARY — strip with the rest of the #345 investigation once the
// mechanism is identified and the fix contract is written.
/// <summary>
/// Initial state from <c>ACDREAM_DUMP_TRANSIT_FAIL=1</c>.
/// </summary>
public static bool DumpTransitFailEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_DUMP_TRANSIT_FAIL") == "1";
/// <summary>
/// The stuck-tick predicate's "the request was real" threshold, metres
/// squared. 1 mm: comfortably under any legitimate per-tick movement
/// request (the #345 capture's stuck ticks requested ~0.23 m), well over
/// float roundoff.
/// </summary>
private const float TransitFailNonzeroRequestXYSq = 0.001f * 0.001f;
/// <summary>
/// The stuck-tick predicate's "the result was zero" threshold, metres
/// squared. 0.1 mm: the #345 capture's stuck ticks returned a position
/// byte-identical to the input, so this only needs to be tight enough to
/// reject genuine (if small) movement.
/// </summary>
private const float TransitFailZeroXYSq = 0.0001f * 0.0001f;
[ThreadStatic] private static List<string>? _transitFailBuffer;
[ThreadStatic] private static string? _transitFailAdjustLine;
/// <summary>
/// Reset the per-tick buffer. Call once per
/// <see cref="PhysicsEngine.ResolveWithTransition"/> call, before
/// <c>FindTransitionalPosition</c> runs, so a tick that captures nothing
/// does not carry over the previous tick's lines. No-op unless
/// <see cref="DumpTransitFailEnabled"/>.
/// </summary>
public static void BeginTransitFailTrace()
{
if (!DumpTransitFailEnabled) return;
(_transitFailBuffer ??= new List<string>()).Clear();
_transitFailAdjustLine = null;
}
/// <summary>
/// One line per <c>FindPrimaryCellCollisions</c> call — the
/// env-&gt;building-&gt;objects composition inside one
/// <c>TransitionalInsert</c> attempt. <paramref name="phase"/> is
/// <c>"environment"</c>, <c>"building"</c>, or <c>"objects"</c> —
/// whichever channel produced <paramref name="outcome"/> (env/building
/// short-circuit the other two on a non-OK result, matching production
/// control flow, so the skipped channels' states arrive as
/// <see langword="null"/>). On a <see cref="TransitionState.Collided"/>
/// outcome, also names the colliding polygon's plane normal and which
/// of the three channels wrote it (<paramref name="collidedObjectGuid"/>
/// is non-null only for the <c>objects</c> phase).
/// </summary>
public static void TraceTransitInsertAttempt(
uint moverId,
int attempt,
string phase,
TransitionState envState,
TransitionState? buildingState,
TransitionState? objectsState,
TransitionState outcome,
Vector3 collisionNormal,
uint? collidedObjectGuid)
{
if (!DumpTransitFailEnabled) return;
var ci = System.Globalization.CultureInfo.InvariantCulture;
string buildingText = buildingState is { } b ? b.ToString() : "n/a";
string objectsText = objectsState is { } o ? o.ToString() : "n/a";
string collidedText = outcome == TransitionState.Collided
? string.Format(ci,
" collN=({0:F3},{1:F3},{2:F3}) src={3}",
collisionNormal.X, collisionNormal.Y, collisionNormal.Z,
phase == "objects"
? (collidedObjectGuid is { } guid
? string.Format(ci, "object:0x{0:X8}", guid)
: "object:none")
: phase)
: "";
(_transitFailBuffer ??= new List<string>()).Add(string.Format(ci,
"[transit-fail-insert] mover=0x{0:X8} attempt={1} phase={2} " +
"env={3} building={4} objects={5} outcome={6}{7}",
moverId, attempt, phase, envState, buildingText, objectsText,
outcome, collidedText));
}
/// <summary>
/// One line per <c>Transition.DoStepUp</c> entry or exit, mirroring the
/// content of the existing <c>ACDREAM_DUMP_STEPUP</c> probe (same input
/// normal / walkable verdict / landing-plane fields) so a stuck-tick
/// capture carries the step-up chain without a second flag.
/// <paramref name="edge"/> is <c>"enter"</c> or <c>"exit"</c>;
/// <paramref name="succeeded"/> and <paramref name="landedNormal"/> are
/// only meaningful on <c>"exit"</c>.
/// </summary>
public static void TraceTransitStepUp(
uint moverId,
string edge,
Vector3 inputNormal,
bool onWalkable,
float stepUpHeight,
Vector3 pos,
bool? succeeded,
Vector3? landedNormal)
{
if (!DumpTransitFailEnabled) return;
var ci = System.Globalization.CultureInfo.InvariantCulture;
float floor = PhysicsGlobals.FloorZ;
string verdict = inputNormal.Z >= floor ? "WALKABLE" : "STEEP";
string outcomeText;
if (succeeded is null)
{
outcomeText = "";
}
else if (succeeded.Value && landedNormal is { } landed)
{
string landedVerdict = landed.Z >= floor ? "WALKABLE" : "STEEP";
outcomeText = string.Format(ci,
" outcome=SUCCESS landedN=({0:F3},{1:F3},{2:F3})->{3}",
landed.X, landed.Y, landed.Z, landedVerdict);
}
else
{
outcomeText = " outcome=FAILED";
}
(_transitFailBuffer ??= new List<string>()).Add(string.Format(ci,
"[transit-fail-stepup] mover=0x{0:X8} edge={1} " +
"n=({2:F3},{3:F3},{4:F3})->{5} onWalkable={6} stepUpHeight={7:F3} " +
"pos=({8:F2},{9:F2},{10:F2}){11}",
moverId, edge,
inputNormal.X, inputNormal.Y, inputNormal.Z, verdict,
onWalkable, stepUpHeight,
pos.X, pos.Y, pos.Z, outcomeText));
}
/// <summary>
/// One line per <c>ValidateWalkable</c> outcome. <paramref name="branch"/>
/// is <c>"above"</c> (comfortably clear of the surface, no state
/// change), <c>"resting"</c> (within EPSILON of the surface),
/// <c>"below-push"</c> (below the surface — covers both the ordinary
/// push and the step-down interpolation reject; <paramref
/// name="outcome"/> distinguishes them), or <c>"checkwalkable-fail"</c>
/// (a walkable probe below the surface, which always fails closed).
/// <paramref name="guardPassed"/> is <see langword="null"/> for branches
/// that never reach the <c>SetCollisionNormal</c> site
/// (<c>"above"</c>/<c>"checkwalkable-fail"</c>/the interpolation
/// reject); otherwise it is the evaluated
/// <c>!oi.Contact &amp;&amp; !sp.StepDown</c> guard, and <paramref
/// name="normal"/> is the plane normal that would have been (or was)
/// written.
/// </summary>
public static void TraceTransitValidateWalkable(
uint moverId,
string branch,
float dist,
float waterDepth,
bool oiContact,
bool spStepDown,
bool? guardPassed,
Vector3 normal,
TransitionState outcome)
{
if (!DumpTransitFailEnabled) return;
var ci = System.Globalization.CultureInfo.InvariantCulture;
string guardText = guardPassed is { } g ? g.ToString() : "n/a";
(_transitFailBuffer ??= new List<string>()).Add(string.Format(ci,
"[transit-fail-walk] mover=0x{0:X8} branch={1} dist={2:F5} " +
"waterDepth={3:F4} oiContact={4} spStepDown={5} guardPassed={6} " +
"normal=({7:F3},{8:F3},{9:F3}) outcome={10}",
moverId, branch, dist, waterDepth, oiContact, spStepDown,
guardText, normal.X, normal.Y, normal.Z, outcome));
}
/// <summary>
/// Overwritten on every <c>AdjustOffset</c> call within the tick, so
/// only the LAST call before the tick ends survives to the flush — the
/// final per-tick input/output offset pair the mechanism contract asks
/// for.
/// </summary>
public static void TraceTransitAdjustOffset(
uint moverId, string branch, Vector3 offsetIn, Vector3 offsetOut)
{
if (!DumpTransitFailEnabled) return;
var ci = System.Globalization.CultureInfo.InvariantCulture;
_transitFailAdjustLine = string.Format(ci,
"[transit-fail-adjust] mover=0x{0:X8} branch={1} " +
"in=({2:F4},{3:F4},{4:F4}) out=({5:F4},{6:F4},{7:F4})",
moverId, branch,
offsetIn.X, offsetIn.Y, offsetIn.Z,
offsetOut.X, offsetOut.Y, offsetOut.Z);
}
/// <summary>
/// The stuck-tick predicate, evaluated at
/// <see cref="PhysicsEngine.ResolveWithTransition"/>'s tail where both
/// the requested and the actual XY displacement are known. Fires
/// (flushes the buffered trace to the console) only when the tick
/// requested a real XY move (&gt; <see cref="TransitFailNonzeroRequestXYSq"/>)
/// and delivered essentially none (&lt;
/// <see cref="TransitFailZeroXYSq"/>) — the #345 fingerprint. Always
/// clears the buffer afterward, whether it fired or not, so a healthy
/// tick never leaks into the next one. No-op unless
/// <see cref="DumpTransitFailEnabled"/>.
/// </summary>
public static void EmitTransitFailIfStuck(
uint moverId, Vector3 currentPos, Vector3 targetPos, Vector3 resultPos)
{
if (!DumpTransitFailEnabled) return;
float reqX = targetPos.X - currentPos.X;
float reqY = targetPos.Y - currentPos.Y;
float reqXYSq = reqX * reqX + reqY * reqY;
float actX = resultPos.X - currentPos.X;
float actY = resultPos.Y - currentPos.Y;
float actXYSq = actX * actX + actY * actY;
bool stuck = reqXYSq >= TransitFailNonzeroRequestXYSq
&& actXYSq <= TransitFailZeroXYSq;
if (stuck)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
int lineCount = (_transitFailBuffer?.Count ?? 0)
+ (_transitFailAdjustLine is null ? 0 : 1);
Console.WriteLine(string.Format(ci,
"[transit-fail] mover=0x{0:X8} STUCK-TICK " +
"reqXY=({1:F4},{2:F4}) reqLen={3:F4} " +
"actXY=({4:F4},{5:F4}) actLen={6:F4} " +
"in=({7:F3},{8:F3},{9:F3}) tgt=({10:F3},{11:F3},{12:F3}) " +
"out=({13:F3},{14:F3},{15:F3}) lines={16}",
moverId, reqX, reqY, MathF.Sqrt(reqXYSq),
actX, actY, MathF.Sqrt(actXYSq),
currentPos.X, currentPos.Y, currentPos.Z,
targetPos.X, targetPos.Y, targetPos.Z,
resultPos.X, resultPos.Y, resultPos.Z,
lineCount));
if (_transitFailBuffer is { Count: > 0 } buffer)
{
foreach (string line in buffer)
Console.WriteLine(line);
}
if (_transitFailAdjustLine is not null)
Console.WriteLine(_transitFailAdjustLine);
}
_transitFailBuffer?.Clear();
_transitFailAdjustLine = null;
}
private static int ParsePositiveInt(string? value) =>
int.TryParse(
value,

View file

@ -1922,6 +1922,12 @@ public sealed class PhysicsEngine
// sentinel value for that case. No-op when the probe is off.
PhysicsDiagnostics.BeginContactPlaneAttribution();
// #345 probe (2026-08-08): reset the per-tick transition-phase trace
// buffer. Scoped to exactly one resolve/tick — see
// PhysicsDiagnostics.BeginTransitFailTrace. No-op when the probe is
// off.
PhysicsDiagnostics.BeginTransitFailTrace();
var transition = RentTransition();
try
{
@ -2470,6 +2476,13 @@ public sealed class PhysicsEngine
Orientation: sp.CurOrientation); // Render Residual A — the sweep failed (find_valid_position == 0)
}
// #345 probe (2026-08-08): the self-selecting stuck-tick
// predicate — fires only when this tick requested a real XY
// move and delivered none, flushing the buffered
// transition-phase trace. No-op when the probe is off.
PhysicsDiagnostics.EmitTransitFailIfStuck(
movingEntityId, currentPos, targetPos, resolveResult.Position);
// A6.P3 #98 capture: emit one JSON Lines record per player call,
// with bodyBefore snapshot (taken at method entry, before any
// engine mutation) + bodyAfter snapshot (taken now, after the

View file

@ -2460,7 +2460,16 @@ public sealed class Transition
cellId,
FindEnvCollisions(engine, cellId));
if (environment != TransitionState.OK)
{
// #345 probe (2026-08-08): buffered into PhysicsDiagnostics'
// stuck-tick trace; self-guards internally, zero cost when
// ACDREAM_DUMP_TRANSIT_FAIL is unset.
PhysicsDiagnostics.TraceTransitInsertAttempt(
ObjectInfo.SelfEntityId, innerAttempt, "environment",
environment, null, null, environment,
CollisionInfo.CollisionNormal, CollisionInfo.LastCollidedObjectGuid);
return environment;
}
TransitionState building = ObservePrimaryCellPhase(
engine,
@ -2468,7 +2477,13 @@ public sealed class Transition
cellId,
FindBuildingCollisions(engine, cellId));
if (building != TransitionState.OK)
{
PhysicsDiagnostics.TraceTransitInsertAttempt(
ObjectInfo.SelfEntityId, innerAttempt, "building",
environment, building, null, building,
CollisionInfo.CollisionNormal, CollisionInfo.LastCollidedObjectGuid);
return building;
}
TransitionState objects = ObservePrimaryCellPhase(
engine,
@ -2476,6 +2491,10 @@ public sealed class Transition
cellId,
FindObjCollisionsInCell(engine, cellId));
DumpPhase2(innerAttempt, environment, objects);
PhysicsDiagnostics.TraceTransitInsertAttempt(
ObjectInfo.SelfEntityId, innerAttempt, "objects",
environment, building, objects, objects,
CollisionInfo.CollisionNormal, CollisionInfo.LastCollidedObjectGuid);
return objects;
}
@ -3685,17 +3704,40 @@ public sealed class Transition
CacheWalkableContext(sp, contactPlane, walkableVertices);
}
if (!oi.Contact && !sp.StepDown)
// #345 probe (2026-08-08): named local so the trace call
// below can report exactly what the production guard
// evaluated to, without changing the guard itself.
bool restingGuardPassed = !oi.Contact && !sp.StepDown;
if (restingGuardPassed)
{
ci.SetCollisionNormal(contactPlane.Normal);
ci.CollidedWithEnvironment = true;
}
PhysicsDiagnostics.TraceTransitValidateWalkable(
oi.SelfEntityId, "resting", dist, waterDepth,
oi.Contact, sp.StepDown, restingGuardPassed,
contactPlane.Normal, TransitionState.OK);
}
else
{
PhysicsDiagnostics.TraceTransitValidateWalkable(
oi.SelfEntityId, "above", dist, waterDepth,
oi.Contact, sp.StepDown, guardPassed: null,
contactPlane.Normal, TransitionState.OK);
}
return TransitionState.OK;
}
// ── Below the surface ─────────────────────────────────────────────
if (sp.CheckWalkable) return TransitionState.Collided; // walkable probe fails
if (sp.CheckWalkable)
{
PhysicsDiagnostics.TraceTransitValidateWalkable(
oi.SelfEntityId, "checkwalkable-fail", dist, waterDepth,
oi.Contact, sp.StepDown, guardPassed: null,
contactPlane.Normal, TransitionState.Collided);
return TransitionState.Collided; // walkable probe fails
}
// zDist: how far we need to push up along Z to clear the surface.
// contactPlane.Normal.Z is 1 for flat ground, so this is just dist.
@ -3712,7 +3754,13 @@ public sealed class Transition
// Validate step-down interpolation factor.
float interp = (1f - (-1f / (sp.StepDownAmt * sp.WalkInterp)) * zDist) * sp.WalkInterp;
if (interp >= sp.WalkInterp || interp < -0.1f)
{
PhysicsDiagnostics.TraceTransitValidateWalkable(
oi.SelfEntityId, "below-push", dist, waterDepth,
oi.Contact, sp.StepDown, guardPassed: null,
contactPlane.Normal, TransitionState.Collided);
return TransitionState.Collided;
}
sp.WalkInterp = interp;
}
@ -3720,12 +3768,20 @@ public sealed class Transition
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, -zDist));
}
if (!oi.Contact && !sp.StepDown)
// #345 probe (2026-08-08): named local, same rationale as
// restingGuardPassed above.
bool belowPushGuardPassed = !oi.Contact && !sp.StepDown;
if (belowPushGuardPassed)
{
ci.SetCollisionNormal(contactPlane.Normal);
ci.CollidedWithEnvironment = true;
}
PhysicsDiagnostics.TraceTransitValidateWalkable(
oi.SelfEntityId, "below-push", dist, waterDepth,
oi.Contact, sp.StepDown, belowPushGuardPassed,
contactPlane.Normal, TransitionState.Adjusted);
return TransitionState.Adjusted;
}
@ -5563,6 +5619,12 @@ public sealed class Transition
collisionAngle: 0f,
walkInterp: sp.WalkInterp);
// #345 probe (2026-08-08): overwrites the buffered "final
// AdjustOffset" slot every call; only the last write before the
// tick ends survives to the stuck-tick flush.
PhysicsDiagnostics.TraceTransitAdjustOffset(
ObjectInfo.SelfEntityId, branch, offset, result);
return result;
}
@ -5685,6 +5747,11 @@ public sealed class Transition
collisionAngle: collisionAngle,
walkInterp: sp.WalkInterp);
// #345 probe (2026-08-08): same overwrite as the no-contact-plane
// early return above.
PhysicsDiagnostics.TraceTransitAdjustOffset(
ObjectInfo.SelfEntityId, branch, offset, result);
return result;
}
@ -5912,6 +5979,18 @@ public sealed class Transition
$"CurPos=({sp.CurPos.X:F2},{sp.CurPos.Y:F2},{sp.CurPos.Z:F2})");
}
// #345 probe (2026-08-08): mirrors the ACDREAM_DUMP_STEPUP content
// above into the buffered stuck-tick trace (self-guards internally,
// zero cost when ACDREAM_DUMP_TRANSIT_FAIL is unset) so a stuck-tick
// capture carries the step-up chain without a second flag.
PhysicsDiagnostics.TraceTransitStepUp(
oi.SelfEntityId, "enter", collisionNormal,
onWalkable: (oi.State & ObjectInfoState.OnWalkable) != 0,
stepUpHeight: oi.StepUpHeight,
pos: sp.CurPos,
succeeded: null,
landedNormal: null);
// L.2.3c (2026-04-29): capture the existing contact plane BEFORE
// clearing it. On step-up failure (too-tall wall) we restore it so
// the mover stays grounded — without this, walking into a wall
@ -5971,6 +6050,18 @@ public sealed class Transition
}
}
// #345 probe (2026-08-08): the matching exit line for the "enter"
// trace above.
PhysicsDiagnostics.TraceTransitStepUp(
oi.SelfEntityId, "exit", collisionNormal,
onWalkable: (oi.State & ObjectInfoState.OnWalkable) != 0,
stepUpHeight: oi.StepUpHeight,
pos: sp.CheckPos,
succeeded: stepDown,
landedNormal: stepDown && ci.ContactPlaneValid
? ci.ContactPlane.Normal
: null);
if (!stepDown)
{
sp.RestoreCheckPos();