#96: FIX -- normalise the myomer kinetic heat term to a reference frame rate

The previous commit only instrumented.  This is the behavioural change.

The binary's drive-heat integrator (FUN_004b8d18) multiplies its climb and accel
terms by the time slice but NOT the kinetic one:

    heat += ratio^2 * (1+dmg) * [ (1-accEff)*|vy|*m*g*dt
                                + (1-velEff)*(0.5*m*|v|^2)     <-- no dt
                                + (1-accEff)*|v|*|a|*m*dt ]

A per-TICK energy is a constant on a fixed-frame 1995 machine and a variable on a
modern PC: heat/second scales with frame rate, so two players on different
hardware -- or one player in a heavy scene versus an empty one -- get different
heat from identical throttle.  That is a defect independent of what the pod's
rate actually was.

The kinetic term is now multiplied by (dt * kPodFrameHz), which is exactly 1.0
when dt == 1/30 and makes the result machine-independent.  This is the one
deliberate divergence from a verbatim transcription of @004b8d18, and it is
marked as such in the source.

Measured, Owens at seek 4 flat out:
    before   crossed degradeT(1000) at ~27s, peaked 1213, settled ~710
    after    peak 1007, settled ~525
i.e. roughly halved at our measured ~59Hz, which is the direction the players
asked for ("too much IMO", "def need to tone down the myomer heat some").

⚠ CALIBRATION IS NOT PROVEN.  30Hz is the i860 BOARD frame ([T1],
rendering.md:177); the HOST simulation rate is not separately established, and no
fixed-timestep constant exists in the image (searched 1/15..1/60 and 30/60 Hz --
the arcade computed dt from a clock, as we do).  So the reference rate is the
best-documented value, not a proven one.  BT_MYO_HZ overrides it with no rebuild
so it can be calibrated by feel.

Precedent for the bug class, already logged in the KB: "trail density is
frame-rate-dependent (2/frame @60fps = 2x pod density)" (rendering.md:222).

NOT settled: whether the per-chassis ordering matches player memory (a Thor
sustaining seek 4 where a light chicken-walker cannot).  Post-fix benches put the
Thor HOTTER (crosses at 40s vs the Owens at 47s), but those runs are BT_GOTO
path-driven and the acceleration term is large and path-dependent, so they are
too noisy to conclude from.  The cooling data being identical across chassis IS
solid (measured, previous commit).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-01 13:34:44 -05:00
co-authored by Claude Opus 5
parent 9dd7d48b41
commit ee079083a3
+39 -3
View File
@@ -745,10 +745,38 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
// accTerm = m |v| |a| dt (acceleration power)
Scalar work = mass * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 (_DAT_004b8ee4)
// The three terms, kept separate for diagnosis (#96 "myomers run too hot"):
// two are POWER (x dt), one is an ENERGY added per TICK.
// The three terms (#96 "myomers run too hot"). Two are POWER (x dt); the
// kinetic one is an ENERGY the binary adds once per TICK with NO dt
// (FUN_004b8d18: param_2 multiplies the climb and accel terms only).
//
// FRAME-RATE NORMALISATION. A per-tick energy makes heat/SECOND scale with
// the tick rate, which on a 1995 fixed-frame machine is a constant and on a
// modern PC is not: two players on different hardware, or the same player in
// a heavy scene, get different heat from identical throttle. Measured here
// at ~59Hz the myomers cross their degradation threshold in ~27s of seek-4
// cruising; at the pod's documented 30Hz board frame that is ~54s.
//
// So the term is rescaled to the reference frame rate: multiply by
// dt * kPodFrameHz, which is 1.0 exactly when dt == 1/30 and makes the heat
// identical on every machine. This is the ONE deliberate divergence from a
// verbatim transcription of @004b8d18, and it exists because the binary's
// value depends on a constant our port does not have.
//
// ⚠ 30Hz is the i860 board frame ([T1], rendering.md) -- the HOST simulation
// rate is not separately established, so treat this as the reference, not as
// proven. BT_MYO_HZ overrides it without a rebuild so the pod veterans can
// calibrate it by feel.
static const Scalar kPodFrameHz = 30.0f;
static Scalar s_hz = -1.0f;
if (s_hz < 0.0f)
{
const char *hv = getenv("BT_MYO_HZ");
s_hz = (hv && *hv) ? (Scalar)atof(hv) : kPodFrameHz;
if (s_hz <= 0.0f) s_hz = kPodFrameHz;
}
Scalar termClimb = velComplement * vy * mass * gravity * time_slice;
Scalar termKinetic = workComplement * work; // <-- no dt, per the binary
Scalar termKinetic = workComplement * work * (time_slice * s_hz); // rate-normalised
Scalar termAccel = velComplement * velMag * accMag * mass * time_slice;
Scalar gain = ratio * ratio * damageGain;
@@ -757,9 +785,17 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
if (getenv("BT_MYO_LOG"))
{
static int s_n = 0;
static Scalar s_elapsed = 0.0f;
s_elapsed += time_slice; // wall clock since the sim started
if ((s_n++ % 120) == 0)
{
Scalar sum = termClimb + termKinetic + termAccel;
// T and the clock: how long does seek 4 actually take to cook the
// myomers? (#96 -- the players' claim is "under a minute".)
DEBUG_STREAM << "[myotemp] t=" << s_elapsed
<< " T=" << currentTemperature
<< " degradeT=" << degradationTemperature
<< " failT=" << failureTemperature << "\n" << std::flush;
DEBUG_STREAM << "[myoheat] v=" << velMag << " a=" << accMag
<< " m=" << mass << " g=" << gravity
<< " ratio=" << ratio << " dmgGain=" << damageGain