diag(motion): instrumentation for remote walk↔run leg-cycle bug (Commit A)
Adds five diagnostics, no behavior changes. All gated on existing
ACDREAM_REMOTE_VEL_DIAG=1 env var. Plan at
~/.claude/plans/yes-make-a-plan-parsed-axolotl.md.
Five hypotheses surviving from the four-agent investigation
(docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md):
H1 SEQSTATE silently swallowed by OMEGA_DIAG sharing throttle clock
H2 ApplyServerControlledVelocityCycle races UM-driven SetCycle per UP
H3 SetCycle fast-path returns without updating _currNode
H4 GetLink/GetCycle null → defensive fallback lands on stale head
H5 PartTemplate.Count diverges from anim PartFrames.Count → silent
identity-quat freeze
Diagnostics added (all log lines are grep-prefixed):
D1 Split LastSeqStateLogTime field for SEQSTATE — own throttle.
Foundational: every other diag depends on SEQSTATE telling truth.
D2 [UPCYCLE] inside ApplyServerControlledVelocityCycle, +
[UPCYCLE_SRC] at the call site (wire vs synth velocity).
D3 [SCFAST] in fast-path return, [SCFULL] at full-rebuild end.
D4 [SCNULLFALLBACK] in the null-data defensive fallback.
D5 [PARTSDIAG] with pt.Count / seqFrames.Count / setup.Parts.Count /
anim.PartFrames[0].Frames.Count + sum-of-components hash.
Repro recipe:
$env:ACDREAM_INTERP_MANAGER = "1"
$env:ACDREAM_REMOTE_VEL_DIAG = "1"
dotnet run … 2>&1 | Tee-Object tools/diag-logs/walkrun-<ts>.log
Then watch a retail-driven character through acdream and exercise:
idle → W run → release → shift+W walk → release → demote → promote →
run+turn (this last one is the H1 trap).
Decision matrix in the plan file maps each [TAG] signature to a
specific Commit B fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7f1bd1809a
commit
23004a4791
3 changed files with 161 additions and 2 deletions
|
|
@ -377,6 +377,23 @@ public sealed class GameWindow : IDisposable
|
||||||
public double PrevServerPosTime;
|
public double PrevServerPosTime;
|
||||||
public double LastOmegaDiagLogTime;
|
public double LastOmegaDiagLogTime;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): own
|
||||||
|
/// throttle clock for the SEQSTATE log line in TickAnimations.
|
||||||
|
/// Previously SEQSTATE shared <see cref="LastOmegaDiagLogTime"/> with
|
||||||
|
/// the OMEGA_DIAG block, which fires at 0.5s and resets the clock —
|
||||||
|
/// any remote that turned during a transition silently swallowed
|
||||||
|
/// SEQSTATE for 0.5–1.5s, masking the bug we're trying to diagnose
|
||||||
|
/// (walk↔run leg-cycle sticking on observed retail chars). Split
|
||||||
|
/// 2026-05-03 (Commit A).
|
||||||
|
/// </summary>
|
||||||
|
public double LastSeqStateLogTime;
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): own
|
||||||
|
/// throttle clock for the PARTSDIAG log line in TickAnimations
|
||||||
|
/// (D5). One log per remote per ~1s.
|
||||||
|
/// </summary>
|
||||||
|
public double LastPartsDiagLogTime;
|
||||||
|
/// <summary>
|
||||||
/// Diagnostic-only: max |sequencer.CurrentVelocity| observed across
|
/// Diagnostic-only: max |sequencer.CurrentVelocity| observed across
|
||||||
/// all per-tick samples since the last UpdatePosition arrival. The
|
/// all per-tick samples since the last UpdatePosition arrival. The
|
||||||
/// next UP compares this against (LastServerPos - PrevServerPos) /
|
/// next UP compares this against (LastServerPos - PrevServerPos) /
|
||||||
|
|
@ -3295,6 +3312,23 @@ public sealed class GameWindow : IDisposable
|
||||||
uint style = ae.Sequencer.CurrentStyle != 0
|
uint style = ae.Sequencer.CurrentStyle != 0
|
||||||
? ae.Sequencer.CurrentStyle
|
? ae.Sequencer.CurrentStyle
|
||||||
: 0x8000003Du;
|
: 0x8000003Du;
|
||||||
|
|
||||||
|
// D2 (Commit A 2026-05-03): UPCYCLE diag — proves whether
|
||||||
|
// ApplyServerControlledVelocityCycle is racing UpdateMotion-driven
|
||||||
|
// SetCycle for player-driven remotes. If this fires every ~100-200ms
|
||||||
|
// during a Walk→Run press with `motion` flipping between buckets,
|
||||||
|
// H2 (UP-vs-UM race) is confirmed. UPs (5-10 Hz) would then
|
||||||
|
// perpetually overwrite the cycle the UM just set.
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||||
|
{
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[UPCYCLE] guid={serverGuid:X8} "
|
||||||
|
+ $"vel=({velocity.X:F2},{velocity.Y:F2},{velocity.Z:F2}) "
|
||||||
|
+ $"|v|={velocity.Length():F2} "
|
||||||
|
+ $"-> motion=0x{plan.Motion:X8} speedMod={plan.SpeedMod:F2} "
|
||||||
|
+ $"prev=0x{currentMotion:X8} "
|
||||||
|
+ $"airborne={rm.Airborne} moveTo={rm.ServerMoveToActive}");
|
||||||
|
}
|
||||||
ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod);
|
ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3607,6 +3641,17 @@ public sealed class GameWindow : IDisposable
|
||||||
&& rmState.HasServerVelocity
|
&& rmState.HasServerVelocity
|
||||||
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
|
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
|
||||||
{
|
{
|
||||||
|
// D2 (Commit A 2026-05-03): tag whether the velocity feeding
|
||||||
|
// ApplyServerControlledVelocityCycle is wire-explicit (rare for
|
||||||
|
// player remotes — ACE almost never sets HasVelocity on player
|
||||||
|
// UPs) or synthesized from position deltas (the common case).
|
||||||
|
// Pairs with the [UPCYCLE] line printed inside the call.
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||||
|
{
|
||||||
|
string velSrc = update.Velocity is null ? "synth" : "wire";
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[UPCYCLE_SRC] guid={update.Guid:X8} src={velSrc}");
|
||||||
|
}
|
||||||
ApplyServerControlledVelocityCycle(
|
ApplyServerControlledVelocityCycle(
|
||||||
update.Guid,
|
update.Guid,
|
||||||
aeForVelocity,
|
aeForVelocity,
|
||||||
|
|
@ -6522,12 +6567,16 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||||
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmDiag)
|
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmDiag)
|
||||||
&& nowSec - rmDiag.LastOmegaDiagLogTime > 1.0)
|
&& nowSec - rmDiag.LastSeqStateLogTime > 1.0)
|
||||||
{
|
{
|
||||||
|
// D1 (2026-05-03): SEQSTATE has its own throttle clock
|
||||||
|
// (LastSeqStateLogTime) so it isn't silently swallowed by
|
||||||
|
// OMEGA_DIAG resetting LastOmegaDiagLogTime when the
|
||||||
|
// observed remote happens to be turning.
|
||||||
System.Console.WriteLine(
|
System.Console.WriteLine(
|
||||||
$"[SEQSTATE] guid={serverGuid:X8} CurrentMotion=0x{ae.Sequencer.CurrentMotion:X8} "
|
$"[SEQSTATE] guid={serverGuid:X8} CurrentMotion=0x{ae.Sequencer.CurrentMotion:X8} "
|
||||||
+ $"CurrentSpeedMod={ae.Sequencer.CurrentSpeedMod:F3}");
|
+ $"CurrentSpeedMod={ae.Sequencer.CurrentSpeedMod:F3}");
|
||||||
rmDiag.LastOmegaDiagLogTime = nowSec;
|
rmDiag.LastSeqStateLogTime = nowSec;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seqFrames = ae.Sequencer.Advance(dt);
|
seqFrames = ae.Sequencer.Advance(dt);
|
||||||
|
|
@ -6564,6 +6613,51 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
int partCount = ae.PartTemplate.Count;
|
int partCount = ae.PartTemplate.Count;
|
||||||
|
|
||||||
|
// D5 (Commit A 2026-05-03): PARTSDIAG — proves whether
|
||||||
|
// PartTemplate.Count diverges from seqFrames.Count (silent
|
||||||
|
// identity-quat fallback freezes parts → H5) and whether the
|
||||||
|
// per-part frames returned by Advance actually change between
|
||||||
|
// Walk and Run cycles. The seqFrames hash is a sum-of-components
|
||||||
|
// proxy: cheap, unitless, monotonically distinct between cycles
|
||||||
|
// for any non-degenerate animation. If [PARTSDIAG] shows the
|
||||||
|
// hash unchanged across a Walk→Run transition while [SEQSTATE]
|
||||||
|
// shows CurrentMotion flipping, the sequencer is serving stale
|
||||||
|
// frames despite the cycle being correct.
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||||
|
&& serverGuid != 0
|
||||||
|
&& serverGuid != _playerServerGuid
|
||||||
|
&& _remoteDeadReckon.TryGetValue(serverGuid, out var rmParts))
|
||||||
|
{
|
||||||
|
double nowSecParts = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||||
|
if (nowSecParts - rmParts.LastPartsDiagLogTime > 1.0)
|
||||||
|
{
|
||||||
|
int seqCount = seqFrames?.Count ?? -1;
|
||||||
|
int setupParts = ae.Setup.Parts.Count;
|
||||||
|
int animFrame0Parts = ae.Animation.PartFrames.Count > 0
|
||||||
|
? ae.Animation.PartFrames[0].Frames.Count
|
||||||
|
: -1;
|
||||||
|
double seqHash = 0.0;
|
||||||
|
if (seqFrames is not null)
|
||||||
|
{
|
||||||
|
for (int hi = 0; hi < seqFrames.Count; hi++)
|
||||||
|
{
|
||||||
|
var f = seqFrames[hi];
|
||||||
|
seqHash += f.Origin.X + f.Origin.Y + f.Origin.Z
|
||||||
|
+ f.Orientation.X + f.Orientation.Y
|
||||||
|
+ f.Orientation.Z + f.Orientation.W;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[PARTSDIAG] guid={serverGuid:X8} "
|
||||||
|
+ $"pt.Count={partCount} seqFrames.Count={seqCount} "
|
||||||
|
+ $"setup.Parts.Count={setupParts} "
|
||||||
|
+ $"anim.PartFrames[0].Frames.Count={animFrame0Parts} "
|
||||||
|
+ $"seqHash={seqHash:F4}");
|
||||||
|
rmParts.LastPartsDiagLogTime = nowSecParts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var newMeshRefs = new List<AcDream.Core.World.MeshRef>(partCount);
|
var newMeshRefs = new List<AcDream.Core.World.MeshRef>(partCount);
|
||||||
var scaleMat = ae.Scale == 1.0f
|
var scaleMat = ae.Scale == 1.0f
|
||||||
? System.Numerics.Matrix4x4.Identity
|
? System.Numerics.Matrix4x4.Identity
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,15 @@ public sealed class AnimationSequencer
|
||||||
private const double FrameEpsilon = 1e-5;
|
private const double FrameEpsilon = 1e-5;
|
||||||
private const double RateEpsilon = 1e-6;
|
private const double RateEpsilon = 1e-6;
|
||||||
|
|
||||||
|
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
||||||
|
// Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines
|
||||||
|
// emitted from SetCycle. Gated on env var ACDREAM_REMOTE_VEL_DIAG=1; reads
|
||||||
|
// the env var inline rather than caching so a launch can be re-toggled
|
||||||
|
// without restarting. Throttled to one log per ~0.5s per sequencer
|
||||||
|
// instance; the log line itself carries the motion ID so multiple cycles
|
||||||
|
// alternating fast still get distinguished in the log.
|
||||||
|
private double _lastSetCycleDiagTime;
|
||||||
|
|
||||||
// ── Constructor ──────────────────────────────────────────────────────────
|
// ── Constructor ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -406,6 +415,25 @@ public sealed class AnimationSequencer
|
||||||
MultiplyCyclicFramerate(speedMod / CurrentSpeedMod);
|
MultiplyCyclicFramerate(speedMod / CurrentSpeedMod);
|
||||||
CurrentSpeedMod = speedMod;
|
CurrentSpeedMod = speedMod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D3 (Commit A 2026-05-03): SCFAST — proves whether the fast-path
|
||||||
|
// is firing instead of the full rebuild. If [SCFAST] dominates
|
||||||
|
// during a Walk→Run press AND the leg-cycle hash from D5 doesn't
|
||||||
|
// change, H3 (fast-path leaves _currNode on a stale link) is the
|
||||||
|
// bug.
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||||
|
{
|
||||||
|
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||||
|
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||||
|
{
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[SCFAST] motion=0x{motion:X8} speedMod={speedMod:F3} "
|
||||||
|
+ $"oldSpeedMod={CurrentSpeedMod:F3} "
|
||||||
|
+ $"qCount={_queue.Count} "
|
||||||
|
+ $"currNodeIsCyclic={(_currNode == _firstCyclic)}");
|
||||||
|
_lastSetCycleDiagTime = nowSec;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,6 +538,42 @@ public sealed class AnimationSequencer
|
||||||
// Defensive fallback: nothing newly added AND no current node.
|
// Defensive fallback: nothing newly added AND no current node.
|
||||||
_currNode = _queue.First;
|
_currNode = _queue.First;
|
||||||
_framePosition = _currNode?.Value.GetStartFramePosition() ?? 0.0;
|
_framePosition = _currNode?.Value.GetStartFramePosition() ?? 0.0;
|
||||||
|
|
||||||
|
// D4 (Commit A 2026-05-03): SCNULLFALLBACK — proves whether the
|
||||||
|
// null-data fallback is being hit. If this fires during a
|
||||||
|
// Walk→Run transition for the watched remote, H4 (MotionTable
|
||||||
|
// GetLink/GetCycle returns null for the remote's setup) is the
|
||||||
|
// bug. linkData/cycleData null almost certainly means a
|
||||||
|
// MotionTable lookup gap for that style+motion combo.
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||||
|
{
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[SCNULLFALLBACK] motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||||
|
+ $"linkNull={(linkData is null)} cycleNull={(cycleData is null)} "
|
||||||
|
+ $"qCount={_queue.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3 (Commit A 2026-05-03): SCFULL — counterpart to SCFAST. Fires on
|
||||||
|
// the full-rebuild SetCycle path. Together they tell us the
|
||||||
|
// fast-path-vs-rebuild ratio. If the visible cycle is wrong despite
|
||||||
|
// SCFULL firing with a non-null _currNode and firstNew set, the bug
|
||||||
|
// is downstream (H5 PartTemplate divergence, or sequencer-internal
|
||||||
|
// serving stale frames despite correct CurrentMotion).
|
||||||
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||||
|
{
|
||||||
|
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||||
|
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||||
|
{
|
||||||
|
System.Console.WriteLine(
|
||||||
|
$"[SCFULL] motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||||
|
+ $"speedMod={speedMod:F3} "
|
||||||
|
+ $"qCount={_queue.Count} "
|
||||||
|
+ $"firstNewNull={(firstNew is null)} "
|
||||||
|
+ $"currNodeIsCyclic={(_currNode == _firstCyclic)} "
|
||||||
|
+ $"firstCyclicNull={(_firstCyclic is null)}");
|
||||||
|
_lastSetCycleDiagTime = nowSec;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentStyle = style;
|
CurrentStyle = style;
|
||||||
|
|
|
||||||
1
tools/diag-logs/.gitignore
vendored
Normal file
1
tools/diag-logs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.log
|
||||||
Loading…
Add table
Add a link
Reference in a new issue