cadence census item 2: gyro spring-damper stepped on the pod's 28Hz clock (gotcha 32) -- the cockpit bounce runs the machine's timescale again. The binary steps IntegrateEyeJoint @004b2ec0 / IntegrateBody @004b30ec once per pod frame with PER-TICK math (no-dt position step, damping overwrite carrying last-tick state); at ~60fps the springs stepped 2.14x too often. Fix: 28Hz per-instance accumulator (heat.cpp #119 map pattern), 1/28 slices, remainder carried, catch-up capped 28; integrator BODIES byte-untouched; BT_GYRO_SPRING_HZ override (=0 restores render cadence, proven behaviorally identical to the old code: control run reproduces the BEFORE curve exactly, 0.0471 @ +0.240s / period 1.435s). Impulse bench (new BT_GYRO_KICK one-shot deterministic hit through the authentic GyroApplyDamage fan-out; [gtrace] gains wall-ms): BEFORE eye trough -0.0452 @ +0.24s, overshoot 23%, period ~1.43s, settle +3.44s; AFTER trough -0.0537 @ +0.29s, overshoot 7%, period ~2.4s, settle +3.49s; 97 integrator steps / 3.49s ~ 28Hz -- the clock provably ticks. Shape verdict: both single-overshoot damped responses; every delta is the per-tick semantics at dt=1/28 (per-SECOND damping ~e^c is cadence-invariant, hence equal settle). Nothing beyond the timescale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
This commit is contained in:
Joe DiPrima
2026-08-13 19:54:14 -05:00
co-authored by Claude Fable 5
parent 2c6dc001e3
commit f811c64590
2 changed files with 90 additions and 2 deletions
+49 -2
View File
@@ -53,6 +53,8 @@
#endif
#include <JOINT.hpp> // Joint, JointSubsystem (fwd shim)
#include <ROTATION.hpp> // EulerAngles, Radian (fwd shim)
#include <time.h> // clock() -- [gtrace] wall-ms stamps (cadence bench)
#include <map> // the 28Hz integrator clock (heat.cpp #119 pattern)
#if !defined(APP_HPP)
# include <app.hpp>
#endif
@@ -478,8 +480,51 @@ void
if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise;
if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise;
IntegrateEyeJoint(time_slice); // FUN_004b2ec0
IntegrateBody(time_slice); // FUN_004b30ec
// (cadence census item 2, gotcha 32) THE INTEGRATOR CLOCK: the binary
// steps these two spring-damper integrators once per Perform = once per
// pod frame (28 Hz), and the math is PER-TICK (no-dt position step,
// damping overwrite carrying last-tick state) -- at the port's ~60 fps
// the springs stepped 2.14x as often and the cockpit bounce ran a
// foreign timescale. Step them on a 28 Hz accumulator (heat.cpp #119
// pattern: per-instance static map -- the factory size-locks the layout,
// no new members), passing the pod tick as the time_slice exactly as
// the machine's frame did; remainder carries. The integrator BODIES
// stay byte-exact (including the no-dt position step and the damping
// carry -- per-tick semantics, untouched). Impulses landing between
// ticks sit in the force accumulators until the next tick, same as a
// between-frame hit on the pod. Catch-up is capped at 28 ticks (1 s);
// a longer hitch drops the excess like the heat clock does.
// BT_GYRO_SPRING_HZ=<hz> overrides for bracketing (=0 or negative
// restores raw render-cadence stepping).
{
static Scalar s_springHz = -2.0f;
if (s_springHz < -1.0f)
{
const char *hz = getenv("BT_GYRO_SPRING_HZ");
s_springHz = (hz != 0 && *hz != '\0') ? (Scalar)atof(hz) : 28.0f;
}
if (s_springHz > 0.0f)
{
static std::map<const void *, Scalar> s_springClock;
Scalar &clock = s_springClock[this];
clock += time_slice;
const Scalar kTick = 1.0f / s_springHz;
int catchUp = 0;
while (clock >= kTick && ++catchUp <= 28)
{
clock -= kTick;
IntegrateEyeJoint(kTick); // FUN_004b2ec0, pod-tick slice
IntegrateBody(kTick); // FUN_004b30ec
}
if (catchUp > 28)
clock = 0.0f; // hitch: drop the excess
}
else
{
IntegrateEyeJoint(time_slice); // FUN_004b2ec0 (render cadence)
IntegrateBody(time_slice); // FUN_004b30ec
}
}
// NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here.
// WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called
@@ -659,6 +704,8 @@ void
+ bodyOrientation.z*bodyOrientation.z;
if (m2 > 1e-9f)
DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame
<< " t=" << (long)clock() // wall ms (MSVC CLOCKS_PER_SEC=1000) -- the
// bounce-timescale bench needs real time, not frames
<< " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y
<< " " << (float)eyePosition.z
<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
+41
View File
@@ -7886,6 +7886,47 @@ void
}
}
// GYRO IMPULSE BENCH (cadence census item 2, night17):
// BT_GYRO_KICK=<frame>[,<amt>] injects EXACTLY ONE deterministic hit
// into the gyro's authentic damage fan-out (GyroApplyDamage ->
// ApplyDamageResponse @004b2980) at the given master-perf frame
// (default 900) -- a FIXED damageForce so the direction never rides
// the random fallback, ballistic type, amt default 25. Pure gyro
// impulse (no zone damage, no armor change): the [gtrace] receipts
// then record the eye/body bounce trajectory for the 28-vs-render
// spring-timescale comparison. Env-gated, off by default.
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_GYRO_KICK"))
{
static int s_gkFrame = 0;
static int s_gkDone = 0;
++s_gkFrame;
int gkAt = 900;
float gkAmt = 25.0f;
{
char spec[64];
strncpy(spec, getenv("BT_GYRO_KICK"), sizeof(spec) - 1);
spec[sizeof(spec) - 1] = 0;
char *comma = strchr(spec, ',');
if (comma != 0) { *comma = 0; gkAmt = (float)atof(comma + 1); }
if (spec[0] && atoi(spec) > 0) gkAt = atoi(spec);
}
if (!s_gkDone && s_gkFrame >= gkAt)
{
s_gkDone = 1;
Damage dmg;
dmg.damageType = Damage::BallisticDamageType;
dmg.damageAmount = gkAmt;
dmg.burstCount = 1;
dmg.damageForce = Vector3D(1.0f, 0.0f, 0.3f); // fixed => deterministic dir
dmg.impactPoint = localOrigin.linearPosition;
extern void GyroApplyDamage(Subsystem *, const Damage &);
GyroApplyDamage(gyroSubsystem, dmg);
DEBUG_STREAM << "[gyro-kick] frame=" << s_gkFrame
<< " amt=" << gkAmt << " (one-shot)\n" << std::flush;
}
}
// Gitea #6 scripted verify (BT_VIEWCYCLE_TEST=<frame>): pulse one
// secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at
// the given frame and every 300 frames after -- with