Gauges: fix out-of-memory crash (HEAPSIZE), draw instrument panels

Enabling gauges crashed (null-this, illegal 0x478). Root cause was NOT video:
the MUNGA main heap (fixed 6MB default) exhausted loading gauge images. The
heap size comes from getenv("HEAPSIZE") (BTL4OPT.EXE @0x401076; default
0x600000); the pod's PARAMETR.BAT sets it (:POD=15MB, :REVIEW/:LOOP=32MB) but
our minimal setenv launch never did. memsize is irrelevant (it's the game's
own heap, not DOS memory -- verified memsize=127 still gave 6MB). Pods have
32MB RAM so 15MB fits.

Fix (no binary patch): set HEAPSIZE=15000000 + L4GAUGE=640x480x16 in
gauge.conf. Game now runs sustained with gauges on (500+ frames); the VESA
640x480x16 mode switches fine on our emulated S3 and the cockpit instrument
panels draw (DISPLAY/PROGRAM/ENG DATA/TRIGGER CONFIG...).

patch_btl4opt.py also gained Verify_Failed (DEBUG-build assertion) neutral-
ization -- release-equivalent robustness, separate from the heap fix.

Known-open (GAUGE-NOTES.md): the framebuffer renders only a top strip,
garbled -- the game bank-switches via a hardcoded far pointer in L4GAUGE.INI
(C000:2616, the STB Horizon+ card's VESA WinFuncPtr) that isn't valid on our
emulated S3, so only the first bank(s) page in. Fixing bank-switching (or an
LFB mode) is next; then the full buffer can be split into the six displays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-03 17:48:07 -05:00
co-authored by Claude Opus 4.8
parent 08f71eb471
commit b2e4c5eaa6
7 changed files with 168 additions and 25 deletions
+41 -25
View File
@@ -1,33 +1,46 @@
#!/usr/bin/env python3
"""Neutralize the shipped-in debug crash macros in BTL4OPT.EXE.
The Rel 4.10 BattleTech binary was built with the RIO serial driver's
`DIE_ON_ERROR equ 1` diagnostic enabled (CODE/RP/MUNGA_L4/PCSPAK.ASM). Its
`DISABLE_AND_DIE` macro deliberately faults ("crash loudly to the debugger")
on any serial-transmit anomaly:
The Rel 4.10 BattleTech binary is a DEBUG build (DEBUG_LEVEL 1; the binary
even contains "// this file has not been approved for release"). Two families
of deliberate "crash to the debugger" macros are compiled in, both faulting
by writing to 0xFFFFFFFF (Exception 0E). On the real pod a clean cockpit
harness rarely tripped them; through emulation + serial passthrough they fire
on benign hiccups and halt the whole game.
push eax; push edx
mov edx, 0FFFFFFFFh ; crash loudly
mov eax, <error-code>
mov [edx], eax ; write to 0xFFFFFFFF -> Exception 0E
1. DISABLE_AND_DIE -- the RIO serial driver's `DIE_ON_ERROR equ 1` macro
(CODE/RP/MUNGA_L4/PCSPAK.ASM):
push eax; push edx
mov edx, 0FFFFFFFFh ; crash loudly
mov eax, <error-code>
mov [edx], eax
e.g. error 3 = a RIO packet byte > 0x7F (PCSPAK.ASM:1630), tripped by a
serial glitch during a board reset. Release build (DIE_ON_ERROR equ 0)
compiles it out and recovers (`and al,07Fh`).
On the real pod a clean cockpit harness never tripped these; through the
DOSBox-X serial passthrough a RIO glitch (e.g. during a board reset) hits
error 3 (PCSPAK.ASM:1630, "body char > 0x7F") and halts the whole game. The
release build (`DIE_ON_ERROR equ 0`) compiles the macro to nothing and lets
the normal recovery path (`and al,07Fh`) continue.
2. Verify_Failed / Fail_To_Debugger -- the general C++ assertion routines
(CODE/RP/MUNGA/VERIFY.HPP; Verify(c) -> Verify_Failed on !c). After
printing message/file/line they crash:
or edx, 0FFFFFFFFh
xor ecx, ecx
mov [edx], ecx
The routine's own re-entry guard already jumps *past* this to the clean
pop/ret, so NOPing the crash makes every failed assertion "print and
continue" -- the DEBUGOFF behavior. (Failing gauge-init Verifys were
halting the game before the secondary displays could come up.)
This tool finds every DISABLE_AND_DIE site by its exact 14-byte signature and
replaces it with NOPs -- exactly the release-build behavior -- writing a
<name>.orig backup first. Idempotent.
This tool finds every site of both idioms by exact signature and NOPs them,
writing a <name>.orig backup first. Idempotent.
Usage: patch_btl4opt.py <path-to-BTL4OPT.EXE>
"""
import os
import re
import sys
SIG = re.compile(
DIE = re.compile(
rb'\x50\x52\xBA\xFF\xFF\xFF\xFF\xB8(.)\x00\x00\x00\x89\x02', re.DOTALL)
VERIFY = re.compile(rb'\x83\xCA\xFF\x33\xC9\x89\x0A') # or edx,-1;xor ecx;mov[edx],ecx
def main():
@@ -35,22 +48,25 @@ def main():
raise SystemExit(__doc__)
path = sys.argv[1]
data = bytearray(open(path, 'rb').read())
sites = list(SIG.finditer(bytes(data)))
if not sites:
print("no DISABLE_AND_DIE sites found (already patched?)")
die = list(DIE.finditer(bytes(data)))
ver = list(VERIFY.finditer(bytes(data)))
if not die and not ver:
print("no crash sites found (already patched?)")
return
import os
bak = path + '.orig'
if not os.path.exists(bak):
open(bak, 'wb').write(data)
print(f"backup written: {bak}")
for m in sites:
code = m.group(1)[0]
for m in die:
va = 0x401000 + m.start() - 0xe00 # CODE VA 0x401000 @ file 0xe00
print(f" NOP VA 0x{va:06x} (error code {code})")
print(f" NOP VA 0x{va:06x} DISABLE_AND_DIE (code {m.group(1)[0]})")
data[m.start():m.end()] = b'\x90' * (m.end() - m.start())
for m in ver:
va = 0x401000 + m.start() - 0xe00
print(f" NOP VA 0x{va:06x} Verify_Failed crash tail")
data[m.start():m.end()] = b'\x90' * (m.end() - m.start())
open(path, 'wb').write(data)
print(f"patched {len(sites)} sites")
print(f"patched {len(die)} DISABLE_AND_DIE + {len(ver)} Verify sites")
if __name__ == "__main__":