From dce89a8571015e01c64680f2d841916ef744747a Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 3 Jul 2026 14:27:58 -0500 Subject: [PATCH] Phase 3b: live OpenGL render backend in DOSBox-X (VPX_RENDER=1) The emulated VPX board now draws frames in real time. The FIFO burst assembler feeds an in-process DPL scene store; each vr_draw_scene publishes a frame snapshot to a dedicated WGL window thread that renders it fixed-function. - vpxlog.cpp: scene_burst() decode + VScene store + rt_main GL window thread; glFrustum from the view-node window rect, glScalef(-1,1,1) for Division's mirrored screen-x, action-31 camera as modelview. No build change (opengl32 already linked). - Validated: flyk divrgb.scn opens the "VPX VelociRender (emulated)" window and draws the SMPTE bars live via the real camera.spl spline camera (divrgb-live-gl.png), matching the offline render_capture.py decode. - Game path (alpha1.conf) opens the window + draws background but hits the pre-existing production btdpl.ini vr_sync timeout (sends_wo_rcv) -- not a 3b issue. Remaining work tracked in PHASE3-PROGRESS.md. Co-Authored-By: Claude Opus 4.8 --- emulator/.gitignore | 1 + emulator/PHASE3-PROGRESS.md | 63 ++++-- emulator/divrgb-live-gl.png | Bin 0 -> 9705 bytes emulator/vpx-device/vpxlog.cpp | 342 ++++++++++++++++++++++++++++++++- 4 files changed, 386 insertions(+), 20 deletions(-) create mode 100644 emulator/divrgb-live-gl.png diff --git a/emulator/.gitignore b/emulator/.gitignore index 93487a6..4dcd35b 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -5,6 +5,7 @@ image/ # Phase 3 evidence renders (keep: decoded from the captured VPX stream) !divrgb-decoded.png !divrgb-frame0.png +!divrgb-live-gl.png dbx_out.txt vpx*.txt sweep_*.txt diff --git a/emulator/PHASE3-PROGRESS.md b/emulator/PHASE3-PROGRESS.md index 9bb7198..d12e9b4 100644 --- a/emulator/PHASE3-PROGRESS.md +++ b/emulator/PHASE3-PROGRESS.md @@ -1,9 +1,10 @@ # Phase 3 — Render Backend: Progress -**Status (2026-07-03): Phase 3a complete — the render command stream is fully -captured and decoded, and a captured DIVRGB frame has been reconstructed to -pixels. First image ever produced from the Rel 4.10 VPX protocol without a -real board.** +**Status (2026-07-03): Phase 3a + 3b complete. 3a — the render command stream +is fully captured and decoded, and a captured DIVRGB frame reconstructed to +pixels offline. 3b — a live OpenGL window built into the emulated board draws +each frame in real time inside DOSBox-X. First images ever produced from the +Rel 4.10 VPX protocol without a real board.** ![decoded DIVRGB calibration screen](divrgb-decoded.png) @@ -71,12 +72,50 @@ PLUGE row — which validates vertices, connectivity, materials, camera and window mapping in one image. (Division screen x runs opposite to a GL-style eye space; without negating x the pattern comes out mirrored.) -## Next: 3b — live OpenGL backend +## 3b. Live OpenGL backend (DONE) -Feed the same decode path a live stream inside DOSBox-X instead of a dump: -maintain the node store in `vpxlog.cpp` (or a sibling `vpxrender.cpp`), upload -geometry on set_geom_verts/conns, draw on draw_scene into an OpenGL window -(or the DOSBox surface). The `divrgb.fifodump` fixture allows developing the -backend offline; `flyk yip.scn` (CYCLE, Red Planet geometry) is the next -fixture up once the DOS/4GW sync variant is handled, then the game itself -(`buttee.bgf` etc. via the production content path). +![live GL window rendering DIVRGB](divrgb-live-gl.png) + +`VPX_RENDER=1` turns on a live render backend built into `vpxlog.cpp` +(Windows/WGL). The same burst assembler that feeds `VPX_FIFODUMP` now also +feeds an in-process scene store (`scene_burst()` → `struct VScene`), and each +`vr_draw_scene` publishes a frame snapshot to a dedicated OpenGL window thread +(`rt_main`) that draws it with fixed-function GL. No build-system change was +needed — `opengl32` was already linked, and `vpxlog.cpp` is already in +`hardware/`. + +Validated: `flyk divrgb.scn` (`divrgb.conf` with `VPX_RENDER=1`) opens the +"VPX VelociRender (emulated)" window and draws the SMPTE bars live, framed by +the real `camera.spl` spline camera the app sends — matching the offline +`render_capture.py` decode. The window runs on its own thread, so it keeps +displaying the last frame after the DOS app exits. + +Design notes: +- Multi-burst payloads (`set_geom_verts`/`conns` continuations) are tracked + with `geom_active` / `conn_active` state, same as the offline decoder. +- Projection: `glFrustum` from the view-node window rect scaled by + `near/window_distance`; a `glScalef(-1,1,1)` handles Division's mirrored + screen-x. Camera is the row-major 3×3 from action 31 loaded as the + modelview rotation, then `glTranslatef(-eye)`. +- Frame handoff is a critical-section-guarded `VFrame` + auto-reset event; + the emulator thread never blocks on GL. + +Game path (`alpha1.conf`, full BattleTech v4.10): the window opens and draws +the view background, but the run hits the **pre-existing production-path sync +timeout** (`velocirender_receive timed out — sends_wo_rcv=3`, then +`failed in vr_sync`) — the `btdpl.ini`/netnub launch bursts several sends +before its first receive, which the POLL_THRESHOLD gating stalls. This is not +a 3b issue (the `flyk` clean-launch path renders fine); it is the same +production-sync item still open from Phase 2. + +## Remaining + +1. **Production-path vr_sync timeout** (`sends_wo_rcv`): the game's + `btdpl.ini`/netnub launch path. Needed before the game itself renders. +2. **Content**: `test.egg` carries no models, so even past sync the game has + no `buttee.bgf`/`mslr.bgf` geometry to draw — needs a real mission/content + tree from the pod image. +3. **DOS/4GW sync variant** for the CYCLE `flyk yip.scn` fixture (Red Planet + geometry) — the older action-check sync. +4. Texturing (`set_texmap_texels`, action 23-data + SVT), lighting, and + depth/material niceties; current backend is flat-shaded untextured polys. diff --git a/emulator/divrgb-live-gl.png b/emulator/divrgb-live-gl.png new file mode 100644 index 0000000000000000000000000000000000000000..381bf1029ff4ced1010996d10276ce15ee3c4702 GIT binary patch literal 9705 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 B#Lxf$ literal 0 HcmV?d00001 diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 2cd1d1f..20ad767 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -141,9 +141,13 @@ static void fifo_arm_action(void) { fifo_arm = true; fifo_cap_pos = 0; fifo_cap static FILE *fifo_dump_fp = NULL; static unsigned char *fifo_buf = NULL; static size_t fifo_buf_len = 0, fifo_buf_cap = 0; +static bool vpx_render = false; /* Phase 3b live GL backend */ +static void scene_burst(const unsigned char *p, size_t n); /* fwd (3b) */ +static void scene_reset(void); /* fwd (3b) */ static void fifo_buf_push(unsigned char v) { - if (fifo_dump_fp == NULL) return; + if (fifo_dump_fp == NULL && !vpx_render) return; + if (fifo_buf_len >= (1u << 20)) return; /* runaway guard */ if (fifo_buf_len == fifo_buf_cap) { size_t ncap = fifo_buf_cap ? fifo_buf_cap * 2 : 4096; unsigned char *nb = (unsigned char *)realloc(fifo_buf, ncap); @@ -153,13 +157,16 @@ static void fifo_buf_push(unsigned char v) { fifo_buf[fifo_buf_len++] = v; } static void fifo_flush_record(void) { - if (fifo_dump_fp == NULL || fifo_buf_len == 0) return; - unsigned char hdr[8] = { 'V','P','X','M', - (unsigned char)(fifo_buf_len), (unsigned char)(fifo_buf_len >> 8), - (unsigned char)(fifo_buf_len >> 16), (unsigned char)(fifo_buf_len >> 24) }; - fwrite(hdr, 1, sizeof hdr, fifo_dump_fp); - fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp); - fflush(fifo_dump_fp); + if (fifo_buf_len == 0) return; + if (fifo_dump_fp) { + unsigned char hdr[8] = { 'V','P','X','M', + (unsigned char)(fifo_buf_len), (unsigned char)(fifo_buf_len >> 8), + (unsigned char)(fifo_buf_len >> 16), (unsigned char)(fifo_buf_len >> 24) }; + fwrite(hdr, 1, sizeof hdr, fifo_dump_fp); + fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp); + fflush(fifo_dump_fp); + } + if (vpx_render) scene_burst(fifo_buf, fifo_buf_len); fifo_buf_len = 0; } @@ -378,6 +385,7 @@ static void vpx_write(Bitu port, Bitu val, Bitu iolen) { fifo_arm = false; fifo_cap_pos = 0; fifo_cap = 0; expect_sync_token = false; sync_pending = false; sync_token = 0; frame_outstanding = false; fifo_flush_record(); + scene_reset(); note("board reset"); } else if (off == 1) { saw_write = true; /* outputData: a download/response byte */ @@ -391,6 +399,317 @@ static void vpx_write(Bitu port, Bitu val, Bitu iolen) { } } +/* ================= Phase 3b: live render backend (VPX_RENDER=1) ========== + * Reconstructs the DPL scene graph from the FIFO message stream (protocol + * established in PHASE3-PROGRESS.md / render_capture.py) and draws each + * vr_draw_scene frame in a native OpenGL window on a dedicated thread. + * Windows-only for now (WGL); the scene decode itself is portable. */ +#if defined(_WIN32) || defined(WIN32) +#define VPX_RENDER_SUPPORTED 1 +#endif + +#ifdef VPX_RENDER_SUPPORTED +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include + +static unsigned rd_u32(const unsigned char *p) { + return (unsigned)p[0] | ((unsigned)p[1] << 8) | + ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24); +} +static float rd_f32(const unsigned char *p) { + float f; unsigned u = rd_u32(p); memcpy(&f, &u, 4); return f; +} + +struct VCol { float c[3]; }; +struct VPoly { float rgb[3]; std::vector xyz; }; /* x,y,z triples */ +struct VFrame { + bool valid; + float bg[3]; + float win[5]; /* l, b, r, t, window-plane distance */ + float nearp, farp; + int vw, vh; + bool has_cam; + float rot[9], eye[3]; /* row-major rotation; eye = R*(p - e) */ + std::vector polys; + VFrame() : valid(false), nearp(2), farp(12000), vw(832), vh(512), + has_cam(false) { + bg[0] = bg[1] = bg[2] = 0; + win[0] = -1; win[1] = -0.6153846f; win[2] = 1; win[3] = 0.6153846f; + win[4] = 1.3f; + } +}; + +static struct VScene { + std::map type; /* name -> node type */ + std::map > verts; /* geometry -> xyz */ + std::map > > polys; + std::map mat; /* material -> RGB */ + std::map ggmat; /* geogroup -> material */ + std::map > children; + VFrame view; /* view/bg/camera state */ + /* multi-burst assembly */ + unsigned geom_node; size_t geom_need; bool geom_active; + unsigned conn_node, conn_npolys, conn_loop; bool conn_active; +} S; + +/* ---- render thread ------------------------------------------------------ */ +static HANDLE rt_thread = NULL, rt_event = NULL; +static CRITICAL_SECTION rt_lock; +static VFrame rt_pending; +static bool rt_new = false; +static unsigned long rt_frames = 0; + +static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { + glViewport(0, 0, cw, ch); + glClearColor(f.bg[0], f.bg[1], f.bg[2], 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + if (f.has_cam && !f.polys.empty()) { + double n = f.nearp > 0 ? f.nearp : 2.0; + double fa = f.farp > n ? f.farp : 12000.0; + double wd = f.win[4] != 0 ? f.win[4] : 1.3; + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + /* Division screen x runs opposite to GL eye x (SMPTE pattern comes + * out mirrored otherwise) -- flip x after projection. */ + glScalef(-1.0f, 1.0f, 1.0f); + glFrustum(f.win[0] * n / wd, f.win[2] * n / wd, + f.win[1] * n / wd, f.win[3] * n / wd, n, fa); + glMatrixMode(GL_MODELVIEW); + GLfloat m[16]; + for (int r = 0; r < 3; r++) + for (int c = 0; c < 4; c++) + m[c * 4 + r] = (c < 3) ? f.rot[r * 3 + c] : 0.0f; + m[3] = m[7] = m[11] = 0.0f; m[15] = 1.0f; + glLoadMatrixf(m); + glTranslatef(-f.eye[0], -f.eye[1], -f.eye[2]); + glEnable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_LIGHTING); + glShadeModel(GL_FLAT); + for (size_t i = 0; i < f.polys.size(); i++) { + const VPoly &p = f.polys[i]; + glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]); + glBegin(GL_POLYGON); + for (size_t v = 0; v + 2 < p.xyz.size(); v += 3) + glVertex3f(p.xyz[v], p.xyz[v + 1], p.xyz[v + 2]); + glEnd(); + } + } + SwapBuffers(dc); +} + +static LRESULT CALLBACK rt_wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == WM_CLOSE) { ShowWindow(w, SW_MINIMIZE); return 0; } + return DefWindowProcA(w, msg, wp, lp); +} + +static DWORD WINAPI rt_main(LPVOID) { + WNDCLASSA wc; memset(&wc, 0, sizeof wc); + wc.style = CS_OWNDC; + wc.lpfnWndProc = rt_wndproc; + wc.hInstance = GetModuleHandleA(NULL); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszClassName = "VPXGL"; + RegisterClassA(&wc); + RECT r = { 0, 0, 832, 512 }; + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); + HWND wnd = CreateWindowA("VPXGL", "VPX VelociRender (emulated)", + WS_OVERLAPPEDWINDOW | WS_VISIBLE, 40, 40, + r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); + if (!wnd) return 1; + HDC dc = GetDC(wnd); + PIXELFORMATDESCRIPTOR pfd; memset(&pfd, 0, sizeof pfd); + pfd.nSize = sizeof pfd; pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 24; pfd.cDepthBits = 24; + int pf = ChoosePixelFormat(dc, &pfd); + SetPixelFormat(dc, pf, &pfd); + HGLRC gl = wglCreateContext(dc); + if (!gl) return 1; + wglMakeCurrent(dc, gl); + VFrame cur; + for (;;) { + DWORD w = MsgWaitForMultipleObjects(1, &rt_event, FALSE, INFINITE, + QS_ALLINPUT); + MSG msg; + while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageA(&msg); + } + bool redraw = false; + if (w == WAIT_OBJECT_0) { + EnterCriticalSection(&rt_lock); + if (rt_new) { cur = rt_pending; rt_new = false; redraw = true; } + LeaveCriticalSection(&rt_lock); + } + if (redraw && cur.valid) { + RECT cr; GetClientRect(wnd, &cr); + rt_draw(dc, cur, cr.right, cr.bottom); + rt_frames++; + } + } +} + +/* ---- scene-graph message decode ----------------------------------------- */ +static void scene_publish_frame(void) { + VFrame f = S.view; + f.valid = true; + for (std::map::const_iterator gi = S.ggmat.begin(); + gi != S.ggmat.end(); ++gi) { + VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */ + std::map::const_iterator mi = S.mat.find(gi->second); + if (mi != S.mat.end()) col = mi->second; + std::map >::const_iterator ci = + S.children.find(gi->first); + if (ci == S.children.end()) continue; + for (size_t k = 0; k < ci->second.size(); k++) { + unsigned geo = ci->second[k]; + std::map >::const_iterator vi = + S.verts.find(geo); + std::map > >::const_iterator + pi = S.polys.find(geo); + if (vi == S.verts.end() || pi == S.polys.end()) continue; + const std::vector &vv = vi->second; + for (size_t q = 0; q < pi->second.size(); q++) { + const std::vector &idx = pi->second[q]; + VPoly poly; + memcpy(poly.rgb, col.c, sizeof poly.rgb); + for (size_t j = 0; j < idx.size(); j++) { + size_t o = (size_t)idx[j] * 3; + if (o + 2 >= vv.size()) continue; + poly.xyz.push_back(vv[o]); + poly.xyz.push_back(vv[o + 1]); + poly.xyz.push_back(vv[o + 2]); + } + if (poly.xyz.size() >= 9) f.polys.push_back(poly); + } + } + } + EnterCriticalSection(&rt_lock); + rt_pending = f; + rt_new = true; + LeaveCriticalSection(&rt_lock); + SetEvent(rt_event); +} + +static void scene_burst(const unsigned char *p, size_t n) { + if (n < 4) return; + unsigned action = rd_u32(p); + const unsigned char *d = p + 4; + size_t nb = n - 4; + + /* multi-burst payload continuations take priority over new headers */ + if (S.geom_active && action == 23) { + std::vector &vl = S.verts[S.geom_node]; + for (size_t o = 0; o + 11 < nb; o += 12) { + vl.push_back(rd_f32(d + o)); + vl.push_back(rd_f32(d + o + 4)); + vl.push_back(rd_f32(d + o + 8)); + } + if (vl.size() / 3 >= S.geom_need) S.geom_active = false; + return; + } + if (S.conn_active && action == 25) { + std::vector > &pl = S.polys[S.conn_node]; + size_t nw = nb / 4; + for (size_t o = 0; o + S.conn_loop <= nw; o += S.conn_loop) { + std::vector loop; + for (unsigned j = 0; j + 1 < S.conn_loop; j++) /* drop closing dup */ + loop.push_back((int)rd_u32(d + (o + j) * 4)); + pl.push_back(loop); + } + if (pl.size() >= S.conn_npolys) S.conn_active = false; + return; + } + + switch (action) { + case 1: /* create [type][name] */ + if (nb >= 8) S.type[rd_u32(d + 4)] = rd_u32(d); + break; + case 3: { /* flush [name][type][struct] */ + if (nb < 8) break; + unsigned name = rd_u32(d), t = rd_u32(d + 4); + if (t == 11 && nb >= 92) { /* material diffuse */ + VCol c = { { rd_f32(d + 48), rd_f32(d + 52), rd_f32(d + 56) } }; + S.mat[name] = c; + } else if (t == 9 && nb >= 80) { /* geogroup material */ + S.ggmat[name] = rd_u32(d + 64); + } else if (t == 3 && nb >= 104) { /* view */ + S.view.win[0] = rd_f32(d + 24); S.view.win[1] = rd_f32(d + 28); + S.view.win[2] = rd_f32(d + 32); S.view.win[3] = rd_f32(d + 36); + S.view.win[4] = rd_f32(d + 40); + S.view.vw = (int)rd_f32(d + 44); S.view.vh = (int)rd_f32(d + 48); + S.view.nearp = rd_f32(d + 52); S.view.farp = rd_f32(d + 56); + S.view.bg[0] = rd_f32(d + 60); S.view.bg[1] = rd_f32(d + 64); + S.view.bg[2] = rd_f32(d + 68); + } + break; + } + case 11: /* list_add [parent][child] */ + if (nb >= 8) S.children[rd_u32(d)].push_back(rd_u32(d + 4)); + break; + case 23: /* set_geom_verts header */ + if (nb >= 36) { + S.geom_node = rd_u32(d); + S.geom_need = rd_u32(d + 8); + S.geom_active = S.geom_need > 0; + S.verts[S.geom_node].clear(); + } + break; + case 25: /* set_geom_conns header [name][n_polys][loop_len][0] */ + if (nb >= 16) { + S.conn_node = rd_u32(d); + S.conn_npolys = rd_u32(d + 4); + S.conn_loop = rd_u32(d + 8); + S.conn_active = (S.conn_npolys > 0 && S.conn_loop >= 2 && + S.conn_loop <= 16); + S.polys[S.conn_node].clear(); + } + break; + case 31: /* per-frame camera [?][view][3x3 rows][eye] */ + if (nb >= 56) { + for (int i = 0; i < 9; i++) + S.view.rot[i] = rd_f32(d + 8 + i * 4); + for (int i = 0; i < 3; i++) + S.view.eye[i] = rd_f32(d + 44 + i * 4); + S.view.has_cam = true; + } + break; + case 9: /* draw_scene: commit */ + scene_publish_frame(); + break; + default: + break; + } +} + +static void scene_reset(void) { + S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear(); + S.ggmat.clear(); S.children.clear(); + S.view = VFrame(); + S.geom_active = false; S.conn_active = false; +} + +static void vpx_render_start(void) { + InitializeCriticalSection(&rt_lock); + rt_event = CreateEventA(NULL, FALSE, FALSE, NULL); + rt_thread = CreateThread(NULL, 0, rt_main, NULL, 0, NULL); + vpx_render = (rt_thread != NULL); +} +#else /* !VPX_RENDER_SUPPORTED */ +static void scene_burst(const unsigned char *, size_t) {} +static void scene_reset(void) {} +static void vpx_render_start(void) {} +#endif + void VPXLOG_Init(void) { const char *env = getenv("VPXLOG"); if (env == NULL || env[0] == '\0') return; @@ -411,6 +730,13 @@ void VPXLOG_Init(void) { if (fifo_dump_fp == NULL) LOG_MSG("VPXLOG: cannot open fifodump '%s'", fd); } + const char *rn = getenv("VPX_RENDER"); + if (rn && rn[0] && rn[0] != '0') { + vpx_render_start(); + LOG_MSG("VPXLOG: live render backend %s", + vpx_render ? "started" : "unavailable"); + } + IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18); IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18);