From 1ace2e3cb45f8d888c3326f81462a8b7cdc559ee Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 09:56:55 -0500 Subject: [PATCH] Gyro: BT_GYRO_TRACE per-frame integrator trace -- oscillation verified vs authored constants (task #56) 25-hit MP autofire trace: kick -> damped oscillation -> settle, peaks well inside clamps, no NaN/drift; measured Y:X frequency ratio 7.75x matches sqrt(springK.y/springK.x) exactly. Co-Authored-By: Claude Fable 5 --- context/cockpit-view.md | 6 ++++ game/reconstructed/gyro.cpp | 27 ++++++++++++++++++ scratchpad/gtrace_plot.py | 56 +++++++++++++++++++++++++++++++++++++ scratchpad/mp_gtrace.sh | 8 ++++++ 4 files changed, 97 insertions(+) create mode 100644 scratchpad/gtrace_plot.py create mode 100644 scratchpad/mp_gtrace.sh diff --git a/context/cockpit-view.md b/context/cockpit-view.md index 4718caa..1b24b6c 100644 --- a/context/cockpit-view.md +++ b/context/cockpit-view.md @@ -118,6 +118,12 @@ inverse(eyeWorld)`. No LookAt anywhere. (projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,−1.5) × damage/16, @4bc136-4bc19c). Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed. + **Quantitatively verified (2026-07-11 MP trace):** `BT_GYRO_TRACE=1` logs the integrator state + every displaced frame (`[gtrace]`); a 25-hit autofire session (scratchpad/GYRO_TRACE.png) + shows per-hit kick → damped oscillation → settle-to-zero-and-stay, peaks ±0.028u X/Z ±0.005u Y + (well inside the ±0.1/0.15 clamps), body pitch/roll ±0.02 rad, zero NaN/drift — and the + measured frequency ratio matches the authored constants: Y period 9.5 frames vs X/Z ~75 ≈ + √(springK.y/springK.x)=√60≈7.75× as dictated by springK=(−0.2,−12,−0.2). [T2] - **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE (@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index a6fec11..df238fd 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -643,6 +643,27 @@ void DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << "," << (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush; } + // BOUNCE TRACE (task #56 verification): BT_GYRO_TRACE=1 logs this gyro's + // integrated state EVERY frame while displaced, uncapped, tagged with the + // instance + a running frame index -- enough to PLOT the oscillation and + // check the damped-spring behaviour against the authored constants. + static const int s_gtrace = getenv("BT_GYRO_TRACE") ? 1 : 0; + if (s_gtrace) + { + static int s_traceFrame = 0; + ++s_traceFrame; + const float m2 = eyePosition.x*eyePosition.x + eyePosition.y*eyePosition.y + + eyePosition.z*eyePosition.z + + bodyOrientation.x*bodyOrientation.x + + bodyOrientation.y*bodyOrientation.y + + bodyOrientation.z*bodyOrientation.z; + if (m2 > 1e-9f) + DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame + << " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y + << " " << (float)eyePosition.z + << " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y + << " " << (float)bodyOrientation.z << "\n" << std::flush; + } if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0 { SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION @@ -844,6 +865,12 @@ void << ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/" << (float)yaw << "/" << (float)vibration << "\n" << std::flush; + if (getenv("BT_GYRO_TRACE")) + DEBUG_STREAM << "[gtrace] g=" << (void *)this << " HIT t=" << (float)trans + << " p=" << (float)pitchRoll << " y=" << (float)yaw << " v=" << (float)vibration + << " dir=" << (float)dir.x << " " << (float)dir.y << " " << (float)dir.z + << "\n" << std::flush; + ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00 the directional knock ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23 ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y, diff --git a/scratchpad/gtrace_plot.py b/scratchpad/gtrace_plot.py new file mode 100644 index 0000000..d2c5974 --- /dev/null +++ b/scratchpad/gtrace_plot.py @@ -0,0 +1,56 @@ +import re, sys +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +frames, eyes, bodies = [], [], [] +hits = [] # (frame_at_hit, t, p, y, v) +last_f = None +for line in open("../content/gtrace_a.log", encoding="utf-8", errors="replace"): + m = re.match(r"\[gtrace\] g=\S+ f=(\d+) eye=(\S+) (\S+) (\S+) body=(\S+) (\S+) (\S+)", line) + if m: + last_f = int(m.group(1)) + frames.append(last_f) + eyes.append(tuple(float(m.group(i)) for i in (2,3,4))) + bodies.append(tuple(float(m.group(i)) for i in (5,6,7))) + continue + m = re.match(r"\[gtrace\] g=\S+ HIT t=(\S+) p=(\S+) y=(\S+) v=(\S+)", line) + if m: + hits.append((last_f, *[float(m.group(i)) for i in (1,2,3,4)])) + +print(f"trace frames: {len(frames)}, hits: {len(hits)}") +print(f"frame range: {frames[0]}..{frames[-1]}") + +# stats per volley: peak |eye|, settle +import math +mag = [math.sqrt(e[0]**2+e[1]**2+e[2]**2) for e in eyes] +peak = max(mag); print(f"peak |eyePos| = {peak:.4f} u (clamp legal range +-0.1/0.15 per axis)") +peakY = max(abs(e[1]) for e in eyes); peakX = max(abs(e[0]) for e in eyes); peakZ = max(abs(e[2]) for e in eyes) +print(f"peak per-axis |x|={peakX:.4f} |y|={peakY:.4f} |z|={peakZ:.4f}") +bad = [e for e in eyes if any(x != x for x in e)] +print(f"NaN frames: {len(bad)}") + +# oscillation period of Y (zero crossings) in the first big volley +ys = [e[1] for e in eyes] +cross = [frames[i] for i in range(1,len(ys)) if ys[i-1]*ys[i] < 0 and frames[i]-frames[i-1] == 1] +if len(cross) > 3: + gaps = [b-a for a,b in zip(cross, cross[1:]) if b-a < 30] + if gaps: print(f"Y half-period ~{sum(gaps)/len(gaps):.1f} frames (full period ~{2*sum(gaps)/len(gaps):.1f})") + +# settle check: gap in frames = eye at rest (below 1e-9 magnitude threshold not logged) +gapct = sum(1 for a,b in zip(frames, frames[1:]) if b-a > 5) +print(f"rest gaps (>5 frames unlogged = settled to ~0): {gapct}") + +fig, ax = plt.subplots(2, 1, figsize=(16, 8), sharex=True) +ax[0].plot(frames, [e[0] for e in eyes], '.', ms=2, label='eye X (lateral)') +ax[0].plot(frames, [e[1] for e in eyes], '.', ms=2, label='eye Y (vert)') +ax[0].plot(frames, [e[2] for e in eyes], '.', ms=2, label='eye Z (fore/aft)') +for h in [h for h in hits if h[0] is not None]: ax[0].axvline(h[0], color='r', alpha=.25, lw=.8) +ax[0].legend(); ax[0].set_ylabel('eyePosition (u)'); ax[0].set_title('Gyro eye spring — red lines = hits received') +ax[1].plot(frames, [b[0] for b in bodies], '.', ms=2, label='body X (pitch)') +ax[1].plot(frames, [b[1] for b in bodies], '.', ms=2, label='body Y') +ax[1].plot(frames, [b[2] for b in bodies], '.', ms=2, label='body Z (roll)') +for h in [h for h in hits if h[0] is not None]: ax[1].axvline(h[0], color='r', alpha=.25, lw=.8) +ax[1].legend(); ax[1].set_ylabel('bodyOrientation (rad)'); ax[1].set_xlabel('frame') +plt.tight_layout(); plt.savefig("GYRO_TRACE.png", dpi=90) +print("wrote GYRO_TRACE.png") diff --git a/scratchpad/mp_gtrace.sh b/scratchpad/mp_gtrace.sh new file mode 100644 index 0000000..f6128c2 --- /dev/null +++ b/scratchpad/mp_gtrace.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +cd /c/git/bt411/content || exit 1 +rm -f gtrace_a.log gtrace_b.log +BT_LOG=gtrace_a.log BT_AFFINITY=0x1 BT_GYRO_LOG=1 BT_GYRO_TRACE=1 BT_START_INSIDE=1 ../build/Debug/btl4.exe -egg MP.EGG -net 1501 & +sleep 2 +BT_LOG=gtrace_b.log BT_AFFINITY=0x2 BT_AUTOFIRE=1 ../build/Debug/btl4.exe -egg MP.EGG -net 1601 & +sleep 8 +exec python ../tools/btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601