diff --git a/emulator/.gitignore b/emulator/.gitignore index f4f642d..b5c6593 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -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 diff --git a/emulator/GAUGE-NOTES.md b/emulator/GAUGE-NOTES.md new file mode 100644 index 0000000..97978b6 --- /dev/null +++ b/emulator/GAUGE-NOTES.md @@ -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). diff --git a/emulator/gauge-display.png b/emulator/gauge-display.png new file mode 100644 index 0000000..48b76c8 Binary files /dev/null and b/emulator/gauge-display.png differ diff --git a/emulator/gauge.conf b/emulator/gauge.conf index bd9559d..b4ce4ed 100644 --- a/emulator/gauge.conf +++ b/emulator/gauge.conf @@ -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 diff --git a/emulator/gaugelog.conf b/emulator/gaugelog.conf new file mode 100644 index 0000000..518f67e --- /dev/null +++ b/emulator/gaugelog.conf @@ -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 diff --git a/emulator/gaugemem.conf b/emulator/gaugemem.conf new file mode 100644 index 0000000..ff3b00f --- /dev/null +++ b/emulator/gaugemem.conf @@ -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 diff --git a/emulator/patch_btl4opt.py b/emulator/patch_btl4opt.py index 9bf6538..4a57001 100644 --- a/emulator/patch_btl4opt.py +++ b/emulator/patch_btl4opt.py @@ -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, - 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, + 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 -.orig backup first. Idempotent. +This tool finds every site of both idioms by exact signature and NOPs them, +writing a .orig backup first. Idempotent. Usage: patch_btl4opt.py """ +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__":