From ee079083a3e3512ddeb5bcbfe1d40fb7000ea697 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 1 Aug 2026 13:34:44 -0500 Subject: [PATCH] #96: FIX -- normalise the myomer kinetic heat term to a reference frame rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- game/reconstructed/myomers.cpp | 42 +++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index a981fc8..27a2985 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -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