/* 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\n", wf_action, wf_last_nb); fflush(vpx_fp); } wf_payload_left = -1; } } } /* 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; static int vpx_max_postboot_acks = 40; /* safety cap; tune via env */ static void queue_render_ack(unsigned char action) { unsigned char m[12] = { 0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */ action, 0x00, 0x00, 0x00, /* payload[0..3] = action (LE) */ 0x00, 0x00, 0x00, 0x00 /* payload[4..7] = node (unused) */ }; memcpy(in_fifo, m, sizeof m); in_len = (int)sizeof m; in_pos = 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 */ } 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) { /* game is reading -> expects a renderer reply * (first is the vr_init ack after i860 boot). */ if (postboot_acks < vpx_max_postboot_acks) { queue_render_ack((unsigned char)(last_action & 0xff)); postboot_acks++; if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# post-boot: echo action %u\n", last_action); fflush(vpx_fp); } 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; note("board reset"); } else if (off == 1) { saw_write = true; /* outputData: a download/response byte */ if (parse_frames) parse_out_byte((unsigned char)val); } } 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); IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18); IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18); 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)"); }