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<g> = 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<g>.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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-03 22:30:33 -05:00
co-authored by Claude Fable 5
parent 12d2952e9f
commit 7670d40b37
4 changed files with 208 additions and 38 deletions
+23
View File
@@ -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 <code>` 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
+31
View File
@@ -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
+27
View File
@@ -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
+127 -38
View File
@@ -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 "<dir>/DUMP" appears, each display window writes its
* current RGB buffer to "<dir>/win<g>.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: <name> = "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<g> = "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<g>.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();