/* VPX link-adapter device (Tesla Rel 4.10 emulation) * * Impersonates the INMOS C012 transputer link adapter the Division VPX board * hung off the ISA bus (host source: sda4/DPL3/LINKIO.C, setLA): * * base+0 (0x150) inputData R byte board->host * base+1 (0x151) outputData W byte host->board * base+2 (0x152) inputStatus R bit0 = inbound byte available * base+3 (0x153) outputStatus R bit0 = ready to accept outbound byte * base+16 (0x160) resetRoot W board reset strobe * base+17 (0x161) analyseRoot W board analyse strobe * * Two modes, selected by environment variables (device inert unless VPXLOG set): * * VPXLOG= Phase 1: log every access to . * VPX_RESPOND=1 Phase 2: also answer as the transputer would, so the * game gets past boot_xputer()'s startup_handshake(). * VPX_HANDSHAKES=N number of iserver transactions to feed (default 3; * Phil's note in VR_COMMS.C: the transputer C runtime * does 3 iserver transactions at startup). * * Protocol reference: sda4/DPL3/VR_COMMS.C. * - receive_protocol(): host reads a 4-byte little-endian length/route word * (bit31=iserver, bits16-23=sender, low16=payload length nb<=1040) then nb * payload bytes. startup_handshake() does N such reads; each is answered by * iserver_action() and only success is checked -- so feeding N well-formed * iserver "version" requests (tag 42, no request-content dependency) * satisfies the handshake. * - The monitor (VRENDMON.BTL) and i860 (VRNOSTEX.MNG) segment downloads are * pure host->board writes; the device just absorbs them. */ #include "dosbox.h" #include "inout.h" #include "logging.h" #include #include #include static const io_port_t VPX_BASE = 0x150; static FILE *vpx_fp = NULL; /* log file, or NULL */ static bool vpx_respond = false; static int vpx_max_handshakes = 3; /* ---- board->host FIFO (what the emulated board sends the game) ---------- */ static unsigned char in_fifo[64]; static int in_len = 0, in_pos = 0; /* ---- boot state machine ------------------------------------------------- */ enum Phase { P_INIT, P_HANDSHAKE, P_POSTBOOT }; static Phase phase = P_INIT; static bool saw_write = false; /* game has written at least one byte */ static int handshakes_fed = 0; /* ---- outbound frame parser (renderer messages, post-handshake) ---------- * * Wire format (VR_COMMS.C velocirender_transmit, little-endian PC path): * [length_word:4][action:4][data:(nb-4)] where nb = length_word & 0xffff. * iserver responses keep the same nb = length_word & 0xffff, so byte-count * alignment is preserved even when one slips through. We track the action of * the last complete message so the reply can echo it (the renderer echoes the * action it received: see velocirender_create/delete/sync in DPL_HOST.C). */ static void flush_run(void); /* fwd decl (logging, defined below) */ static bool parse_frames = false; /* true once handshake requests done */ static int wf_need_len = 4; /* bytes still needed for length_word */ static unsigned wf_lenbuf = 0, wf_lenshift = 0; static int wf_payload_left = -1; /* -1 => currently reading length_word */ static int wf_action_pos = 0; static unsigned wf_action = 0; static int wf_last_nb = 0; static unsigned last_action = 0; static void parse_out_byte(unsigned char v) { if (wf_payload_left < 0) { /* accumulating length_word */ wf_lenbuf |= ((unsigned)v) << wf_lenshift; wf_lenshift += 8; if (--wf_need_len == 0) { int nb = (int)(wf_lenbuf & 0xffff); wf_lenbuf = 0; wf_lenshift = 0; wf_need_len = 4; wf_payload_left = nb > 0 ? nb : 0; wf_last_nb = nb; wf_action_pos = 0; wf_action = 0; if (wf_payload_left == 0) wf_payload_left = -1; /* empty msg */ } } else if (wf_payload_left > 0) { /* reading payload */ if (wf_action_pos < 4) { wf_action |= ((unsigned)v) << (wf_action_pos * 8); wf_action_pos++; } if (--wf_payload_left == 0) { last_action = wf_action; /* message complete */ /* diagnostic: log small control frames (skip 512B i860 chunks) */ if (vpx_fp && wf_last_nb <= 64) { flush_run(); fprintf(vpx_fp, "# FRAME action=%u nb=%d (slow)\n", wf_action, wf_last_nb); fflush(vpx_fp); } wf_payload_left = -1; } } } /* ---- FIFO fast-path action extraction ---------------------------------- * * After the i860 boots the host switches to the FIFO path (OUTSW.ASM): each * message writes an outsw tag byte to outputData, then pumps the payload as * 16-bit words to the FIFO data port (observed at 0x154/0x155, low/high). The * payload of a control message begins with the 4-byte action. We arm on each * outputData write and capture the next 4 FIFO bytes; if they form a valid * action code (< 32, the vr_action enum range) we treat it as the action to * echo. Data runs (e.g. text/coords) yield large values and are ignored. */ static bool fifo_arm = false; static int fifo_cap_pos = 0; static unsigned fifo_cap = 0; static const unsigned VR_ACTION_MAX = 24; /* vr_action enum has 24 (0..23) */ static const unsigned VR_SYNC_ACTION = 0x2d; /* Rel4.10 sync/ping (from disasm) */ /* Sync protocol (velocirender_sync in BTL4OPT.EXE @0x48D220, disassembled): * sends action 0x2d with data [token, 0], receives, and checks that the * REPLY's node[0] == token. (The "unexpected action %d" message only prints * the received action; the real check at 0x48D271 is `cmp token, node[0]`.) * So the device must reply to a 0x2d message with node[0] = the sent token. * The token is the data word of the NEXT FIFO run after the 0x2d action. */ static bool expect_sync_token = false; static bool sync_pending = false; static unsigned sync_token = 0; static bool frame_outstanding = false; /* draw_scene sent, frame-ack owed */ static void fifo_arm_action(void) { fifo_arm = true; fifo_cap_pos = 0; fifo_cap = 0; } /* ---- Phase 3: full FIFO message dump (VPX_FIFODUMP=) -------------- * * The FIFO wire format (VR_COMMS.C velocirender_transmit + OUTSW.ASM): each * transmit is TWO tag-delimited bursts. The C caller fires the first 3 bytes * of the protocol word at outputData, outsw() sends 0x40 as its final byte * and REP OUTSWs the payload into the FIFO port. Burst 1 carries the 4-byte * action, burst 2 the data (protocol word (0xff<<16)|(data+4)). So recording * every FIFO byte between outputData writes yields alternating action/data * records that a decoder can pair back into [action][data] messages. * Record format: 'VPXM' u32 magic, u32 length LE, then the raw burst bytes. */ 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 && !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); if (nb == NULL) return; /* drop byte rather than crash */ fifo_buf = nb; fifo_buf_cap = ncap; } fifo_buf[fifo_buf_len++] = v; } static void fifo_flush_record(void) { 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; } static void parse_fifo_byte(unsigned char v) { if (!fifo_arm) return; fifo_cap |= ((unsigned)v) << (fifo_cap_pos * 8); if (++fifo_cap_pos == 4) { unsigned w = fifo_cap; fifo_cap = 0; fifo_cap_pos = 0; if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# FIFOCAP 0x%08X\n", w); fflush(vpx_fp); } if (expect_sync_token) { sync_token = w; sync_pending = true; expect_sync_token = false; fifo_arm = false; } else if (w == VR_SYNC_ACTION) { expect_sync_token = true; /* token follows contiguously; stay armed */ } else { /* don't clear sync_pending here: a pending sync token must survive * intervening sends until the sync receive consumes it. */ if (w < VR_ACTION_MAX) last_action = w; if (w == 9 /*vr_draw_scene*/) frame_outstanding = true; fifo_arm = false; } } } /* A well-formed iserver "version" request (VR_COMMS.C iserver_action case 42): * length_word = nb | (sender<<16) | 0x80000000, little-endian; payload = tag. * nb is padded even for iserver; use nb=2, payload {0x2a,0x00}. sender=0. */ static const unsigned char VERSION_REQUEST[6] = { 0x02, 0x00, 0x00, 0x80, /* length_word 0x80000002 LE */ 0x2a, 0x00 /* tag 42 (version) + pad */ }; static void queue_version_request(void) { memcpy(in_fifo, VERSION_REQUEST, sizeof VERSION_REQUEST); in_len = (int)sizeof VERSION_REQUEST; in_pos = 0; } /* A minimal non-iserver renderer reply for velocirender_receive() (VR_COMMS.C): * top bit of length_word clear (non-iserver), nb>=8, first int32 of payload = * action code. The post-boot vr_init reply is ignored by the caller * (DPL_HOST.C), so any action != vr_draw_scene_action satisfies it. */ static int postboot_acks = 0; /* Post-boot reply cap. This was a Phase-2 bring-up guard against a runaway * reply loop while the protocol was still being reversed. The protocol is now * solid and a real game session issues an unbounded number of sync/frame/ * render replies -- BattleTech aborts with "velocirender_receive timed out - * sends_wo_rcv" the instant the device stops answering. Default is now * effectively unlimited; override with VPX_MAX_ACKS only for diagnostics. */ static int vpx_max_postboot_acks = 0x7fffffff; static int empty_polls = 0; static const int POLL_THRESHOLD = 6; /* consecutive empty polls => blocking receive */ static void queue_render_ack_node(unsigned char action, unsigned node) { fifo_flush_record(); /* a receive means the outstanding burst is complete */ unsigned char m[12] = { 0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */ action, 0x00, 0x00, 0x00, /* payload[0..3] = action (LE) */ (unsigned char)(node), (unsigned char)(node >> 8), (unsigned char)(node >> 16), (unsigned char)(node >> 24) /* node[0] */ }; memcpy(in_fifo, m, sizeof m); in_len = (int)sizeof m; in_pos = 0; } static void queue_render_ack(unsigned char action) { queue_render_ack_node(action, 0); } /* ---- logging (run-length coalesced) ------------------------------------- */ static unsigned long vpx_seq = 0; static io_port_t last_port = 0xFFFF; static unsigned last_val = ~0u; static bool last_write = false; static unsigned long last_run = 0; static char reg_name_buf[16]; static const char *reg_name(io_port_t port) { switch (port - VPX_BASE) { case 0: return "inputData"; case 1: return "outputData"; case 2: return "inputStatus"; case 3: return "outputStatus"; case 16: return "resetRoot"; case 17: return "analyseRoot"; default: snprintf(reg_name_buf, sizeof reg_name_buf, "port_%03X", (unsigned)port); return reg_name_buf; } } static void flush_run(void) { if (last_run == 0 || vpx_fp == NULL) return; if (last_run == 1) fprintf(vpx_fp, "%8lu %s %-13s 0x%02X\n", vpx_seq++, last_write ? "W" : "R", reg_name(last_port), last_val & 0xFF); else fprintf(vpx_fp, "%8lu %s %-13s 0x%02X x%lu\n", vpx_seq++, last_write ? "W" : "R", reg_name(last_port), last_val & 0xFF, last_run); fflush(vpx_fp); last_run = 0; } static void record(io_port_t port, unsigned val, bool write) { if (vpx_fp == NULL) return; if (port == last_port && val == last_val && write == last_write) { last_run++; return; } flush_run(); last_port = port; last_val = val; last_write = write; last_run = 1; io_port_t off = port - VPX_BASE; if (write && (off == 1 || off == 16 || off == 17)) flush_run(); } static void note(const char *msg) { if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# %s\n", msg); fflush(vpx_fp); } } /* ---- I/O handlers ------------------------------------------------------- */ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) { io_port_t off = (io_port_t)port - VPX_BASE; Bitu ret = 0xFF; switch (off) { case 3: /* outputStatus: always ready to accept a byte */ ret = 0x01; break; case 2: /* inputStatus: bit0 = inbound byte available */ ret = 0x00; if (vpx_respond) { if (in_pos < in_len) { ret = 0x01; /* still draining a queued message */ empty_polls = 0; } else { /* FIFO empty: decide whether to start a new transaction. */ if (phase == P_INIT && saw_write) phase = P_HANDSHAKE; if (phase == P_HANDSHAKE) { if (handshakes_fed < vpx_max_handshakes) { queue_version_request(); handshakes_fed++; note("feeding iserver version request"); if (handshakes_fed >= vpx_max_handshakes) parse_frames = true; /* i860 dl + init now framed */ ret = 0x01; } else { phase = P_POSTBOOT; note("handshake complete; entering post-boot"); } } if (phase == P_POSTBOOT && in_pos >= in_len) { /* Distinguish a blocking receive (inRecord spins, * polling inputStatus many times) from a speculative * single poll (handle_iserver_stuff/altRecord polls * once and proceeds if not-ready). Only feed a reply * after several consecutive empty polls, so we don't * inject renderer replies into iserver-drain checks. */ if (++empty_polls < POLL_THRESHOLD) { ret = 0x00; break; } empty_polls = 0; if (postboot_acks < vpx_max_postboot_acks) { if (sync_pending) { /* velocirender_sync: reply node[0] = token so * its `cmp token, node[0]` passes. Action field * is not checked (just printed on failure). */ queue_render_ack_node((unsigned char)VR_SYNC_ACTION, sync_token); sync_pending = false; if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# post-boot: sync reply token=0x%X\n", sync_token); fflush(vpx_fp); } } else if (frame_outstanding) { /* velocirender_frameack expects a message with * action == vr_draw_scene_action (9). */ frame_outstanding = false; queue_render_ack(9); if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# post-boot: frame ack (action 9)\n"); fflush(vpx_fp); } } else { /* Reply action is handler-specific (board side, * VR_REMOT.C): most echo data[0] (the sent * action); a few overwrite it: * vr_init_action(0)/statistics(15) -> 1 */ unsigned reply = last_action; if (last_action == 0 || last_action == 15) reply = 1; { const char *o = getenv("VPX_INIT_REPLY"); if (o && last_action == 0) reply = (unsigned)atoi(o); } queue_render_ack((unsigned char)(reply & 0xff)); if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# post-boot: reply action %u (sent %u)\n", reply, last_action); fflush(vpx_fp); } } postboot_acks++; ret = 0x01; } } } } break; case 0: /* inputData: next byte from board->host FIFO */ if (vpx_respond && in_pos < in_len) ret = in_fifo[in_pos++]; else ret = 0xFF; break; default: ret = 0xFF; break; } record((io_port_t)port, (unsigned)ret, false); return ret; } static bool warned_iolen = false; static void vpx_write(Bitu port, Bitu val, Bitu iolen) { io_port_t off = (io_port_t)port - VPX_BASE; if (iolen != 1 && !warned_iolen && vpx_fp) { warned_iolen = true; flush_run(); fprintf(vpx_fp, "# NOTE: non-byte write iolen=%u port=0x%03X val=0x%X\n", (unsigned)iolen, (unsigned)port, (unsigned)val); fflush(vpx_fp); } record((io_port_t)port, (unsigned)val, true); if (off == 16 && (val & 1)) { /* resetRoot asserted: board reset -> clear all state. */ phase = P_INIT; saw_write = false; handshakes_fed = 0; postboot_acks = 0; in_len = in_pos = 0; parse_frames = false; wf_need_len = 4; wf_lenbuf = 0; wf_lenshift = 0; wf_payload_left = -1; wf_action_pos = 0; wf_action = 0; last_action = 0; 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 */ empty_polls = 0; /* a write means the game isn't blocking-reading */ fifo_flush_record(); /* outputData write = FIFO burst boundary */ if (parse_frames) { parse_out_byte((unsigned char)val); fifo_arm_action(); } } else if (off == 4 || off == 5) { /* FIFO data port (link B): payload words, low/high bytes. */ fifo_buf_push((unsigned char)val); if (parse_frames) parse_fifo_byte((unsigned char)val); } } /* ================= 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 */ std::vector uv; /* u,v pairs (empty = untextured) */ unsigned matkey; /* material name for texture lookup, 0 = none */ VPoly() : matkey(0) {} }; /* Baked RGBA texture (texture intensity x material ramp), keyed by material. * Shared with the GL thread under rt_lock; ver bumps force re-upload. */ struct TexImg { int w, h; unsigned ver; std::vector rgba; }; 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; bool ydown; /* game world is y-down (DCS reflections) */ 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), ydown(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; } }; struct M16 { float m[16]; }; /* row-major 4x4, row 3 = translation */ struct VRamp { float lo[3], hi[3]; }; struct VTex { int w, h; unsigned ver; std::vector px; }; static struct VScene { std::map type; /* name -> node type */ std::map > verts; /* geometry -> xyz */ std::map > uvs; /* geometry -> u,v */ std::map > > polys; std::map mat; /* material -> RGB */ std::map ggmat; /* geogroup -> material */ std::map > children; /* game (full DPL) hierarchy: instance placement + articulation */ std::map inst_object; /* instance -> object */ std::map dcs_mat; /* dcs -> local matrix */ std::map dcs_parent; /* dcs_link child->parent */ /* texture chain: material -> texmap -> texture, colorized by the * material's ramp (type 14: lo/hi RGB; texels are intensities) */ std::map tex; /* texture node -> texels */ std::map texmap_tex; /* texmap -> texture */ std::map mat_texmap; /* material -> texmap */ std::map mat_ramp; /* material -> ramp */ std::map ramp; /* ramp -> lo/hi */ VFrame view; /* view/bg/camera state */ /* multi-burst assembly (stride-aware: game verts are 3..9 floats each) */ unsigned geom_node; size_t geom_need, geom_stride; bool geom_active; std::vector geom_acc; unsigned conn_node, conn_npolys, conn_loop; bool conn_active; /* action-26 texel upload assembly */ unsigned tex_node; size_t tex_need; bool tex_active; std::vector tex_acc; } S; static void m16_id(M16 &o) { for (int i = 0; i < 16; i++) o.m[i] = (i % 5 == 0) ? 1.0f : 0.0f; } static void m16_mul(const M16 &a, const M16 &b, M16 &o) { /* o = a * b */ for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) { float s = 0; for (int k = 0; k < 4; k++) s += a.m[r * 4 + k] * b.m[k * 4 + c]; o.m[r * 4 + c] = s; } } static void m16_xform(const M16 &w, const float *v, float *o) { for (int c = 0; c < 3; c++) o[c] = v[0] * w.m[c] + v[1] * w.m[4 + c] + v[2] * w.m[8 + c] + w.m[12 + c]; } /* world transform of a dcs: local * parent_world (row-vector convention) */ static void dcs_world(unsigned dcs, std::map &cache, M16 &out, int depth = 0) { std::map::iterator ci = cache.find(dcs); if (ci != cache.end()) { out = ci->second; return; } M16 local; std::map::iterator mi = S.dcs_mat.find(dcs); if (mi != S.dcs_mat.end()) local = mi->second; else m16_id(local); std::map::iterator pi = S.dcs_parent.find(dcs); if (pi != S.dcs_parent.end() && pi->second != dcs && depth < 64) { M16 pw; dcs_world(pi->second, cache, pw, depth + 1); M16 w; m16_mul(local, pw, w); out = w; } else { out = local; } cache[dcs] = out; } /* ---- 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 std::map rt_texs; /* material -> baked RGBA (rt_lock) */ 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. The game world * is additionally y-down (its DCS matrices carry a reflection). */ glScalef(-1.0f, f.ydown ? -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); /* upload any new/changed baked material textures */ static std::map gltex; /* matkey -> GL name */ static std::map gltex_ver; EnterCriticalSection(&rt_lock); std::map texs = rt_texs; /* small; copy out */ LeaveCriticalSection(&rt_lock); for (std::map::const_iterator it = texs.begin(); it != texs.end(); ++it) { std::map::const_iterator vi = gltex_ver.find(it->first); if (vi != gltex_ver.end() && vi->second == it->second.ver) continue; GLuint id; std::map::const_iterator gi = gltex.find(it->first); if (gi == gltex.end()) { glGenTextures(1, &id); gltex[it->first] = id; } else id = gi->second; glBindTexture(GL_TEXTURE_2D, id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, it->second.w, it->second.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, &it->second.rgba[0]); gltex_ver[it->first] = it->second.ver; } glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); unsigned bound = 0; for (size_t i = 0; i < f.polys.size(); i++) { const VPoly &p = f.polys[i]; bool tex = p.matkey && gltex.count(p.matkey) && p.uv.size() * 3 == p.xyz.size() * 2; if (tex) { if (bound != p.matkey) { glBindTexture(GL_TEXTURE_2D, gltex[p.matkey]); bound = p.matkey; } glEnable(GL_TEXTURE_2D); glColor3f(1.0f, 1.0f, 1.0f); } else { glDisable(GL_TEXTURE_2D); 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) { if (tex) glTexCoord2f(p.uv[v / 3 * 2], p.uv[v / 3 * 2 + 1]); glVertex3f(p.xyz[v], p.xyz[v + 1], p.xyz[v + 2]); } glEnd(); } glDisable(GL_TEXTURE_2D); } 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 ----------------------------------------- */ /* Bake material's texture (intensity) through its ramp into RGBA and stash it * for the GL thread. Returns the material key if textured, else 0. */ static std::map baked_ver; /* material -> baked tex ver */ static unsigned bake_material_tex(unsigned matname) { std::map::const_iterator tmi = S.mat_texmap.find(matname); if (tmi == S.mat_texmap.end()) return 0; std::map::const_iterator txi = S.texmap_tex.find(tmi->second); if (txi == S.texmap_tex.end()) return 0; std::map::const_iterator ti = S.tex.find(txi->second); if (ti == S.tex.end() || ti->second.px.empty()) return 0; const VTex &t = ti->second; if ((size_t)t.w * t.h > t.px.size()) return 0; std::map::const_iterator bi = baked_ver.find(matname); if (bi != baked_ver.end() && bi->second == t.ver) return matname; float lo[3] = { 0, 0, 0 }, hi[3] = { 1, 1, 1 }; std::map::const_iterator ri = S.mat_ramp.find(matname); if (ri != S.mat_ramp.end()) { std::map::const_iterator rr = S.ramp.find(ri->second); if (rr != S.ramp.end()) { memcpy(lo, rr->second.lo, sizeof lo); memcpy(hi, rr->second.hi, sizeof hi); } } TexImg img; img.w = t.w; img.h = t.h; img.ver = t.ver; img.rgba.resize((size_t)t.w * t.h * 4); for (size_t i = 0; i < (size_t)t.w * t.h; i++) { float a = t.px[i] / 255.0f; for (int c = 0; c < 3; c++) { float v = lo[c] + (hi[c] - lo[c]) * a; img.rgba[i * 4 + c] = (unsigned char)(v < 0 ? 0 : v > 1 ? 255 : v * 255.0f + 0.5f); } img.rgba[i * 4 + 3] = 255; } EnterCriticalSection(&rt_lock); rt_texs[matname] = img; LeaveCriticalSection(&rt_lock); baked_ver[matname] = t.ver; return matname; } static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) { VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */ unsigned matname = 0; std::map::const_iterator gmi = S.ggmat.find(gg); if (gmi != S.ggmat.end()) { matname = gmi->second; std::map::const_iterator mi = S.mat.find(matname); if (mi != S.mat.end()) col = mi->second; } unsigned texkey = matname ? bake_material_tex(matname) : 0; std::map >::const_iterator ci = S.children.find(gg); if (ci == S.children.end()) return; 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; const std::vector *uv = NULL; std::map >::const_iterator ui = S.uvs.find(geo); if (ui != S.uvs.end() && ui->second.size() * 3 == vv.size() * 2) uv = &ui->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); poly.matkey = (texkey && uv) ? texkey : 0; for (size_t j = 0; j < idx.size(); j++) { size_t o = (size_t)idx[j] * 3; if (o + 2 >= vv.size()) continue; float out[3]; if (world) m16_xform(*world, &vv[o], out); else { out[0] = vv[o]; out[1] = vv[o + 1]; out[2] = vv[o + 2]; } poly.xyz.push_back(out[0]); poly.xyz.push_back(out[1]); poly.xyz.push_back(out[2]); if (poly.matkey) { poly.uv.push_back((*uv)[(size_t)idx[j] * 2]); poly.uv.push_back((*uv)[(size_t)idx[j] * 2 + 1]); } } if (poly.xyz.size() >= 9) f.polys.push_back(poly); } } } static void scene_publish_frame(void) { VFrame f = S.view; f.valid = true; f.ydown = !S.dcs_mat.empty(); /* game path: world is y-down */ /* Game (full DPL) path: instances are list_add children of DCS nodes; * instance -> object -> lod -> geogroup -> geometry, transformed by the * dcs_link articulation tree. */ std::map cache; std::map gg_done; for (std::map >::const_iterator di = S.children.begin(); di != S.children.end(); ++di) { if (S.type.count(di->first) == 0 || S.type[di->first] != 5) continue; M16 world; dcs_world(di->first, cache, world); for (size_t i = 0; i < di->second.size(); i++) { unsigned inst = di->second[i]; std::map::const_iterator oi = S.inst_object.find(inst); if (oi == S.inst_object.end()) continue; std::map >::const_iterator li = S.children.find(oi->second); if (li == S.children.end() || li->second.empty()) continue; unsigned lod = li->second[0]; /* highest LOD */ std::map >::const_iterator ggi = S.children.find(lod); if (ggi == S.children.end()) continue; for (size_t g = 0; g < ggi->second.size(); g++) { emit_geogroup(f, ggi->second[g], &world); gg_done[ggi->second[g]] = true; } } } /* flyk (flat) path: geogroups with geometry directly, no instance */ for (std::map::const_iterator gi = S.ggmat.begin(); gi != S.ggmat.end(); ++gi) if (!gg_done.count(gi->first)) emit_geogroup(f, gi->first, NULL); 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) { for (size_t o = 0; o + 3 < nb; o += 4) S.geom_acc.push_back(rd_f32(d + o)); if (S.geom_acc.size() >= S.geom_need * S.geom_stride) { std::vector &vl = S.verts[S.geom_node]; std::vector &tl = S.uvs[S.geom_node]; vl.clear(); tl.clear(); /* uv offset within a vertex record by stride: 5 = xyz+uv, * 8/9 = xyz+normal+uv(+extra) */ size_t uvo = (S.geom_stride >= 8) ? 6 : (S.geom_stride == 5) ? 3 : 0; for (size_t i = 0; i + 2 < S.geom_need * S.geom_stride; i += S.geom_stride) { vl.push_back(S.geom_acc[i]); vl.push_back(S.geom_acc[i + 1]); vl.push_back(S.geom_acc[i + 2]); if (uvo && i + uvo + 1 < S.geom_acc.size()) { tl.push_back(S.geom_acc[i + uvo]); tl.push_back(S.geom_acc[i + uvo + 1]); } else { tl.push_back(0); tl.push_back(0); } } S.geom_acc.clear(); S.geom_active = false; } return; } if (S.tex_active && action == 26) { for (size_t o = 0; o < nb; o++) S.tex_acc.push_back(d[o]); if (S.tex_acc.size() >= S.tex_need) { VTex &t = S.tex[S.tex_node]; t.px.assign(S.tex_acc.begin(), S.tex_acc.begin() + S.tex_need); t.ver++; S.tex_acc.clear(); S.tex_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; /* texmap / ramp refs: identify fields by node type */ for (size_t o = 8; o + 3 < nb; o += 4) { unsigned val = rd_u32(d + o); if (!val || val == 0xFFFFFFFFu) continue; std::map::const_iterator ti = S.type.find(val); if (ti == S.type.end()) continue; if (ti->second == 12) S.mat_texmap[name] = val; else if (ti->second == 14) S.mat_ramp[name] = val; } } else if (t == 12 && nb >= 12) { /* texmap -> texture */ for (size_t o = 8; o + 3 < nb; o += 4) { unsigned val = rd_u32(d + o); if (!val || val == 0xFFFFFFFFu) continue; std::map::const_iterator ti = S.type.find(val); if (ti != S.type.end() && ti->second == 13) { S.texmap_tex[name] = val; break; } } } else if (t == 14 && nb >= 36) { /* ramp lo/hi RGB */ VRamp &r = S.ramp[name]; for (int i = 0; i < 3; i++) { r.lo[i] = rd_f32(d + 8 + i * 4); r.hi[i] = rd_f32(d + 20 + i * 4); } } 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); } else if (t == 5 && nb >= 132) { /* dcs: 4x4 at f[4..19] */ M16 &mm = S.dcs_mat[name]; for (int i = 0; i < 16; i++) mm.m[i] = rd_f32(d + 16 + i * 4); for (int r = 0; r < 3; r++) mm.m[r * 4 + 3] = 0.0f; mm.m[15] = 1.0f; } else if (t == 4) { /* instance: object ref */ for (size_t o = 8; o + 3 < nb; o += 4) { unsigned val = rd_u32(d + o); if (val && val != 0xFFFFFFFFu) { std::map::const_iterator ti = S.type.find(val); if (ti != S.type.end() && ti->second == 7) S.inst_object[name] = val; } } } break; } case 7: /* dcs_link [parent][child]: articulation tree */ if (nb >= 8) S.dcs_parent[rd_u32(d + 4)] = rd_u32(d); 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: [name][0][n_verts][stride_floats].. */ if (nb >= 36) { S.geom_node = rd_u32(d); S.geom_need = rd_u32(d + 8); S.geom_stride = rd_u32(d + 12); if (S.geom_stride < 3 || S.geom_stride > 16) S.geom_stride = 3; S.geom_active = S.geom_need > 0; S.geom_acc.clear(); 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 26: /* texel upload header [node][nbytes][w][h]... (8bpp) */ if (nb >= 16) { unsigned node = rd_u32(d), nbytes = rd_u32(d + 4); unsigned tw = rd_u32(d + 8), th = rd_u32(d + 12); if (nbytes && nbytes <= (1u << 20) && tw && th && tw <= 1024 && th <= 1024 && (size_t)tw * th <= nbytes) { S.tex_node = node; S.tex_need = nbytes; S.tex_active = true; S.tex_acc.clear(); VTex &t = S.tex[node]; t.w = (int)tw; t.h = (int)th; } } 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.inst_object.clear(); S.dcs_mat.clear(); S.dcs_parent.clear(); S.view = VFrame(); S.geom_active = false; S.conn_active = false; S.geom_acc.clear(); } 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 /* ================= VDB: VWE LBE4 video splitter board ==================== * * ISA card (I/O 0x300-0x31A) that taps the PC's Cirrus Logic VGA output off * the feature connector and fans the single framebuffer out to the six * secondary cockpit displays (5 mono + 1 color), dividing the pixel clock. * Register map from the driver (CODE/RP/MUNGA_L4/L4SVGA16.ASM + L4VB16.CPP; * "Adam's port decoder design" -- Adam G., VWE hardware): * * 0x300 / 0x308 / 0x310 three palette register groups, each VGA-DAC-like: * +0 write-address +1 data (R,G,B triplets) +2 pixel-mask +3 read-address * 0x319 write => splitter high-color clock divider OFF (VWE_HC_OFF) * 0x31A write => splitter high-color clock divider ON (VWE_HC_ON) * * The game only WRITES to the VDB (fire-and-forget, no status/ACK -- confirmed * from the driver and by the hardware owner). This device records the palette * contents + clock-divider state so the six-display encoding can be decoded * later; reads return 0xFF. Active when VPXLOG is set; logs to the same file. */ static const io_port_t VDB_BASE = 0x300; struct VDBPalette { unsigned char waddr, raddr, sub, mask; unsigned char ram[768]; }; static VDBPalette vdb_pal[3]; static bool vdb_splitter_on = false; /* lazy coalescing of palette data-byte writes so a 768-byte load is one line */ static int vdb_data_group = -1; static unsigned long vdb_data_count = 0; static int vdb_group_of(io_port_t off) { /* group index for 0x300/0x308/0x310 */ if (off <= 3) return 0; if (off >= 8 && off <= 11) return 1; if (off >= 16 && off <= 19) return 2; return -1; } static void vdb_flush_data(void) { if (vpx_fp && vdb_data_group >= 0 && vdb_data_count) { flush_run(); fprintf(vpx_fp, "# VDB pal%d loaded %lu data bytes\n", vdb_data_group, vdb_data_count); fflush(vpx_fp); } vdb_data_group = -1; vdb_data_count = 0; } static void vdb_note(const char *msg) { if (vpx_fp) { vdb_flush_data(); flush_run(); fprintf(vpx_fp, "# VDB %s\n", msg); fflush(vpx_fp); } } static void vdb_write(Bitu port, Bitu val, Bitu /*iolen*/) { io_port_t off = (io_port_t)port - VDB_BASE; unsigned char v = (unsigned char)val; if (port == 0x319) { vdb_splitter_on = false; vdb_note("splitter clock OFF (0x319)"); return; } if (port == 0x31A) { vdb_splitter_on = true; vdb_note("splitter clock ON (0x31A)"); return; } int g = vdb_group_of(off); if (g < 0) return; VDBPalette &p = vdb_pal[g]; switch (off & 3) { case 0: /* write-address */ vdb_flush_data(); p.waddr = v; p.sub = 0; if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# VDB pal%d write-addr=%u\n", g, v); fflush(vpx_fp); } break; case 1: /* data: R,G,B triplet with auto-increment */ p.ram[((unsigned)p.waddr * 3 + p.sub) % 768] = v; if (++p.sub == 3) { p.sub = 0; p.waddr++; } if (vdb_data_group != g) { vdb_flush_data(); vdb_data_group = g; } vdb_data_count++; break; case 2: /* pixel-mask */ p.mask = v; vdb_note("pixel-mask set"); break; case 3: /* read-address */ p.raddr = v; p.sub = 0; vdb_note("read-addr set"); break; } } static Bitu vdb_read(Bitu port, Bitu /*iolen*/) { /* the game does not read the VDB; provide DAC-style read-back anyway. */ io_port_t off = (io_port_t)port - VDB_BASE; int g = vdb_group_of(off); if (g >= 0 && (off & 3) == 1) { VDBPalette &p = vdb_pal[g]; unsigned char v = p.ram[((unsigned)p.raddr * 3 + p.sub) % 768]; if (++p.sub == 3) { p.sub = 0; p.raddr++; } return v; } return 0xFF; } static void vdb_reset(void) { memset(vdb_pal, 0, sizeof vdb_pal); vdb_splitter_on = false; vdb_data_group = -1; vdb_data_count = 0; } void VPXLOG_Init(void) { const char *env = getenv("VPXLOG"); if (env == NULL || env[0] == '\0') return; const char *path = (strchr(env, '/') || strchr(env, '\\') || strchr(env, '.')) ? env : "vpxlog.txt"; vpx_fp = fopen(path, "w"); if (vpx_fp == NULL) { LOG_MSG("VPXLOG: cannot open '%s'", path); } const char *r = getenv("VPX_RESPOND"); vpx_respond = (r && r[0] && r[0] != '0'); const char *h = getenv("VPX_HANDSHAKES"); if (h && atoi(h) > 0) vpx_max_handshakes = atoi(h); const char *ma = getenv("VPX_MAX_ACKS"); if (ma && atoi(ma) > 0) vpx_max_postboot_acks = atoi(ma); const char *fd = getenv("VPX_FIFODUMP"); if (fd && fd[0]) { fifo_dump_fp = fopen(fd, "wb"); 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); /* VDB video splitter board (0x300-0x31A). Registered whenever we are * logging, unless VDB=0. Records palette + splitter-clock state. */ const char *vdbenv = getenv("VDB"); if (!(vdbenv && vdbenv[0] == '0')) { vdb_reset(); IO_RegisterReadHandler(VDB_BASE, vdb_read, IO_MB, 0x1B); /* 0x300-0x31A */ IO_RegisterWriteHandler(VDB_BASE, vdb_write, IO_MB, 0x1B); if (vpx_fp) { fprintf(vpx_fp, "# VDB video splitter board at 0x%03X-0x31A (palettes 300/308/310, clock 319/31A)\n", VDB_BASE); fflush(vpx_fp); } } if (vpx_fp) { fprintf(vpx_fp, "# VPX link adapter, base 0x%03X, respond=%d handshakes=%d\n", VPX_BASE, vpx_respond ? 1 : 0, vpx_max_handshakes); fprintf(vpx_fp, "# seq dir register value [run]\n"); fflush(vpx_fp); } LOG_MSG("VPXLOG: base 0x%03X respond=%d handshakes=%d log='%s'", VPX_BASE, vpx_respond ? 1 : 0, vpx_max_handshakes, vpx_fp ? path : "(none)"); }