From f811c64590bd8ddf0e24cc49c66238877588bc9d Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 13 Aug 2026 19:54:14 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD --- game/reconstructed/gyro.cpp | 51 ++++++++++++++++++++++++++++++++++-- game/reconstructed/mech4.cpp | 41 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index 5b3d81f..72a697b 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -53,6 +53,8 @@ #endif #include // Joint, JointSubsystem (fwd shim) #include // EulerAngles, Radian (fwd shim) +#include // clock() -- [gtrace] wall-ms stamps (cadence bench) +#include // the 28Hz integrator clock (heat.cpp #119 pattern) #if !defined(APP_HPP) # include #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= 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 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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 9ed2609..f890a58 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -7886,6 +7886,47 @@ void } } + // GYRO IMPULSE BENCH (cadence census item 2, night17): + // BT_GYRO_KICK=[,] 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=): pulse one // secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at // the given frame and every 300 frames after -- with