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:
@@ -11,6 +11,7 @@ image/
|
||||
!game-live-gl.png
|
||||
!game-live-textured.png
|
||||
!game-live-lit.png
|
||||
!gauge-display.png
|
||||
dbx_out.txt
|
||||
vpx*.txt
|
||||
sweep_*.txt
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Gauge / secondary-display path (cockpit instruments)
|
||||
|
||||
The cockpit's six secondary displays (5 mono + 1 color) show instruments,
|
||||
driven by the PC's own SVGA video (an STB Horizon+ / S3-class card on the
|
||||
pod) in **VESA mode 0x111 = 640×480×16**, which the VDB splits across the
|
||||
monitors (see RIO-NOTES.md / HISTORY.md). Enabled by `L4GAUGE=640x480x16`
|
||||
(pod `PARAMETR.BAT :POD`) or `setenv.bat … g`. Config layout, instrument
|
||||
images, and gauge scripts live in `ALPHA_1/REL410/BT/GAUGE/`
|
||||
(`L4GAUGE.INI`, `L4GAUGE.CFG`, hundreds of `.GIM`/`.PCC`/`.PCX`).
|
||||
|
||||
## Fixed: the gauge crash was out-of-memory, not video
|
||||
|
||||
Enabling gauges crashed the game (`Exception 0E … illegal address 0x478`,
|
||||
a null-`this`). Root cause, from the game's own debug log:
|
||||
|
||||
```
|
||||
Requested 2808 bytes from MUNGA Heap
|
||||
Largest block remaining is 1100 bytes
|
||||
Heap size was set to 6291432 bytes
|
||||
d:\tesla_bt\munga\HEAP.CPP(342): No remaining block large enough!
|
||||
Failing to debugger
|
||||
```
|
||||
|
||||
The MUNGA main heap (`UserHeap MainStorage`, fixed size, no growth) exhausted
|
||||
loading the gauge images. It reads its size from the **`HEAPSIZE` environment
|
||||
variable** (BTL4OPT.EXE @VA 0x401076: `getenv("HEAPSIZE")`; else default
|
||||
`0x600000` = 6 MB). The pod's `PARAMETR.BAT` sets it (`:POD` → 15000000,
|
||||
`:REVIEW`/`:LOOP` → 32000000); our minimal `setenv.bat` launch never did, so
|
||||
the game used the 6 MB default and starved. **`memsize` does not affect it —
|
||||
it is the game's own heap, not DOS memory** (verified: `memsize=127` still
|
||||
gave 6 MB). The pod machines have **32 MB RAM**, so a 15 MB heap fits.
|
||||
|
||||
Fix (no binary patch): `set HEAPSIZE=15000000` before launch (see
|
||||
`gauge.conf`). The game then runs sustained with gauges on (500+ frames), the
|
||||
VESA mode switches (DOSBox screen blanks into graphics), and it draws the
|
||||
instrument panels — the VESA 640×480×16 mode works fine on our emulated S3.
|
||||
|
||||
Separately, `patch_btl4opt.py` now also neutralizes the general `Verify_Failed`
|
||||
debug-assertion crash (this is a `DEBUG_LEVEL 1` build); not required for the
|
||||
heap fix but makes the debug build behave like release under emulation.
|
||||
|
||||
## Open: bank-switching garbles the framebuffer
|
||||
|
||||
The gauge framebuffer currently renders **only a top strip, garbled** — the
|
||||
rest black. 640×480×16 is 600 KB accessed through a 64 KB window; the game
|
||||
pages it by **calling a hardcoded far pointer** from `L4GAUGE.INI`:
|
||||
|
||||
```
|
||||
[640x480x16]
|
||||
mode=273 ;0111h
|
||||
pageFcnPtr=796182 ;C000:2616 <- STB Horizon+ VESA bank-switch routine
|
||||
```
|
||||
|
||||
`C000:2616` is the STB card's VESA `WinFuncPtr`. On our emulated S3 that
|
||||
address is not the bank-switch function, so paging fails and only the first
|
||||
bank(s) (~top 50–100 lines) render. Until this is fixed there is no complete
|
||||
framebuffer to divide into the six displays.
|
||||
|
||||
Fix options (next):
|
||||
1. **Point `pageFcnPtr` at our card's real `WinFuncPtr`** — query VESA
|
||||
GetModeInfo (int 10h/4F01) for mode 0x111 under our S3 and put that far
|
||||
pointer in `L4GAUGE.INI` (`special`/`pageFcnPtr` fields), or force the
|
||||
game to use the queried pointer instead of the baked-in one.
|
||||
2. **Use a linear-framebuffer (LFB) mode** (VESA 2.0, mode 0x111|0x4000) so
|
||||
no bank switching is needed — requires the SVGA16 paging path to target
|
||||
the LFB.
|
||||
3. **Route bank switches through int 10h/4F05** (portable VESA windowing)
|
||||
instead of the direct pointer — a small game patch.
|
||||
|
||||
Once the full 640×480 buffer renders, splitting it into the six VDB display
|
||||
regions is straightforward (the VDB device already models the splitter).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -17,6 +17,11 @@ cd \REL410\BT
|
||||
set VIDEOFORMAT=svga
|
||||
set BLASTER=A220 I5 D1 H5 P330 T6
|
||||
set TEMP=c:\
|
||||
rem The pod's PARAMETR.BAT :POD path sets these; the game's HEAPSIZE default
|
||||
rem is only 6MB (too small once gauges load -> MUNGA heap exhaustion). Match
|
||||
rem the pod: 15MB heap + the 640x480x16 gauge mode.
|
||||
set HEAPSIZE=15000000
|
||||
set L4GAUGE=640x480x16
|
||||
rem arg4=g enables the gauge/secondary displays -> exercises the VDB splitter
|
||||
call setenv.bat r s n g
|
||||
32rtm.exe -x
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[sdl]
|
||||
output=opengl
|
||||
[dosbox]
|
||||
memsize=32
|
||||
machine=svga_s3
|
||||
[cpu]
|
||||
core=dynamic
|
||||
cputype=pentium
|
||||
cycles=max
|
||||
[serial]
|
||||
serial1=disabled
|
||||
serial2=disabled
|
||||
[autoexec]
|
||||
mount c "C:\VWE\TeslaRel410\ALPHA_1"
|
||||
c:
|
||||
cd \REL410\BT
|
||||
set VIDEOFORMAT=svga
|
||||
set BLASTER=A220 I5 D1 H5 P330 T6
|
||||
set TEMP=c:\
|
||||
call setenv.bat r s n g
|
||||
32rtm.exe -x
|
||||
btl4opt.exe -egg test.egg > c:\gaugelog.txt
|
||||
32rtm.exe -u
|
||||
echo ALPHA1-RUN-DONE
|
||||
pause
|
||||
@@ -0,0 +1,25 @@
|
||||
[sdl]
|
||||
output=opengl
|
||||
[dosbox]
|
||||
memsize=127
|
||||
machine=svga_s3
|
||||
[cpu]
|
||||
core=dynamic
|
||||
cputype=pentium
|
||||
cycles=max
|
||||
[serial]
|
||||
serial1=disabled
|
||||
serial2=disabled
|
||||
[autoexec]
|
||||
mount c "C:\VWE\TeslaRel410\ALPHA_1"
|
||||
c:
|
||||
cd \REL410\BT
|
||||
set VIDEOFORMAT=svga
|
||||
set BLASTER=A220 I5 D1 H5 P330 T6
|
||||
set TEMP=c:\
|
||||
call setenv.bat r s n g
|
||||
32rtm.exe -x
|
||||
btl4opt.exe -egg test.egg > c:\gaugelog.txt
|
||||
32rtm.exe -u
|
||||
echo ALPHA1-RUN-DONE
|
||||
pause
|
||||
+38
-22
@@ -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.
|
||||
|
||||
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 ; write to 0xFFFFFFFF -> Exception 0E
|
||||
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__":
|
||||
|
||||
Reference in New Issue
Block a user