diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 9c52bf5..d8119ea 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -548,6 +548,26 @@ Respawn SIM path: `DropZoneReply` `FUN_004bffd0` → `Mech::Reset` @0x4009fb74; --- +## THE ENGINE FRAME-RATE GLOBAL — `DAT_0052140c` = 28.0f (2026-08-02) [T1] + +The DOS binary's per-frame<->per-second conversion scalar, set ONCE at startup: + + 0x401ace: mov [0x52140c], 0x41E00000 ; 28.0f (nominal rate) + 0x401ada: mov [0x52140c], 0x4191A6E0 ; 18.2065f (BIOS-tick fallback, PIT/65536) + +and passed straight into the ApplicationManager ctor by the DOS main +(0x401189: `push [0x52140c]`). ~90 code sites `fmul`/`fdiv` this global — +whenever the decomp shows an unexplained multiply/divide by `_DAT_0052140c`, +it is per-second <-> per-frame conversion at the pod's 28 Hz. + +**So the pod's nominal simulation cadence was 28 fps, not the i860 board's +30 Hz.** Every "per tick with no dt" accumulation in the binary (the myomer +kinetic heat term; candidate: particle trail density) was calibrated against +28 nominal — and less under load (the 18.2 fallback existing at all says slow +paths were expected; the EFFECTIVE in-pod rate is an open question, see +[[open-questions]]). Our port's frame pacer note in btl4main.cpp claimed 30 +as "the pod's authentic rate" — corrected to 28. + ## Key Relationships - Feeds: [[subsystems]] (factory + ClassIDs), [[combat-damage]] (damage delivery), [[gauges-hud]] (attribute binding). - Verified against: [[reconstruction-method]] (the decomp loop), [[reconstruction-gotchas]] (why raw offsets fail). diff --git a/game/btl4main.cpp b/game/btl4main.cpp index cdf783c..138b1bd 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -1512,9 +1512,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // (end_of_frame = now + 1/frameRate) degenerated and the game ran at a // timer-quantized ~15 FPS (66-70ms frames measured) on ANY hardware. // Default 60; env TARGETFPS still overrides (clamped to sanity). - int target_fps = 30; // the pod's authentic rate; frame work (~20-40ms in - // this build) misses 60Hz beats -> jitter, but holds - // 30 rock-steady. TARGETFPS env still overrides. + int target_fps = 30; // NOTE: the pod's byte-proven NOMINAL rate is 28 + // (DAT_0052140c = 28.0f @0x401ace; 18.2065 BIOS + // fallback @0x401ada -- decomp-reference.md). We + // keep 30 for display smoothness; anything that + // must match the pod's per-tick math normalizes + // itself (e.g. the myomer kinetic term). Frame + // work (~20-40ms) misses 60Hz beats -> jitter, + // holds 30 steady. TARGETFPS env overrides. { const char *tf = getenv("TARGETFPS"); if (tf != 0) diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 27a2985..0e42dc9 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -757,22 +757,41 @@ void Myomers::MyomersDriveHeat(Scalar time_slice) // 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. + // dt * hz, making 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 per-tick value depends on the pod's frame + // rate, which our port does not run at. // - // ⚠ 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; + // THE CLOCK ORIGIN, traced 2026-08-02 (was the open question here): + // the DOS binary keeps its engine frame rate in the global DAT_0052140c, + // set once at startup -- + // 0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal) + // 0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback) + // -- and consumed by ~90 fmul/fdiv sites across the image as THE + // per-frame<->per-second conversion. The DOS main passes it straight into + // the ApplicationManager ctor (0x401189: push [0x52140c]). So the pod's + // nominal Performance cadence was 28 Hz, byte-proven. [T1] + // + // CALIBRATION FACTOR 0.5, testimony (Oracle, 2026-08-02): at the byte rate + // the veterans still report ~2x too much myomer heat -- "If you could + // please halve the myomer heat rate again. We will bracket to something + // reasonable." The plausible physical story is the pod's EFFECTIVE rate + // under load (a 486 host missing beats; the 18.2 fallback existing at all + // says slow paths were expected), which statics cannot settle. Effective + // default therefore 28 * 0.5 = 14 Hz, explicitly marked testimony- + // calibrated [T3]; BT_MYO_HZ= overrides absolutely for bracketing. + static const Scalar kPodFrameHz = 28.0f; // byte-proven nominal [T1] + static const Scalar kSeekHeatCalib = 0.5f; // Oracle bracket 2026-08-02 [T3] 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; + s_hz = (hv && *hv) ? (Scalar)atof(hv) : (kPodFrameHz * kSeekHeatCalib); + if (s_hz <= 0.0f) s_hz = kPodFrameHz * kSeekHeatCalib; + if (getenv("BT_MYO_LOG")) + DEBUG_STREAM << "[myoheat] kinetic-term reference rate: " << s_hz + << " Hz (pod nominal 28 [T1] x calib " << kSeekHeatCalib + << " [T3]" << (hv ? ", ENV OVERRIDE" : "") << ")" << std::endl; } Scalar termClimb = velComplement * vy * mass * gravity * time_slice;