From 7670d40b3780bbdaca4c468195a1755b589824a7 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 3 Jul 2026 22:30:33 -0500 Subject: [PATCH] VDB heads: 4-head cockpit layout (VPX_COCKPIT=1), drop exploratory windows - VPX_COCKPIT=1 places the four VGA heads as borderless windows: Division main 800x600 @ 0,0; radar (win0) 640x480 @ 800,0; MFD heads win3/win4 640x480 @ 1440,0 / 2080,0 (heads 3+4 are driver-spanned into one 1280x480 canvas). VPX_MAIN / VPX_WIN = x,y[,w,h] overrides any window without a rebuild; default debug layout kept. - Remove exploratory win1/win2 (bits 0-7 via static palettes); keep the 0/3/4 numbering so win.bmp dump names stay stable. - Sync commit copy with the build tree: per-head palette decode in pal_draw() and device-side BMP capture (VPX_DUMPDIR). - RIO-NOTES: document the PCSPAK DISABLE_AND_DIE patch (error-3 crash on RIO resets); restore the terrain-shadow section heading it ate. - Add gauge_rio{,_log}.conf launch configs. Co-Authored-By: Claude Fable 5 --- emulator/RIO-NOTES.md | 23 +++++ emulator/gauge_rio.conf | 31 +++++++ emulator/gauge_rio_log.conf | 27 ++++++ emulator/vpx-device/vpxlog.cpp | 165 +++++++++++++++++++++++++-------- 4 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 emulator/gauge_rio.conf create mode 100644 emulator/gauge_rio_log.conf diff --git a/emulator/RIO-NOTES.md b/emulator/RIO-NOTES.md index 5f69063..e6ab596 100644 --- a/emulator/RIO-NOTES.md +++ b/emulator/RIO-NOTES.md @@ -56,6 +56,29 @@ emulator receive latencies were fixed with new `directserial` options: `game_rio.conf` uses `realport:COM1 rxpollus:100 rxburst:16`. +## The crash-to-desktop: PCSPAK's DISABLE_AND_DIE (patched) + +The recurring hard fault (`Exception 0E`, write to `0xFFFFFFFF`-ish, e.g. at +`BTL4OPT CODE+0x7D1D1` with `EAX=3`) is **deliberate**: the RIO serial packet +driver (`CODE/RP/MUNGA_L4/PCSPAK.ASM`) was shipped built with +`DIE_ON_ERROR equ 1`, which compiles a `DISABLE_AND_DIE ` debugging +macro at 12 error sites — it retracts the UART IRQ, EOIs the PIC, and then +"crashes loudly" by writing the error code to address `0xFFFFFFFF`. + +Our crash is error **3** (`PCSPAK.ASM:1630`): a byte ≥ 0x80 found in the TX +ring body (the protocol reserves high-bit bytes for commands). It gets hit +via the ACK/NAK-interrupt → restart path — exactly what physical RIO resets +and timeout storms exercise. On clean pod serial timing this never fired; on +a USB-serial rig with resets it does. + +The source's release configuration (`DIE_ON_ERROR equ 0`) makes the macro +empty and the code **recovers** (the next instruction masks the byte with +`and al,7Fh` and continues). We reproduce that intended behavior by patching +all 12 die sequences (`50 52 BA FF FF FF FF B8 xx 00 00 00 89 02` → NOPs) in +the working image's `BTL4OPT.EXE`. Original preserved as `BTL4OPT.EXE.orig`. +Error-code map (from PCSPAK.ASM): 0/1/2 rx framing states, 3 tx body >0x7F, +4/5 tx state. + ## Crash-on-advance fixed: arena terrain shadows With the RIO in sync the sim advances and the game crashed dereferencing the diff --git a/emulator/gauge_rio.conf b/emulator/gauge_rio.conf new file mode 100644 index 0000000..f8b9e6b --- /dev/null +++ b/emulator/gauge_rio.conf @@ -0,0 +1,31 @@ +[sdl] +output=opengl +[dosbox] +memsize=32 +machine=svga_s3 +[cpu] +core=dynamic +cputype=pentium +cycles=max +[serial] +# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's +# few-ms ACK deadline is met; plasma COM2 later. +serial1=directserial realport:COM1 rxpollus:100 +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:\ +rem gauges (15MB heap + 640x480x16) AND the RIO -- active mission so pal0's +rem palette-animation can be watched for selective flashing. +set HEAPSIZE=15000000 +set L4GAUGE=640x480x16 +call setenv.bat r s n g +32rtm.exe -x +btl4opt.exe -egg test.egg +32rtm.exe -u +echo ALPHA1-RUN-DONE +pause diff --git a/emulator/gauge_rio_log.conf b/emulator/gauge_rio_log.conf new file mode 100644 index 0000000..6064c33 --- /dev/null +++ b/emulator/gauge_rio_log.conf @@ -0,0 +1,27 @@ +[sdl] +output=opengl +[dosbox] +memsize=32 +machine=svga_s3 +[cpu] +core=dynamic +cputype=pentium +cycles=max +[serial] +serial1=directserial realport:COM1 rxpollus:100 +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:\ +set HEAPSIZE=15000000 +set L4GAUGE=640x480x16 +call setenv.bat r s n g +32rtm.exe -x +btl4opt.exe -egg test.egg > c:\riolog.txt +32rtm.exe -u +echo ALPHA1-RUN-DONE +pause diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 001968d..558b634 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -655,12 +655,47 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { SwapBuffers(dc); } -/* ---- VDB display windows: one 640x480 window per display ----------------- - * Renders the actual gauge framebuffer (DOSBox's 16bpp M_LIN16 video memory, - * the encoded video stream the VDB splits) so each window shows "similar - * output" to the DOSBox screen. (Palette-based per-display decode is TBD; for - * now all three show the shared framebuffer.) */ -static void pal_draw(HDC dc, int g, int cw, int ch) { +/* VPX_DUMPDIR: when a file "/DUMP" appears, each display window writes its + * current RGB buffer to "/win.bmp" (checked once per render tick). Lets + * the host grab pixel-perfect captures without moving/raising live windows. */ +static char pal_dump_dir[512] = ""; + +static void write_bmp(const char *path, const unsigned char *rgb, int W, int H) { + FILE *f = fopen(path, "wb"); + if (!f) return; + int rowsz = (W * 3 + 3) & ~3; /* 4-byte row padding */ + unsigned imgsz = (unsigned)rowsz * (unsigned)H; + unsigned filesz = 54u + imgsz; + unsigned char hdr[54]; memset(hdr, 0, sizeof hdr); + hdr[0]='B'; hdr[1]='M'; + hdr[2]=(unsigned char)filesz; hdr[3]=(unsigned char)(filesz>>8); + hdr[4]=(unsigned char)(filesz>>16); hdr[5]=(unsigned char)(filesz>>24); + hdr[10]=54; hdr[14]=40; + hdr[18]=(unsigned char)W; hdr[19]=(unsigned char)(W>>8); + hdr[22]=(unsigned char)H; hdr[23]=(unsigned char)(H>>8); + hdr[26]=1; hdr[28]=24; + hdr[34]=(unsigned char)imgsz; hdr[35]=(unsigned char)(imgsz>>8); + hdr[36]=(unsigned char)(imgsz>>16); hdr[37]=(unsigned char)(imgsz>>24); + fwrite(hdr, 1, 54, f); + static unsigned char row[640*3 + 4]; + const unsigned char pad[3] = {0,0,0}; + for (int y = H - 1; y >= 0; y--) { /* BMP bottom-up; img top-down */ + const unsigned char *s = &rgb[(size_t)y * W * 3]; + for (int x = 0; x < W; x++) { /* RGB -> BGR */ + row[x*3+0] = s[x*3+2]; row[x*3+1] = s[x*3+1]; row[x*3+2] = s[x*3+0]; + } + fwrite(row, 1, (size_t)W * 3, f); + if (rowsz > W*3) fwrite(pad, 1, (size_t)(rowsz - W*3), f); + } + fclose(f); +} + +/* ---- VDB display windows: one 640x480 window per VGA head ---------------- + * Decodes the gauge framebuffer (DOSBox's 16bpp M_LIN16 video memory, the + * encoded stream the VDB splits): win0 = bits 0-7 via pal0 = color radar; + * win3/win4 = bits 8-15 via pal1/pal2 = the two MFD heads (mono MFDs ride + * the individual R/G/B channel wires). g=1/2 (exploratory) no longer used. */ +static void pal_draw(HDC dc, int g, int cw, int ch, bool dump) { const int W = 640, H = 480; static unsigned char img[640 * 480 * 3]; const uint8_t *fb = vga.mem.linear; @@ -669,29 +704,33 @@ static void pal_draw(HDC dc, int g, int cw, int ch) { Bitu start = vga.config.real_start; /* visible page start (bytes) */ Bitu stride = (Bitu)W * 2; /* 16bpp */ if (fb) { - /* window 0 = the color radar = bits 0-7 (3:3:2 RGB); windows g>=1 = - * one single bit each, starting at bit 8 (g=1 -> bit 8 ... g=8 -> - * bit 15), rendered white/black -- so the remaining bits can be - * re-paired into their displays by eye. */ - unsigned bit = 7u + (unsigned)g; /* used for g>=1 */ + /* win0 = framebuffer LOW byte (bits 0-7) through pal0; win3/win4 = + * framebuffer HIGH byte (bits 8-15) through pal1/pal2. All as 8-bit + * RGB (6-bit VGA-DAC expanded to 8-bit). pal0 is dynamic; pal1/pal2 + * are static. (g<=2 would decode bits 0-7 via pal g; only g=0 is + * created now.) */ for (int y = 0; y < H; y++) { Bitu ofs = start + (Bitu)y * stride; unsigned char *d = &img[(size_t)y * W * 3]; for (int x = 0; x < W; x++, ofs += 2, d += 3) { uint16_t px = *(const uint16_t *)(fb + (ofs & mask)); - if (g == 0) { - d[0] = (unsigned char)(( px & 0x7u) * 255u / 7u); /* R: bits 0-2 */ - d[1] = (unsigned char)(((px >> 3u) & 0x7u) * 255u / 7u); /* G: bits 3-5 */ - d[2] = (unsigned char)(((px >> 6u) & 0x3u) * 255u / 3u); /* B: bits 6-7 */ - } else { - unsigned char v = ((px >> bit) & 1u) ? 255 : 0; - d[0] = d[1] = d[2] = v; - } + unsigned idx; int pg; + if (g <= 2) { idx = px & 0xFFu; pg = g; } /* bits 0-7 via pal0/1/2 */ + else { idx = (px >> 8u) & 0xFFu; pg = g - 2; } /* bits 8-15 via pal1/2 */ + const unsigned char *e = &vdb_pal[pg].ram[idx * 3u]; + d[0] = (unsigned char)((e[0] << 2) | (e[0] >> 4)); /* 6-bit DAC -> 8-bit R */ + d[1] = (unsigned char)((e[1] << 2) | (e[1] >> 4)); /* G */ + d[2] = (unsigned char)((e[2] << 2) | (e[2] >> 4)); /* B */ } } } else { memset(img, 0, sizeof img); } + if (dump && pal_dump_dir[0]) { + char p[600]; + snprintf(p, sizeof p, "%s/win%d.bmp", pal_dump_dir, g); + write_bmp(p, img, W, H); + } glViewport(0, 0, cw, ch); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); glDisable(GL_LIGHTING); @@ -709,10 +748,12 @@ static LRESULT CALLBACK rt_wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { /* Create a visible window with its own GL context (each window gets its own * so we can render several from one thread by wglMakeCurrent-ing each). */ static bool make_gl_window(const char *title, int w, int h, int x, int y, + bool borderless, HWND *out_wnd, HDC *out_dc, HGLRC *out_gl) { + DWORD style = borderless ? WS_POPUP : WS_OVERLAPPEDWINDOW; RECT r = { 0, 0, w, h }; - AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); - HWND wnd = CreateWindowA("VPXGL", title, WS_OVERLAPPEDWINDOW | WS_VISIBLE, + AdjustWindowRect(&r, style, FALSE); + HWND wnd = CreateWindowA("VPXGL", title, style | WS_VISIBLE, x, y, r.right - r.left, r.bottom - r.top, NULL, NULL, GetModuleHandleA(NULL), NULL); if (!wnd) return false; @@ -728,6 +769,16 @@ static bool make_gl_window(const char *title, int w, int h, int x, int y, return true; } +/* Optional per-window geometry override: = "x,y[,w,h]". */ +static void env_rect(const char *name, int *x, int *y, int *w, int *h) { + const char *v = getenv(name); + if (!v || !v[0]) return; + int a, b, c, d; + int n = sscanf(v, "%d,%d,%d,%d", &a, &b, &c, &d); + if (n >= 2) { *x = a; *y = b; } + if (n >= 4) { *w = c; *h = d; } +} + static DWORD WINAPI rt_main(LPVOID) { WNDCLASSA wc; memset(&wc, 0, sizeof wc); wc.style = CS_OWNDC; @@ -737,23 +788,49 @@ static DWORD WINAPI rt_main(LPVOID) { wc.lpszClassName = "VPXGL"; RegisterClassA(&wc); - HWND wnd; HDC dc; HGLRC gl; - if (!make_gl_window("VPX VelociRender (emulated)", 832, 512, 40, 40, - &wnd, &dc, &gl)) return 1; + /* VPX_COCKPIT=1: borderless windows on the modernized cockpit's four VGA + * heads -- main (Division) 800x600 @ 0,0; radar (win0) 640x480 @ 800,0; + * the two MFD heads (win3/win4) 640x480 @ 1440,0 and 2080,0 (those two + * outputs are driver-spanned into one 1280x480 canvas starting at + * x=1440). VPX_MAIN / VPX_WIN = "x,y[,w,h]" override any window. */ + const char *ck = getenv("VPX_COCKPIT"); + bool cockpit = (ck && ck[0] && ck[0] != '0'); - /* window 0 = color radar (bits 0-7); windows 1-8 = single bits 8..15, - * fanned out so the user can re-pair them into their displays. */ - const int NPAL = 9; - static const char *pal_titles[9] = { - "display 0 - radar (bits 0-7)", - "bit 8", "bit 9", "bit 10", "bit 11", - "bit 12", "bit 13", "bit 14", "bit 15" }; - HWND pwnd[9]; HDC pdc[9]; HGLRC pgl[9]; - bool phave[9]; - for (int g = 0; g < NPAL; g++) - phave[g] = make_gl_window(pal_titles[g], 480, 360, - 700 + (g % 3) * 500, 20 + (g / 3) * 400, + HWND wnd; HDC dc; HGLRC gl; + int mx = 40, my = 40, mw = 832, mh = 512; + if (cockpit) { mx = 0; my = 0; mw = 800; mh = 600; } + env_rect("VPX_MAIN", &mx, &my, &mw, &mh); + if (!make_gl_window("VPX VelociRender (emulated)", mw, mh, mx, my, + cockpit, &wnd, &dc, &gl)) return 1; + + /* Real VGA heads only: win0 = bits 0-7 via pal0 (color radar); win3/win4 + * = bits 8-15 via pal1/pal2 (the two MFD heads). The former exploratory + * win1/win2 (bits 0-7 via the static palettes) are removed; g keeps the + * original 0..4 numbering so pal_draw's bit/palette selection and the + * win.bmp dump names stay stable. */ + const int NPAL = 5; + static const char *pal_titles[5] = { + "radar (HEAD C) - bits 0-7 via pal0", + "", "", + "MFD head A, 2 lower (bits 8-15 via pal1)", + "MFD head B, 3 upper (bits 8-15 via pal2)" }; + static const int ck_x[5] = { 800, 0, 0, 1440, 2080 }; + HWND pwnd[5]; HDC pdc[5]; HGLRC pgl[5]; + bool phave[5]; + int slot = 0; /* debug-grid position counter */ + for (int g = 0; g < NPAL; g++) { + phave[g] = false; + if (g == 1 || g == 2) continue; /* exploratory decodes, removed */ + int x, y, w, h; + if (cockpit) { x = ck_x[g]; y = 0; w = 640; h = 480; } + else { x = 700 + (slot % 3) * 500; y = 20 + (slot / 3) * 400; + w = 480; h = 360; } + slot++; + char en[16]; snprintf(en, sizeof en, "VPX_WIN%d", g); + env_rect(en, &x, &y, &w, &h); + phave[g] = make_gl_window(pal_titles[g], w, h, x, y, cockpit, &pwnd[g], &pdc[g], &pgl[g]); + } VFrame cur; for (;;) { @@ -778,12 +855,18 @@ static DWORD WINAPI rt_main(LPVOID) { rt_frames++; } } - /* redraw the display windows every tick (live) */ + /* redraw the display windows every tick (live); dump BMPs if triggered */ + bool dump = false; + if (pal_dump_dir[0]) { + char trig[600]; snprintf(trig, sizeof trig, "%s/DUMP", pal_dump_dir); + FILE *tf = fopen(trig, "rb"); + if (tf) { fclose(tf); remove(trig); dump = true; } + } for (int g = 0; g < NPAL; g++) { if (!phave[g]) continue; wglMakeCurrent(pdc[g], pgl[g]); RECT cr; GetClientRect(pwnd[g], &cr); - pal_draw(pdc[g], g, cr.right, cr.bottom); + pal_draw(pdc[g], g, cr.right, cr.bottom, dump); } } } @@ -1325,6 +1408,12 @@ void VPXLOG_Init(void) { if (fifo_dump_fp == NULL) LOG_MSG("VPXLOG: cannot open fifodump '%s'", fd); } + const char *dd = getenv("VPX_DUMPDIR"); + if (dd && dd[0]) { + strncpy(pal_dump_dir, dd, sizeof pal_dump_dir - 1); + pal_dump_dir[sizeof pal_dump_dir - 1] = '\0'; + } + const char *rn = getenv("VPX_RENDER"); if (rn && rn[0] && rn[0] != '0') { vpx_render_start();