RP's cockpit displays decoded near-empty (top-strip only, a copy off-screen at VRAM 0x100000) while BT's were full. Root cause was NOT the VDB decode (byte split/palettes/stride all correct, proven by BT) but an SVGA bank-switch GRANULARITY mismatch: RP's REL410/RP/GAUGE/L4GAUGE.INI [640x480x16] used granularityInKB=4 vs BT's 64 on the same STB Horizon+ (CL-GD5434). DOSBox-X's CL-GD5434 bank emulation handles 64KB granularity (BT full) but mishandles the 4KB-granular paged gauge writes, scattering them off-screen. FIX (config, no rebuild): RP L4GAUGE.INI granularityInKB 4->64 (matches BT, same card). VERIFIED live: VRAM content scan went from 0x0+0x100000 (top-strip + off-screen) to the FULL framebuffer 0x0..0x90000 populated; radar + upper-center MFD confirmed correct, rest under reference check. Also commits the VDB diagnostic tooling that found it (vpxlog.cpp, all default-off): VDB_SCAN (VRAM content locator -- decisive), VDB_PALDUMP, pixel-mask value logging, VDB_REALSTRIDE + live mode/stride/start logging, VDB_BASE read-base override, VDB_APPLYMASK DAC-mask honoring. Full writeup in VDB-NOTES.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2695 lines
128 KiB
C++
2695 lines
128 KiB
C++
/* 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=<path> Phase 1: log every access to <path>.
|
|
* 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.
|
|
*/
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <winsock2.h> /* VPX_FIFOSOCK live tee; must precede windows.h */
|
|
#endif
|
|
|
|
#include "dosbox.h"
|
|
#include "inout.h"
|
|
#include "logging.h"
|
|
#include "mem.h"
|
|
#include "vga.h"
|
|
#include "../ints/int10.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
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;
|
|
|
|
/* ---- reticle pick (weapons-fire target gate) ---------------------------- *
|
|
* The game runs a continuous reticle intersection test: it arms it once with
|
|
* set_sect_pixel (action 38) at screen (0.5,0.5), then every frame reads the
|
|
* result -- hit instance + DCS + 3D intersection point -- which the board is
|
|
* expected to return piggybacked on the draw_scene (action 9) reply. The host
|
|
* (dpl_RapidSectPixel @0x4903c8) copies that into globals; targetReticle.
|
|
* targetEntity = the hit DCS's app-specific pointer. That entity IS the fire
|
|
* gate the weapon state machine checks (mech+0x388): no hit => targetEntity
|
|
* NULL => every weapon misfires ("boo-beep"). A real i860 answered this each
|
|
* frame; our HLE never did. We now carry a real scene hit in the frame ack. */
|
|
static bool sect_armed = false;
|
|
static unsigned sect_mask = 0xffffffffu; /* query mask from the 0x26 arm */
|
|
/* Real reticle raycast (Moller-Trumbore against the live scene, center ray). */
|
|
static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out,
|
|
unsigned *geom_out, float xyz_out[3]);
|
|
|
|
/* ---- 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=<path>) -------------- *
|
|
* 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) */
|
|
|
|
/* ---- live socket tee (VPX_FIFOSOCK=<port>) ------------------------------ *
|
|
* Streams the same VPXM records to one localhost TCP client (the dpl3-revive
|
|
* bridge) with TCP_NODELAY -- removes the fifodump file-poll quantum from the
|
|
* wire-to-photon path. All calls are non-blocking on the emulation thread: a
|
|
* stalled client gets a bounded pending buffer, then is dropped (the bridge
|
|
* reconnects; vehicle poses are absolute so a brief gap self-heals). */
|
|
#ifdef _WIN32
|
|
static SOCKET fifo_lsock = INVALID_SOCKET; /* listener */
|
|
static SOCKET fifo_csock = INVALID_SOCKET; /* single client */
|
|
static unsigned char *fifo_pend = NULL; /* client-stalled unsent tail */
|
|
static size_t fifo_pend_len = 0, fifo_pend_cap = 0;
|
|
|
|
static bool fifo_sock_active(void) { return fifo_lsock != INVALID_SOCKET; }
|
|
|
|
static void fifo_sock_init(int port) {
|
|
WSADATA wd;
|
|
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0) return;
|
|
fifo_lsock = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fifo_lsock == INVALID_SOCKET) return;
|
|
sockaddr_in a; memset(&a, 0, sizeof a);
|
|
a.sin_family = AF_INET;
|
|
a.sin_port = htons((u_short)port);
|
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
u_long nb = 1;
|
|
if (bind(fifo_lsock, (sockaddr *)&a, sizeof a) != 0 ||
|
|
listen(fifo_lsock, 1) != 0 ||
|
|
ioctlsocket(fifo_lsock, FIONBIO, &nb) != 0) {
|
|
closesocket(fifo_lsock); fifo_lsock = INVALID_SOCKET;
|
|
LOG_MSG("VPXLOG: fifosock: cannot listen on 127.0.0.1:%d", port);
|
|
return;
|
|
}
|
|
LOG_MSG("VPXLOG: fifosock listening on 127.0.0.1:%d", port);
|
|
}
|
|
|
|
static void fifo_sock_drop(void) {
|
|
if (fifo_csock != INVALID_SOCKET) closesocket(fifo_csock);
|
|
fifo_csock = INVALID_SOCKET;
|
|
fifo_pend_len = 0;
|
|
}
|
|
|
|
static void fifo_sock_accept(void) {
|
|
if (fifo_lsock == INVALID_SOCKET) return;
|
|
SOCKET c = accept(fifo_lsock, NULL, NULL);
|
|
if (c == INVALID_SOCKET) return; /* WOULDBLOCK: nobody waiting */
|
|
fifo_sock_drop();
|
|
u_long nb = 1; ioctlsocket(c, FIONBIO, &nb);
|
|
int v = 1;
|
|
setsockopt(c, IPPROTO_TCP, TCP_NODELAY, (const char *)&v, sizeof v);
|
|
v = 1 << 20;
|
|
setsockopt(c, SOL_SOCKET, SO_SNDBUF, (const char *)&v, sizeof v);
|
|
fifo_csock = c;
|
|
LOG_MSG("VPXLOG: fifosock client connected");
|
|
}
|
|
|
|
static void fifo_pend_stash(const unsigned char *p, size_t n) {
|
|
if (fifo_pend_len + n > (8u << 20)) { fifo_sock_drop(); return; }
|
|
if (fifo_pend_len + n > fifo_pend_cap) {
|
|
size_t ncap = fifo_pend_cap ? fifo_pend_cap : 65536;
|
|
while (ncap < fifo_pend_len + n) ncap *= 2;
|
|
unsigned char *np = (unsigned char *)realloc(fifo_pend, ncap);
|
|
if (np == NULL) { fifo_sock_drop(); return; }
|
|
fifo_pend = np; fifo_pend_cap = ncap;
|
|
}
|
|
memcpy(fifo_pend + fifo_pend_len, p, n);
|
|
fifo_pend_len += n;
|
|
}
|
|
|
|
static void fifo_sock_write(const unsigned char *p, size_t n) {
|
|
if (fifo_csock == INVALID_SOCKET) return;
|
|
/* drain any stalled tail first (framing must stay contiguous) */
|
|
while (fifo_pend_len) {
|
|
int r = send(fifo_csock, (const char *)fifo_pend,
|
|
(int)(fifo_pend_len > 65536 ? 65536 : fifo_pend_len), 0);
|
|
if (r > 0) {
|
|
memmove(fifo_pend, fifo_pend + r, fifo_pend_len - (size_t)r);
|
|
fifo_pend_len -= (size_t)r;
|
|
} else if (r == SOCKET_ERROR &&
|
|
WSAGetLastError() == WSAEWOULDBLOCK) {
|
|
fifo_pend_stash(p, n); return;
|
|
} else { fifo_sock_drop(); return; }
|
|
}
|
|
size_t off = 0;
|
|
while (off < n) {
|
|
int r = send(fifo_csock, (const char *)(p + off),
|
|
(int)((n - off) > 65536 ? 65536 : (n - off)), 0);
|
|
if (r > 0) off += (size_t)r;
|
|
else if (r == SOCKET_ERROR &&
|
|
WSAGetLastError() == WSAEWOULDBLOCK) {
|
|
fifo_pend_stash(p + off, n - off); return;
|
|
} else { fifo_sock_drop(); return; }
|
|
}
|
|
}
|
|
|
|
/* bridge -> device backchannel on the same socket. The bridge streams its
|
|
* camera pose (the user-validated cockpit view: cam chain + torso twist +
|
|
* hat glance) as text lines: CAM ex ey ez dx dy dz\n
|
|
* raycast_pick prefers this over the device's own S.view decode -- S.view
|
|
* tracks the static view-node pose (~arena origin, not the player's aim),
|
|
* which pointed every reticle pick, and therefore every missile volley and
|
|
* laser beam, at one fixed wrong world point. */
|
|
static char brcam_line[192];
|
|
static size_t brcam_len = 0;
|
|
static float brcam[6];
|
|
static bool brcam_valid = false;
|
|
static DWORD brcam_tick = 0;
|
|
static unsigned brcam_root = 0; /* player vehicle root DCS (7th CAM field) */
|
|
|
|
static void fifo_sock_poll(void) {
|
|
if (fifo_csock == INVALID_SOCKET) return;
|
|
char buf[512];
|
|
for (;;) {
|
|
int r = recv(fifo_csock, buf, (int)sizeof buf, 0);
|
|
if (r == 0) { fifo_sock_drop(); return; } /* client closed */
|
|
if (r < 0) {
|
|
if (WSAGetLastError() != WSAEWOULDBLOCK) fifo_sock_drop();
|
|
return;
|
|
}
|
|
for (int i = 0; i < r; i++) {
|
|
char ch = buf[i];
|
|
if (ch == '\n') {
|
|
brcam_line[brcam_len] = 0;
|
|
float v[6]; unsigned rt = 0;
|
|
int nf = sscanf(brcam_line, "CAM %f %f %f %f %f %f %x",
|
|
&v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &rt);
|
|
if (nf >= 6) {
|
|
memcpy(brcam, v, sizeof brcam);
|
|
brcam_valid = true;
|
|
brcam_tick = GetTickCount();
|
|
brcam_root = (nf >= 7) ? rt : 0;
|
|
}
|
|
brcam_len = 0;
|
|
} else if (brcam_len + 1 < sizeof brcam_line) {
|
|
brcam_line[brcam_len++] = ch;
|
|
} else {
|
|
brcam_len = 0; /* oversize: resync */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
static bool fifo_sock_active(void) { return false; }
|
|
static void fifo_sock_init(int) {}
|
|
static void fifo_sock_accept(void) {}
|
|
static void fifo_sock_write(const unsigned char *, size_t) {}
|
|
static void fifo_sock_poll(void) {}
|
|
#endif
|
|
|
|
/* Latest bridge camera, if fresh: the pick must not outlive a dead bridge
|
|
* by more than a couple of seconds (stale aim = the fixed-point bug again,
|
|
* just slower), so fall back to the device view after 2s of silence. */
|
|
static bool bridge_cam_get(float O[3], float D[3]) {
|
|
#ifdef _WIN32
|
|
if (brcam_valid && (GetTickCount() - brcam_tick) < 2000) {
|
|
O[0] = brcam[0]; O[1] = brcam[1]; O[2] = brcam[2];
|
|
D[0] = brcam[3]; D[1] = brcam[4]; D[2] = brcam[5];
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
static unsigned bridge_cam_root(void) {
|
|
#ifdef _WIN32
|
|
return brcam_root;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static void fifo_buf_push(unsigned char v) {
|
|
if (fifo_dump_fp == NULL && !vpx_render && !fifo_sock_active()) 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;
|
|
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) };
|
|
if (fifo_dump_fp) {
|
|
fwrite(hdr, 1, sizeof hdr, fifo_dump_fp);
|
|
fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp);
|
|
fflush(fifo_dump_fp);
|
|
}
|
|
if (fifo_sock_active()) {
|
|
fifo_sock_accept();
|
|
fifo_sock_poll(); /* drain bridge CAM backchannel */
|
|
fifo_sock_write(hdr, sizeof hdr);
|
|
fifo_sock_write(fifo_buf, fifo_buf_len);
|
|
}
|
|
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); }
|
|
|
|
static void wr_u32(unsigned char *p, unsigned v) {
|
|
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
|
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
|
}
|
|
/* A draw_scene (action 9) reply that also carries the reticle sect result, so
|
|
* velocirender_receive stores a live hit (see 0x491d08 / 0x4922e0 in BTL4OPT).
|
|
* The receive loop copies payload+4 into a host buffer; the store handler then
|
|
* reads instance @buf+0xc, xi/yi/zi @buf+0x10, DCS @buf+0x1c, gg @buf+0x20,
|
|
* geom @buf+0x24. In payload terms (buf == payload+4): action@0x00,
|
|
* instance@0x10, xyz@0x14, DCS@0x20, gg@0x24, geom@0x28. length_word = the
|
|
* payload byte count (matches queue_render_ack_node's nb=8 convention). */
|
|
static void queue_sect_frame_reply(unsigned inst, unsigned dcs,
|
|
unsigned gg, unsigned geom,
|
|
const float xyz[3]) {
|
|
fifo_flush_record(); /* a receive means the outstanding burst is done */
|
|
unsigned char *m = in_fifo;
|
|
memset(m, 0, sizeof in_fifo);
|
|
const unsigned nb = 0x2c; /* payload = action(4) + 40 = 44 bytes */
|
|
wr_u32(m, nb); /* length_word (non-iserver: nb) */
|
|
unsigned char *pl = m + 4;
|
|
wr_u32(pl + 0x00, 9); /* action = vr_draw_scene_action */
|
|
/* pl+0x04/0x08/0x0c: debug echo words (left zero) */
|
|
wr_u32(pl + 0x10, inst); /* hit instance handle (type 4) */
|
|
unsigned u;
|
|
memcpy(&u, &xyz[0], 4); wr_u32(pl + 0x14, u); /* xi */
|
|
memcpy(&u, &xyz[1], 4); wr_u32(pl + 0x18, u); /* yi */
|
|
memcpy(&u, &xyz[2], 4); wr_u32(pl + 0x1c, u); /* zi */
|
|
wr_u32(pl + 0x20, dcs); /* hit DCS handle (type 5) */
|
|
wr_u32(pl + 0x24, gg); /* hit geogroup handle (type 9) --
|
|
* the game does GetAppSpecific(gg) on
|
|
* a hit; sending 0 handed it a null. */
|
|
wr_u32(pl + 0x28, geom); /* hit geometry handle (type 0xa) */
|
|
in_len = (int)(4 + nb); /* 48 bytes */
|
|
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 */
|
|
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 draw_scene (9)
|
|
* reply; we carry the reticle sect result on it
|
|
* so weapons acquire a target (else every shot
|
|
* misfires). The pick runs whenever the game has
|
|
* armed the reticle (sect_armed); VPX_NO_PICK=1
|
|
* is the only escape hatch (falls back to the
|
|
* plain ack -- the pre-pick baseline). */
|
|
frame_outstanding = false;
|
|
static int no_pick = -1;
|
|
if (no_pick < 0)
|
|
no_pick = getenv("VPX_NO_PICK") ? 1 : 0;
|
|
unsigned pinst = 0, pdcs = 0, pgg = 0, pgeom = 0;
|
|
float pxyz[3] = { 0, 0, 0 };
|
|
if (!no_pick && sect_armed) {
|
|
/* ALWAYS answer the armed pick, hit or
|
|
* miss. A miss = all-zero reply: the game
|
|
* clears targetEntity (L4VIDEO: no hit ->
|
|
* NULL) so aiming at open sky un-targets
|
|
* instead of freezing the LAST hit -- the
|
|
* old hit-only path made the aim go STALE
|
|
* whenever the ray cleared the arena
|
|
* walls (plain acks carry no sect data,
|
|
* so the game kept seconds-old targets). */
|
|
bool hit = raycast_pick(&pinst, &pdcs,
|
|
&pgg, &pgeom, pxyz);
|
|
queue_sect_frame_reply(pinst, pdcs, pgg,
|
|
pgeom, pxyz);
|
|
if (vpx_fp) { flush_run();
|
|
fprintf(vpx_fp, "# post-boot: frame ack "
|
|
"(9) + sect %s inst=%08X\n",
|
|
hit ? "hit" : "MISS", pinst);
|
|
fflush(vpx_fp); }
|
|
} else {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* VDB display-palette storage (shared: written by the VDB I/O handler on the
|
|
* emulator thread, read by the GL thread for the live palette strips).
|
|
* Declared here so rt_draw() below can see it. */
|
|
struct VDBPalette { unsigned char waddr, raddr, sub, mask; unsigned char ram[768]; };
|
|
static VDBPalette vdb_pal[3];
|
|
|
|
/* ================= 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 <windows.h>
|
|
#include <GL/gl.h>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <math.h>
|
|
|
|
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]; };
|
|
/* Full dpl_MATERIAL shading params. The shading model is ported from the
|
|
* restoration viewer (restoration/gallery_template.html meshShader), proven
|
|
* against the recovered .SCN scenes:
|
|
* acc = sum_i lightCol_i * max(dot(N, -lightDir_i), 0)
|
|
* c = (amb*sceneAmb + diff*mix(ramp.lo, ramp.hi, acc)) * tex + emis */
|
|
struct VMatG {
|
|
float amb[3], diff[3], emis[3];
|
|
float opacity, spec[3], shin;
|
|
VMatG() : opacity(1), shin(0) {
|
|
for (int i = 0; i < 3; i++) {
|
|
amb[i] = 0.6f; diff[i] = 0.6f; emis[i] = 0.0f; spec[i] = 0.0f;
|
|
}
|
|
}
|
|
};
|
|
struct VPoly {
|
|
float rgb[3]; /* material diffuse */
|
|
float amb[3], emis[3]; /* material ambient / emissive */
|
|
float ramp0[3], ramp1[3]; /* shading ramp lo/hi (identity when the
|
|
* ramp is already baked into the texture) */
|
|
std::vector<float> xyz; /* x,y,z triples */
|
|
std::vector<float> uv; /* u,v pairs (empty = untextured) */
|
|
std::vector<float> nrm; /* nx,ny,nz triples (empty = flat normal) */
|
|
unsigned matkey; /* material name for texture lookup, 0 = none */
|
|
VPoly() : matkey(0) {
|
|
for (int i = 0; i < 3; i++) {
|
|
amb[i] = 0.6f; emis[i] = 0.0f; ramp0[i] = 0.0f; ramp1[i] = 1.0f;
|
|
}
|
|
}
|
|
};
|
|
/* 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<unsigned char> rgba; };
|
|
struct VFrame {
|
|
bool valid;
|
|
float bg[3];
|
|
float win[5]; /* l, b, r, t, window-plane distance */
|
|
float nearp, farp;
|
|
bool fog; /* dpl_SetViewFog: linear haze near..far */
|
|
float fogn, fogf, fogc[3];
|
|
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) */
|
|
/* real wire lights (dpl3-revive: lmodel type-2 ambient + type-3 dir suns).
|
|
* samb = summed scene ambient; nlit dir lights (dir toward light + col). */
|
|
float samb[3];
|
|
int nlit;
|
|
float ldir[2][3], lcol[2][3];
|
|
std::vector<VPoly> polys;
|
|
VFrame() : valid(false), nearp(2), farp(12000), fog(false),
|
|
fogn(0), fogf(0), vw(832), vh(512),
|
|
has_cam(false), ydown(false), nlit(0) {
|
|
bg[0] = bg[1] = bg[2] = 0;
|
|
fogc[0] = fogc[1] = fogc[2] = 0;
|
|
win[0] = -1; win[1] = -0.6153846f; win[2] = 1; win[3] = 0.6153846f;
|
|
win[4] = 1.3f;
|
|
samb[0] = samb[1] = samb[2] = 0.0f;
|
|
}
|
|
};
|
|
|
|
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<unsigned char> px; };
|
|
|
|
static struct VScene {
|
|
std::map<unsigned, unsigned> type; /* name -> node type */
|
|
std::map<unsigned, std::vector<float> > verts; /* geometry -> xyz */
|
|
std::map<unsigned, std::vector<float> > uvs; /* geometry -> u,v */
|
|
std::map<unsigned, std::vector<float> > nrms; /* geometry -> nx,ny,nz */
|
|
std::map<unsigned, std::vector<std::vector<int> > > polys;
|
|
std::map<unsigned, VMatG> mat; /* material -> shading */
|
|
std::map<unsigned, unsigned> ggmat; /* geogroup -> material */
|
|
std::map<unsigned, std::vector<unsigned> > children;
|
|
/* game (full DPL) hierarchy: instance placement + articulation */
|
|
std::map<unsigned, unsigned> inst_object; /* instance -> object */
|
|
std::map<unsigned, unsigned> inst_w3; /* instance -> display mode word3 */
|
|
std::map<unsigned, unsigned> inst_mask; /* instance -> INTERSECT mask */
|
|
std::map<unsigned, M16> dcs_mat; /* dcs -> local matrix */
|
|
std::map<unsigned, unsigned> dcs_parent; /* dcs_link child->parent */
|
|
/* wire lights: dpl3-revive decode -- lmodel (type 6) / light (type 0xe)
|
|
* body: dcs @d+12, light_type @d+16 (2=ambient, 3=directional), rgb
|
|
* @d+20..28. directional aim = the light DCS's +Z world row. */
|
|
struct VLight { unsigned dcs; int ltype; float rgb[3]; };
|
|
std::map<unsigned, VLight> lights; /* light node -> params */
|
|
/* texture chain: material -> texmap -> texture, colorized by the
|
|
* material's ramp (type 14: lo/hi RGB; texels are intensities) */
|
|
std::map<unsigned, VTex> tex; /* texture node -> texels */
|
|
std::map<unsigned, unsigned> texmap_tex; /* texmap -> texture */
|
|
std::map<unsigned, unsigned> mat_texmap; /* material -> texmap */
|
|
std::map<unsigned, unsigned> mat_ramp; /* material -> ramp */
|
|
std::map<unsigned, VRamp> 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<float> geom_acc;
|
|
unsigned conn_node, conn_npolys, conn_loop; bool conn_active;
|
|
std::vector<int> conn_acc; /* whole-batch index staging */
|
|
/* action-26 texel upload assembly */
|
|
unsigned tex_node; size_t tex_need; bool tex_active;
|
|
std::vector<unsigned char> 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];
|
|
}
|
|
static void m16_xform_dir(const M16 &w, const float *v, float *o) {
|
|
for (int c = 0; c < 3; c++) /* rotation only (normals) */
|
|
o[c] = v[0] * w.m[c] + v[1] * w.m[4 + c] + v[2] * w.m[8 + c];
|
|
}
|
|
/* world transform of a dcs: local * parent_world (row-vector convention) */
|
|
static void dcs_world(unsigned dcs, std::map<unsigned, M16> &cache, M16 &out,
|
|
int depth = 0) {
|
|
std::map<unsigned, M16>::iterator ci = cache.find(dcs);
|
|
if (ci != cache.end()) { out = ci->second; return; }
|
|
M16 local;
|
|
std::map<unsigned, M16>::iterator mi = S.dcs_mat.find(dcs);
|
|
if (mi != S.dcs_mat.end()) local = mi->second; else m16_id(local);
|
|
std::map<unsigned, unsigned>::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<unsigned, TexImg> rt_texs; /* material -> baked RGBA (rt_lock) */
|
|
|
|
/* ---- GLSL port of the restoration viewer's shading model ----------------
|
|
* Source: restoration/gallery_template.html meshShader (proven against the
|
|
* recovered .SCN scenes). Per fragment:
|
|
* acc = sum_i lightCol_i * max(dot(N, -lightDir_i), 0) (world space)
|
|
* lit = mix(ramp.lo, ramp.hi, clamp(acc, 0, 1))
|
|
* c = (matAmb*sceneAmb + matDiff*lit) * tex + matEmis
|
|
* out = mix(fogColor, c, max(clamp((end-d)/(end-start),0,1), immune))
|
|
* Geometry reaches GL already in world space (modelview = camera only), so
|
|
* normals and light directions stay in world coordinates like the gallery.
|
|
* Falls back to the old fixed-function path when GLSL is unavailable. */
|
|
#ifndef GL_FRAGMENT_SHADER
|
|
#define GL_FRAGMENT_SHADER 0x8B30
|
|
#endif
|
|
#ifndef GL_VERTEX_SHADER
|
|
#define GL_VERTEX_SHADER 0x8B31
|
|
#endif
|
|
#ifndef GL_COMPILE_STATUS
|
|
#define GL_COMPILE_STATUS 0x8B81
|
|
#endif
|
|
#ifndef GL_LINK_STATUS
|
|
#define GL_LINK_STATUS 0x8B82
|
|
#endif
|
|
typedef GLuint (APIENTRY *pfnCreateShader)(GLenum);
|
|
typedef void (APIENTRY *pfnShaderSource)(GLuint, GLsizei, const char **,
|
|
const GLint *);
|
|
typedef void (APIENTRY *pfnCompileShader)(GLuint);
|
|
typedef void (APIENTRY *pfnGetShaderiv)(GLuint, GLenum, GLint *);
|
|
typedef void (APIENTRY *pfnGetShaderInfoLog)(GLuint, GLsizei, GLsizei *,
|
|
char *);
|
|
typedef GLuint (APIENTRY *pfnCreateProgram)(void);
|
|
typedef void (APIENTRY *pfnAttachShader)(GLuint, GLuint);
|
|
typedef void (APIENTRY *pfnLinkProgram)(GLuint);
|
|
typedef void (APIENTRY *pfnGetProgramiv)(GLuint, GLenum, GLint *);
|
|
typedef void (APIENTRY *pfnUseProgram)(GLuint);
|
|
typedef GLint (APIENTRY *pfnGetUniformLocation)(GLuint, const char *);
|
|
typedef void (APIENTRY *pfnUniform3f)(GLint, GLfloat, GLfloat, GLfloat);
|
|
typedef void (APIENTRY *pfnUniform1f)(GLint, GLfloat);
|
|
typedef void (APIENTRY *pfnUniform1i)(GLint, GLint);
|
|
|
|
static struct RShade {
|
|
bool tried; GLuint prog;
|
|
pfnUseProgram use; pfnUniform3f u3f; pfnUniform1f u1f;
|
|
GLint uAmbScene, uMatAmb, uMatDiff, uMatEmis, uFogColor, uViewFwd;
|
|
GLint uLightDir0, uLightCol0, uLightDir1, uLightCol1;
|
|
GLint uRamp0, uRamp1, uFog, uHasTex;
|
|
} rsh = { false, 0, NULL, NULL, NULL,
|
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
|
|
|
|
static const char *rsh_vs_src =
|
|
"varying vec3 vN; varying vec2 vUV; varying float vDist;\n"
|
|
"void main() {\n"
|
|
" vec4 v = gl_ModelViewMatrix * gl_Vertex;\n"
|
|
" vDist = length(v.xyz);\n"
|
|
" vN = gl_Normal;\n"
|
|
" vUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;\n"
|
|
" gl_Position = gl_ProjectionMatrix * v;\n"
|
|
"}\n";
|
|
/* Shading model ported 1:1 from the dpl3-revive software rasterizer
|
|
* (patha/vrview.py draw(), validated against our own arena captures):
|
|
* lit = sceneAmbient + sum_i |dot(N, Ldir_i)| * Lcol_i (DPL double-sided)
|
|
* base = matEmis + matDiff * lit
|
|
* c = hasTex ? tex * base * 1.275 : base (tex*base/200, base was *255)
|
|
* out = pow(fog(c), 1/1.25) (Division 10-bit-DAC gamma)
|
|
* Material ambient and the light-ramp are intentionally unused here -- the
|
|
* proven Division look folds ambient into the diffuse response and does not
|
|
* ramp the base colour (the ramp only re-colours intensity texels at bake). */
|
|
static const char *rsh_fs_src =
|
|
"varying vec3 vN; varying vec2 vUV; varying float vDist;\n"
|
|
"uniform vec3 uAmbScene, uMatDiff, uMatEmis, uFogColor;\n"
|
|
"uniform vec3 uLightDir0, uLightCol0, uLightDir1, uLightCol1;\n"
|
|
"uniform vec3 uFog;\n" /* uFog: start, end, immune */
|
|
"uniform float uHasTex; uniform sampler2D uTex;\n"
|
|
"void main() {\n"
|
|
" vec3 N = normalize(vN);\n"
|
|
" vec3 lit = uAmbScene\n"
|
|
" + uLightCol0 * abs(dot(N, uLightDir0))\n"
|
|
" + uLightCol1 * abs(dot(N, uLightDir1));\n"
|
|
" vec3 base = uMatEmis + uMatDiff * lit;\n"
|
|
" vec3 tex = texture2D(uTex, vUV).rgb;\n"
|
|
" vec3 c = mix(base, tex * base * 1.275, uHasTex);\n"
|
|
" float fr = clamp((uFog.y - vDist) / max(uFog.y - uFog.x, 0.001),\n"
|
|
" 0.0, 1.0);\n"
|
|
" fr = max(fr, uFog.z);\n"
|
|
" c = mix(uFogColor, c, fr);\n"
|
|
" gl_FragColor = vec4(pow(clamp(c, 0.0, 1.0), vec3(1.0/1.25)), 1.0);\n"
|
|
"}\n";
|
|
|
|
static GLuint rsh_compile(GLenum kind, const char *src,
|
|
pfnCreateShader cs, pfnShaderSource ss,
|
|
pfnCompileShader cc, pfnGetShaderiv gi,
|
|
pfnGetShaderInfoLog il) {
|
|
GLuint s = cs(kind);
|
|
ss(s, 1, &src, NULL);
|
|
cc(s);
|
|
GLint ok = 0;
|
|
gi(s, GL_COMPILE_STATUS, &ok);
|
|
if (!ok) {
|
|
char log[512]; GLsizei n = 0;
|
|
il(s, sizeof log, &n, log);
|
|
fprintf(stderr, "VPX render: shader compile failed: %.*s\n",
|
|
(int)n, log);
|
|
return 0;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static void rsh_init(void) {
|
|
rsh.tried = true;
|
|
pfnCreateShader cs = (pfnCreateShader)wglGetProcAddress("glCreateShader");
|
|
pfnShaderSource ss = (pfnShaderSource)wglGetProcAddress("glShaderSource");
|
|
pfnCompileShader cc = (pfnCompileShader)wglGetProcAddress("glCompileShader");
|
|
pfnGetShaderiv gi = (pfnGetShaderiv)wglGetProcAddress("glGetShaderiv");
|
|
pfnGetShaderInfoLog il = (pfnGetShaderInfoLog)wglGetProcAddress("glGetShaderInfoLog");
|
|
pfnCreateProgram cp = (pfnCreateProgram)wglGetProcAddress("glCreateProgram");
|
|
pfnAttachShader as = (pfnAttachShader)wglGetProcAddress("glAttachShader");
|
|
pfnLinkProgram lp = (pfnLinkProgram)wglGetProcAddress("glLinkProgram");
|
|
pfnGetProgramiv gp = (pfnGetProgramiv)wglGetProcAddress("glGetProgramiv");
|
|
pfnGetUniformLocation gu = (pfnGetUniformLocation)wglGetProcAddress("glGetUniformLocation");
|
|
pfnUniform1i u1i = (pfnUniform1i)wglGetProcAddress("glUniform1i");
|
|
rsh.use = (pfnUseProgram)wglGetProcAddress("glUseProgram");
|
|
rsh.u3f = (pfnUniform3f)wglGetProcAddress("glUniform3f");
|
|
rsh.u1f = (pfnUniform1f)wglGetProcAddress("glUniform1f");
|
|
if (!cs || !ss || !cc || !gi || !il || !cp || !as || !lp || !gp || !gu ||
|
|
!u1i || !rsh.use || !rsh.u3f || !rsh.u1f) {
|
|
fprintf(stderr, "VPX render: GLSL entry points unavailable, "
|
|
"using fixed-function fallback\n");
|
|
return;
|
|
}
|
|
GLuint vs = rsh_compile(GL_VERTEX_SHADER, rsh_vs_src, cs, ss, cc, gi, il);
|
|
GLuint fs = rsh_compile(GL_FRAGMENT_SHADER, rsh_fs_src, cs, ss, cc, gi, il);
|
|
if (!vs || !fs) return;
|
|
GLuint pr = cp();
|
|
as(pr, vs); as(pr, fs);
|
|
lp(pr);
|
|
GLint ok = 0;
|
|
gp(pr, GL_LINK_STATUS, &ok);
|
|
if (!ok) {
|
|
fprintf(stderr, "VPX render: shader link failed, "
|
|
"using fixed-function fallback\n");
|
|
return;
|
|
}
|
|
rsh.uAmbScene = gu(pr, "uAmbScene");
|
|
rsh.uMatAmb = gu(pr, "uMatAmb");
|
|
rsh.uMatDiff = gu(pr, "uMatDiff");
|
|
rsh.uMatEmis = gu(pr, "uMatEmis");
|
|
rsh.uFogColor = gu(pr, "uFogColor");
|
|
rsh.uViewFwd = gu(pr, "uViewFwd");
|
|
rsh.uLightDir0 = gu(pr, "uLightDir0");
|
|
rsh.uLightCol0 = gu(pr, "uLightCol0");
|
|
rsh.uLightDir1 = gu(pr, "uLightDir1");
|
|
rsh.uLightCol1 = gu(pr, "uLightCol1");
|
|
rsh.uRamp0 = gu(pr, "uRamp0");
|
|
rsh.uRamp1 = gu(pr, "uRamp1");
|
|
rsh.uFog = gu(pr, "uFog");
|
|
rsh.uHasTex = gu(pr, "uHasTex");
|
|
rsh.use(pr);
|
|
u1i(gu(pr, "uTex"), 0);
|
|
rsh.use(0);
|
|
rsh.prog = pr;
|
|
fprintf(stderr, "VPX render: dpl3-revive shading model active (GLSL)\n");
|
|
}
|
|
|
|
/* scene lighting until the type-7 light node decode lands (step 2):
|
|
* defaults keep the previous hardcoded sun; VPX_AMBIENT="r,g,b" and
|
|
* VPX_SUN="r,g,b,pitch,yaw" (degrees, y-down world) override for tuning */
|
|
/* dpl3-revive default light rig (vrview.py): scene ambient 0.35 + one sun 0.65
|
|
* when the wire carries no lights (real light-node decode = next increment). */
|
|
static float rsh_amb[3] = { 0.35f, 0.35f, 0.35f };
|
|
static float rsh_sundir[3] = { -0.3412f, 0.8286f, -0.3899f }; /* travel dir */
|
|
static float rsh_suncol[3] = { 0.65f, 0.65f, 0.65f };
|
|
static void rsh_env(void) {
|
|
static bool done = false;
|
|
if (done) return;
|
|
done = true;
|
|
const char *a = getenv("VPX_AMBIENT");
|
|
if (a) sscanf(a, "%f,%f,%f", &rsh_amb[0], &rsh_amb[1], &rsh_amb[2]);
|
|
const char *s = getenv("VPX_SUN");
|
|
if (s) {
|
|
float p = -50.0f, y = 10.0f;
|
|
if (sscanf(s, "%f,%f,%f,%f,%f", &rsh_suncol[0], &rsh_suncol[1],
|
|
&rsh_suncol[2], &p, &y) >= 3) {
|
|
float pr = p * 3.14159265f / 180.0f, yr = y * 3.14159265f / 180.0f;
|
|
rsh_sundir[0] = -sinf(yr) * cosf(pr);
|
|
rsh_sundir[1] = -sinf(pr); /* world is y-down */
|
|
rsh_sundir[2] = -cosf(yr) * cosf(pr);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
|
|
/* VPX_CLEAR="r,g,b" (0-1 floats) overrides the wire back_color clear.
|
|
* Black makes decode gaps honest -- the game's sky-blue back_color reads
|
|
* as terrain-to-the-horizon when instances are missing. */
|
|
static int has_clear = -1;
|
|
static float cc[3];
|
|
if (has_clear < 0) {
|
|
const char *cv = getenv("VPX_CLEAR");
|
|
has_clear = (cv && sscanf(cv, "%f,%f,%f",
|
|
&cc[0], &cc[1], &cc[2]) == 3) ? 1 : 0;
|
|
}
|
|
glViewport(0, 0, cw, ch);
|
|
if (has_clear) glClearColor(cc[0], cc[1], cc[2], 1.0f);
|
|
else 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();
|
|
/* Demo/test path: Division screen x runs opposite to GL eye x
|
|
* (SMPTE pattern comes out mirrored otherwise) -- flip x after
|
|
* projection. Game path (ydown): NO x flip -- dpl3-revive's proven
|
|
* pipeline has no lateral mirror, and the flip made yaw read
|
|
* inverted (turn right, view swings left); the world's y-down DCS
|
|
* reflection still needs the y flip. */
|
|
glScalef(f.ydown ? 1.0f : -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);
|
|
glShadeModel(GL_SMOOTH);
|
|
/* distance haze (dpl_SetViewFog): linear, eye-distance based.
|
|
* VPX_NOFOG=1 disables (diagnostic). */
|
|
static int nofog = -1;
|
|
if (nofog < 0) {
|
|
const char *nf = getenv("VPX_NOFOG");
|
|
nofog = (nf && nf[0] && nf[0] != '0') ? 1 : 0;
|
|
}
|
|
bool fog_on = !nofog && f.fog && f.fogf > f.fogn;
|
|
if (!rsh.tried) { rsh_env(); rsh_init(); }
|
|
bool shade = rsh.prog != 0;
|
|
if (shade) {
|
|
/* gallery shading model: frame-level uniforms (world space) */
|
|
glDisable(GL_FOG);
|
|
glDisable(GL_LIGHTING);
|
|
rsh.use(rsh.prog);
|
|
/* real wire lights (lmodel) when the frame carries any; else the
|
|
* dpl3-revive default rig (scene ambient 0.35 + one sun 0.65). */
|
|
bool wl = f.nlit > 0 ||
|
|
(f.samb[0] + f.samb[1] + f.samb[2]) > 0.0f;
|
|
if (wl) {
|
|
rsh.u3f(rsh.uAmbScene, f.samb[0], f.samb[1], f.samb[2]);
|
|
rsh.u3f(rsh.uLightDir0,
|
|
f.nlit >= 1 ? f.ldir[0][0] : 0.0f,
|
|
f.nlit >= 1 ? f.ldir[0][1] : 1.0f,
|
|
f.nlit >= 1 ? f.ldir[0][2] : 0.0f);
|
|
rsh.u3f(rsh.uLightCol0,
|
|
f.nlit >= 1 ? f.lcol[0][0] : 0.0f,
|
|
f.nlit >= 1 ? f.lcol[0][1] : 0.0f,
|
|
f.nlit >= 1 ? f.lcol[0][2] : 0.0f);
|
|
rsh.u3f(rsh.uLightDir1,
|
|
f.nlit >= 2 ? f.ldir[1][0] : 0.0f,
|
|
f.nlit >= 2 ? f.ldir[1][1] : 1.0f,
|
|
f.nlit >= 2 ? f.ldir[1][2] : 0.0f);
|
|
rsh.u3f(rsh.uLightCol1,
|
|
f.nlit >= 2 ? f.lcol[1][0] : 0.0f,
|
|
f.nlit >= 2 ? f.lcol[1][1] : 0.0f,
|
|
f.nlit >= 2 ? f.lcol[1][2] : 0.0f);
|
|
} else {
|
|
rsh.u3f(rsh.uAmbScene, rsh_amb[0], rsh_amb[1], rsh_amb[2]);
|
|
rsh.u3f(rsh.uLightDir0, rsh_sundir[0], rsh_sundir[1],
|
|
rsh_sundir[2]);
|
|
rsh.u3f(rsh.uLightCol0, rsh_suncol[0], rsh_suncol[1],
|
|
rsh_suncol[2]);
|
|
rsh.u3f(rsh.uLightDir1, 0.0f, 1.0f, 0.0f);
|
|
rsh.u3f(rsh.uLightCol1, 0.0f, 0.0f, 0.0f);
|
|
}
|
|
/* camera forward in world coords: -row2 of the world->eye
|
|
* rotation (rows = eye axes in world; eye looks down -z) */
|
|
rsh.u3f(rsh.uViewFwd, -f.rot[6], -f.rot[7], -f.rot[8]);
|
|
rsh.u3f(rsh.uFogColor, f.fogc[0], f.fogc[1], f.fogc[2]);
|
|
/* uFog = start, end, immune; immune=1 kills fog entirely.
|
|
* Per-material immunity has no wire source yet -- 0 for all. */
|
|
if (fog_on) rsh.u3f(rsh.uFog, f.fogn, f.fogf, 0.0f);
|
|
else rsh.u3f(rsh.uFog, 1.0f, 2.0f, 1.0f);
|
|
} else {
|
|
if (fog_on) {
|
|
GLfloat fc[4] = { f.fogc[0], f.fogc[1], f.fogc[2], 1.0f };
|
|
glFogi(GL_FOG_MODE, GL_LINEAR);
|
|
glFogf(GL_FOG_START, f.fogn);
|
|
glFogf(GL_FOG_END, f.fogf);
|
|
glFogfv(GL_FOG_COLOR, fc);
|
|
glHint(GL_FOG_HINT, GL_NICEST);
|
|
glEnable(GL_FOG);
|
|
} else {
|
|
glDisable(GL_FOG);
|
|
}
|
|
/* directional sun (world coords; modelview is loaded, so GL maps
|
|
* it into eye space). World is y-down: up = -y. */
|
|
GLfloat lpos[4] = { 0.35f, -0.85f, 0.40f, 0.0f };
|
|
GLfloat lamb[4] = { 0.45f, 0.45f, 0.45f, 1.0f };
|
|
GLfloat ldif[4] = { 0.80f, 0.80f, 0.78f, 1.0f };
|
|
glLightfv(GL_LIGHT0, GL_POSITION, lpos);
|
|
glLightfv(GL_LIGHT0, GL_AMBIENT, lamb);
|
|
glLightfv(GL_LIGHT0, GL_DIFFUSE, ldif);
|
|
glEnable(GL_LIGHT0);
|
|
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
|
|
glEnable(GL_COLOR_MATERIAL);
|
|
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
|
|
glEnable(GL_NORMALIZE);
|
|
}
|
|
/* upload any new/changed baked material textures */
|
|
static std::map<unsigned, GLuint> gltex; /* matkey -> GL name */
|
|
static std::map<unsigned, unsigned> gltex_ver;
|
|
static std::map<unsigned, std::pair<int,int> > gltex_dim;
|
|
/* VPX_UVNORM=1: treat wire UVs as texel-space and rescale by the
|
|
* texture dimensions (pxpl5-era convention test) */
|
|
static int uvnorm = -1;
|
|
if (uvnorm < 0) {
|
|
const char *un = getenv("VPX_UVNORM");
|
|
uvnorm = (un && un[0] && un[0] != '0') ? 1 : 0;
|
|
}
|
|
EnterCriticalSection(&rt_lock);
|
|
std::map<unsigned, TexImg> texs = rt_texs; /* small; copy out */
|
|
LeaveCriticalSection(&rt_lock);
|
|
for (std::map<unsigned, TexImg>::const_iterator it = texs.begin();
|
|
it != texs.end(); ++it) {
|
|
std::map<unsigned, unsigned>::const_iterator vi =
|
|
gltex_ver.find(it->first);
|
|
if (vi != gltex_ver.end() && vi->second == it->second.ver) continue;
|
|
GLuint id;
|
|
std::map<unsigned, GLuint>::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);
|
|
/* the i860 board point-sampled (VRENDER SCANLINE.SS: one fld.l per
|
|
* pixel, no bilinear tap) -- NEAREST is authentic (dpl3-revive) */
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
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;
|
|
gltex_dim[it->first] = std::make_pair(it->second.w, it->second.h);
|
|
}
|
|
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;
|
|
if (uvnorm) {
|
|
std::map<unsigned, std::pair<int,int> >::const_iterator
|
|
di = gltex_dim.find(p.matkey);
|
|
glMatrixMode(GL_TEXTURE);
|
|
glLoadIdentity();
|
|
if (di != gltex_dim.end() && di->second.first > 0 &&
|
|
di->second.second > 0)
|
|
glScalef(1.0f / di->second.first,
|
|
1.0f / di->second.second, 1.0f);
|
|
glMatrixMode(GL_MODELVIEW);
|
|
}
|
|
}
|
|
glEnable(GL_TEXTURE_2D);
|
|
if (!shade) glColor3f(1.0f, 1.0f, 1.0f);
|
|
} else {
|
|
glDisable(GL_TEXTURE_2D);
|
|
if (!shade) glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]);
|
|
}
|
|
bool lit = !p.nrm.empty();
|
|
if (shade) {
|
|
rsh.u3f(rsh.uMatAmb, p.amb[0], p.amb[1], p.amb[2]);
|
|
rsh.u3f(rsh.uMatDiff, p.rgb[0], p.rgb[1], p.rgb[2]);
|
|
rsh.u3f(rsh.uMatEmis, p.emis[0], p.emis[1], p.emis[2]);
|
|
rsh.u3f(rsh.uRamp0, p.ramp0[0], p.ramp0[1], p.ramp0[2]);
|
|
rsh.u3f(rsh.uRamp1, p.ramp1[0], p.ramp1[1], p.ramp1[2]);
|
|
rsh.u1f(rsh.uHasTex, tex ? 1.0f : 0.0f);
|
|
if (!lit) {
|
|
/* no wire normals: flat world-space face normal
|
|
* (Newell); the gallery model lights everything and
|
|
* the shader flips it toward the viewer */
|
|
float nx = 0, ny = 0, nz = 0;
|
|
size_t n = p.xyz.size() / 3;
|
|
for (size_t v = 0; v < n; v++) {
|
|
const float *a = &p.xyz[v * 3];
|
|
const float *b = &p.xyz[((v + 1) % n) * 3];
|
|
nx += (a[1] - b[1]) * (a[2] + b[2]);
|
|
ny += (a[2] - b[2]) * (a[0] + b[0]);
|
|
nz += (a[0] - b[0]) * (a[1] + b[1]);
|
|
}
|
|
float l = sqrtf(nx * nx + ny * ny + nz * nz);
|
|
if (l < 1e-20f) { nx = 0; ny = -1; nz = 0; l = 1; }
|
|
glNormal3f(nx / l, ny / l, nz / l);
|
|
}
|
|
} else {
|
|
if (lit) glEnable(GL_LIGHTING); else glDisable(GL_LIGHTING);
|
|
}
|
|
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]);
|
|
if (lit) glNormal3f(p.nrm[v], p.nrm[v + 1], p.nrm[v + 2]);
|
|
glVertex3f(p.xyz[v], p.xyz[v + 1], p.xyz[v + 2]);
|
|
}
|
|
glEnd();
|
|
}
|
|
if (shade) rsh.use(0);
|
|
glDisable(GL_TEXTURE_2D);
|
|
glDisable(GL_LIGHTING);
|
|
}
|
|
SwapBuffers(dc);
|
|
}
|
|
|
|
/* 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] = "";
|
|
/* Explode mode: the pod's radar CRT is mounted sideways, so the encoded
|
|
* frame is 90deg off; rotate clockwise to view it upright on a desktop. */
|
|
static bool pal_radar_cw = false;
|
|
|
|
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];
|
|
/* VDB_APPLYMASK=1: AND the palette index with the group's VGA-DAC
|
|
* pixel-mask (default 0xFF = no-op). RP drives this mask actively where
|
|
* BT leaves it 0xFF, so honoring it may unscramble RP's MFD heads. */
|
|
static const bool apply_mask = getenv("VDB_APPLYMASK") != NULL;
|
|
const uint8_t *fb = vga.mem.linear;
|
|
Bitu mask = vga.draw.linear_mask ? vga.draw.linear_mask
|
|
: (vga.mem.memsize ? vga.mem.memsize - 1 : 0);
|
|
Bitu start = vga.config.real_start; /* visible page start (bytes) */
|
|
/* VDB_BASE=<hex>: override the read base. RP renders its cockpit displays
|
|
* to an OFF-SCREEN page (content found at 0x100000 by VDB_SCAN) while the
|
|
* visible page (real_start) is near-empty; the real VDB is fed from that
|
|
* page. Point us at it. (Proper fix = track the CL-GD5434 start register.) */
|
|
{
|
|
static const char *be = getenv("VDB_BASE");
|
|
if (be && *be) start = (Bitu)strtoul(be, NULL, 0);
|
|
}
|
|
/* VDB_REALSTRIDE=1: use the CRT's actual per-scanline byte advance instead
|
|
* of the hardcoded 640*2. RP may set a wider CL-GD5434 pitch or page-flip;
|
|
* BT uses a plain 1280 so this is a no-op there. Logged once/sec so we can
|
|
* see the real scan_len/addr_add/start and whether the page flips. */
|
|
static const bool real_stride = getenv("VDB_REALSTRIDE") != NULL;
|
|
Bitu stride = (Bitu)W * 2; /* 16bpp */
|
|
if (real_stride && vga.draw.address_add >= (Bitu)W*2
|
|
&& vga.draw.address_add <= (Bitu)W*8) {
|
|
stride = vga.draw.address_add;
|
|
}
|
|
if (vpx_fp && g == 0) {
|
|
static double last_log = 0.0; double nowt = (double)GetTickCount();
|
|
if (nowt - last_log > 1000.0) {
|
|
last_log = nowt;
|
|
fprintf(vpx_fp, "# VDB pal_draw: real_start=0x%X scan_len*2=%u "
|
|
"addr_add=%u using_stride=%u LIVE mode=%d w=%u h=%u memsize=%uK\n",
|
|
(unsigned)start, (unsigned)(vga.config.scan_len*2u),
|
|
(unsigned)vga.draw.address_add, (unsigned)stride,
|
|
(int)vga.mode, (unsigned)vga.draw.width,
|
|
(unsigned)vga.draw.height, (unsigned)(vga.mem.memsize/1024));
|
|
/* VDB_SCAN=1: find WHERE RP's display content actually lives in the
|
|
* Cirrus VRAM. The visible page (real_start) looks near-empty for RP
|
|
* -- so scan the whole framebuffer in 64K windows and report which
|
|
* hold significant non-black 16bpp pixels. */
|
|
if (fb && getenv("VDB_SCAN")) {
|
|
Bitu msz = vga.mem.memsize ? vga.mem.memsize : 0;
|
|
const Bitu WIN = 0x10000;
|
|
fprintf(vpx_fp, "# VRAM content scan (64K windows w/ >2%% lit):");
|
|
for (Bitu base = 0; base + WIN <= msz && base < 0x400000; base += WIN) {
|
|
unsigned lit = 0;
|
|
for (Bitu o = base; o < base + WIN; o += 2*64) { /* sample */
|
|
if (*(const uint16_t *)(fb + (o & mask))) lit++;
|
|
}
|
|
if (lit > (WIN/(2*64))/50) /* >2% of samples lit */
|
|
fprintf(vpx_fp, " 0x%X(%u%%)", (unsigned)base,
|
|
(unsigned)(lit*100/(WIN/(2*64))));
|
|
}
|
|
fprintf(vpx_fp, "\n");
|
|
}
|
|
fflush(vpx_fp);
|
|
}
|
|
}
|
|
if (fb) {
|
|
/* 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));
|
|
unsigned idx; int pg, chn = -1;
|
|
if (g <= 2) { idx = px & 0xFFu; pg = g; } /* bits 0-7 via pal0/1/2 */
|
|
else if (g <= 4) { idx = (px >> 8u) & 0xFFu; pg = g - 2; } /* bits 8-15 via pal1/2 */
|
|
else { idx = (px >> 8u) & 0xFFu; /* wins 5-9: one mono MFD */
|
|
pg = (g <= 6) ? 1 : 2; /* = one color wire of a */
|
|
chn = (g <= 6) ? g - 5 : g - 7; } /* head (pentapus split) */
|
|
if (apply_mask) idx &= vdb_pal[pg].mask;
|
|
const unsigned char *e = &vdb_pal[pg].ram[idx * 3u];
|
|
if (chn < 0) {
|
|
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 {
|
|
unsigned char v = (unsigned char)((e[chn] << 2) | (e[chn] >> 4));
|
|
d[0] = (unsigned char)(v >> 3); /* mono MFD: green-phosphor tube, */
|
|
d[1] = v; /* wire level = beam brightness */
|
|
d[2] = (unsigned char)(v >> 3);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
memset(img, 0, sizeof img);
|
|
}
|
|
int DW = W, DH = H;
|
|
const unsigned char *out = img;
|
|
if (g == 0 && pal_radar_cw) {
|
|
static unsigned char rot[640 * 480 * 3]; /* 90deg CW: 480x640 */
|
|
for (int dy = 0; dy < W; dy++) {
|
|
unsigned char *o = &rot[(size_t)dy * H * 3];
|
|
for (int dx = 0; dx < H; dx++, o += 3) {
|
|
const unsigned char *s =
|
|
&img[((size_t)(H - 1 - dx) * W + dy) * 3];
|
|
o[0] = s[0]; o[1] = s[1]; o[2] = s[2];
|
|
}
|
|
}
|
|
out = rot; DW = H; DH = W;
|
|
}
|
|
if (dump && pal_dump_dir[0]) {
|
|
char p[600];
|
|
snprintf(p, sizeof p, "%s/win%d.bmp", pal_dump_dir, g);
|
|
write_bmp(p, out, DW, DH);
|
|
}
|
|
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);
|
|
glPixelZoom((float)cw / DW, -(float)ch / DH); /* scale + flip y */
|
|
glRasterPos2f(-1.0f, 1.0f);
|
|
glDrawPixels(DW, DH, GL_RGB, GL_UNSIGNED_BYTE, out);
|
|
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);
|
|
}
|
|
|
|
/* Find this process's DOSBox main (SDL) window: visible, not one of our
|
|
* VPXGL windows, "DOSBox" in the title. */
|
|
static BOOL CALLBACK find_dosbox_wnd(HWND h, LPARAM lp) {
|
|
DWORD pid; GetWindowThreadProcessId(h, &pid);
|
|
if (pid != GetCurrentProcessId() || !IsWindowVisible(h)) return TRUE;
|
|
char cls[64]; GetClassNameA(h, cls, sizeof cls);
|
|
if (strcmp(cls, "VPXGL") == 0) return TRUE;
|
|
char t[160]; GetWindowTextA(h, t, sizeof t);
|
|
if (!strstr(t, "DOSBox")) return TRUE;
|
|
*(HWND *)lp = h;
|
|
return FALSE;
|
|
}
|
|
|
|
/* 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, 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;
|
|
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;
|
|
SetPixelFormat(dc, ChoosePixelFormat(dc, &pfd), &pfd);
|
|
HGLRC gl = wglCreateContext(dc);
|
|
if (!gl) return false;
|
|
*out_wnd = wnd; *out_dc = dc; *out_gl = gl;
|
|
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;
|
|
wc.lpfnWndProc = rt_wndproc;
|
|
wc.hInstance = GetModuleHandleA(NULL);
|
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
wc.lpszClassName = "VPXGL";
|
|
RegisterClassA(&wc);
|
|
|
|
/* 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. */
|
|
/* Layout modes (VPX_COCKPIT wins if both are set):
|
|
* VPX_COCKPIT=1 four borderless windows on the rig's VGA heads.
|
|
* VPX_EXPLODE=1 all 7 cockpit displays on one desktop: the two MFD
|
|
* heads split into their individual R/G/B wires (what
|
|
* the pentapus cable does), plus radar and main.
|
|
* (neither) framed debug row of the 3 raw heads. */
|
|
const char *ck = getenv("VPX_COCKPIT");
|
|
bool cockpit = (ck && ck[0] && ck[0] != '0');
|
|
const char *ex = getenv("VPX_EXPLODE");
|
|
bool explode = !cockpit && ex && ex[0] && ex[0] != '0';
|
|
/* VPX_NOMAIN=1: no native Division window (the dpl3-revive bridge is the
|
|
* out-the-window view; ours stays available as a wire-decode diagnostic).
|
|
* Radar/MFD windows are unaffected; the main slot geometry still anchors
|
|
* the DOSBox parking below. */
|
|
const char *nm = getenv("VPX_NOMAIN");
|
|
bool nomain = nm && nm[0] && nm[0] != '0';
|
|
|
|
HWND wnd = NULL; HDC dc = NULL; HGLRC gl = NULL;
|
|
int mx = 40, my = 40, mw = 832, mh = 512;
|
|
if (cockpit) { mx = 0; my = 0; mw = 800; mh = 600; }
|
|
else if (explode) { mx = 2020; my = 20; mw = 800; mh = 600; }
|
|
env_rect("VPX_MAIN", &mx, &my, &mw, &mh);
|
|
if (!nomain &&
|
|
!make_gl_window("VPX VelociRender (emulated)", mw, mh, mx, my,
|
|
cockpit, &wnd, &dc, &gl)) return 1;
|
|
|
|
/* Display windows: win0 = bits 0-7 via pal0 (color radar); win3/win4 =
|
|
* bits 8-15 via pal1/pal2 (the two raw MFD heads, channels superimposed);
|
|
* wins 5-9 = the five mono MFDs, one head color-wire each (pentapus
|
|
* split). The former exploratory win1/win2 are removed; g numbering is
|
|
* stable so pal_draw's decode and the win<g>.bmp dump names hold. */
|
|
const int NWIN = 10;
|
|
static const char *pal_titles[10] = {
|
|
"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)",
|
|
"MFD lower-left (HEAD A / red wire)",
|
|
"MFD lower-right (HEAD A / green wire)",
|
|
"MFD upper-left (HEAD B / red wire)",
|
|
"MFD upper-center (HEAD B / green wire)",
|
|
"MFD upper-right (HEAD B / blue wire)" };
|
|
static const int ck_x[5] = { 800, 0, 0, 1440, 2080 };
|
|
/* explode grid, all displays at native 640x480 (radar 480x640
|
|
* portrait, rotated upright): upper MFD row on top; below it the two
|
|
* lower MFDs in the outer columns (LR aligned under UR) with the radar
|
|
* centered between them; main in the right-hand column (mx/my above). */
|
|
pal_radar_cw = explode;
|
|
/* win7 (UL-decoded) and win9 (UR-decoded) trade screen positions: the two
|
|
* upper-outer MFDs read backward on the desktop (user 2026-07-07). This is
|
|
* a LOCATION swap only -- the decode (channel/palette) is untouched; whether
|
|
* the underlying cause is a color-translation or pentapus-cable order is
|
|
* still TBD and must be checked on a real pod. So win7 sits on the right
|
|
* (x=1340), win9 on the left (x=20). */
|
|
static const int ex_x[10] = { 760, 0,0,0,0, 20, 1340, 1340, 680, 20 };
|
|
static const int ex_y[10] = { 560, 0,0,0,0, 560, 560, 20, 20, 20 };
|
|
HWND pwnd[10]; HDC pdc[10]; HGLRC pgl[10];
|
|
bool phave[10];
|
|
int slot = 0; /* debug-grid position counter */
|
|
for (int g = 0; g < NWIN; g++) {
|
|
phave[g] = false;
|
|
if (g == 1 || g == 2) continue; /* exploratory decodes, removed */
|
|
bool want = explode ? (g == 0 || g >= 5) /* radar + 5 mono MFDs */
|
|
: (g <= 4); /* radar + 2 raw heads */
|
|
if (!want) continue;
|
|
int x, y, w, h;
|
|
if (cockpit) { x = ck_x[g]; y = 0; w = 640; h = 480; }
|
|
else if (explode) { x = ex_x[g]; y = ex_y[g];
|
|
if (g == 0) { w = 480; h = 640; } /* portrait radar */
|
|
else { 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;
|
|
bool dosbox_parked = false;
|
|
for (;;) {
|
|
/* 50ms timeout so the palette windows animate even between VPX
|
|
* frames (pal0 is rewritten continuously by the game). */
|
|
DWORD w = MsgWaitForMultipleObjects(1, &rt_event, FALSE, 50,
|
|
QS_ALLINPUT);
|
|
MSG msg;
|
|
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageA(&msg);
|
|
}
|
|
if (w == WAIT_OBJECT_0) {
|
|
bool redraw = false;
|
|
EnterCriticalSection(&rt_lock);
|
|
if (rt_new) { cur = rt_pending; rt_new = false; redraw = true; }
|
|
LeaveCriticalSection(&rt_lock);
|
|
if (redraw && cur.valid && wnd) {
|
|
wglMakeCurrent(dc, gl);
|
|
RECT cr; GetClientRect(wnd, &cr);
|
|
rt_draw(dc, cur, cr.right, cr.bottom);
|
|
rt_frames++;
|
|
}
|
|
}
|
|
/* 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 < NWIN; 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, dump);
|
|
}
|
|
/* explode mode: once the DOSBox main screen exists, park it
|
|
* centered under the Division window (one-shot; user can move it
|
|
* afterwards). */
|
|
if (explode && !dosbox_parked) {
|
|
HWND dos = NULL;
|
|
EnumWindows(find_dosbox_wnd, (LPARAM)&dos);
|
|
if (dos) {
|
|
RECT dv, db;
|
|
if (wnd) GetWindowRect(wnd, &dv);
|
|
else { dv.left = mx; dv.top = my; /* VPX_NOMAIN: the */
|
|
dv.right = mx + mw; dv.bottom = my + mh; } /* main slot */
|
|
GetWindowRect(dos, &db);
|
|
int x = (dv.left + dv.right) / 2 - (int)(db.right - db.left) / 2;
|
|
int y = dv.bottom + 10;
|
|
SetWindowPos(dos, NULL, x, y, 0, 0,
|
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
dosbox_parked = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---- scene-graph message decode ----------------------------------------- */
|
|
|
|
/* VPX_TEXLOG=<path>: texture-pipeline ground-truth log (type-12/13 flushes,
|
|
* texel-upload headers, bake decisions) for decoding texture params. */
|
|
static FILE *tex_log_fp(void) {
|
|
static FILE *fp = NULL;
|
|
static bool tried = false;
|
|
if (!tried) {
|
|
tried = true;
|
|
const char *p = getenv("VPX_TEXLOG");
|
|
if (p && p[0]) fp = fopen(p, "w");
|
|
}
|
|
return fp;
|
|
}
|
|
|
|
/* 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<unsigned, unsigned> baked_ver; /* material -> baked tex ver */
|
|
static unsigned bake_material_tex(unsigned matname) {
|
|
std::map<unsigned, unsigned>::const_iterator tmi = S.mat_texmap.find(matname);
|
|
if (tmi == S.mat_texmap.end()) return 0;
|
|
std::map<unsigned, unsigned>::const_iterator txi = S.texmap_tex.find(tmi->second);
|
|
if (txi == S.texmap_tex.end()) return 0;
|
|
std::map<unsigned, VTex>::const_iterator ti = S.tex.find(txi->second);
|
|
if (ti == S.tex.end() || ti->second.px.empty()) return 0;
|
|
const VTex &t = ti->second;
|
|
size_t npx = (size_t)t.w * t.h;
|
|
/* effective bpp from the upload-size ratio: dpl_TEXMAP has a
|
|
* bits_per_texel field but its wire semantics are unverified; the
|
|
* byte count is arithmetic ground truth. 16bpp = LE u16 intensity
|
|
* (hi byte), 4bpp = two texels/byte low-nibble-first. */
|
|
int bpp = t.px.size() >= npx * 2 ? 16 :
|
|
t.px.size() >= npx ? 8 :
|
|
t.px.size() * 2 >= npx ? 4 : 0;
|
|
if (!bpp) return 0;
|
|
std::map<unsigned, unsigned>::const_iterator bi = baked_ver.find(matname);
|
|
if (bi != baked_ver.end() && bi->second == t.ver) return matname;
|
|
FILE *tl = tex_log_fp();
|
|
if (tl) {
|
|
fprintf(tl, "bake mat=%08x texmap=%08x %dx%d px=%lu bpp=%d\n",
|
|
matname, txi->second, t.w, t.h,
|
|
(unsigned long)t.px.size(), bpp);
|
|
fflush(tl);
|
|
}
|
|
float lo[3] = { 0, 0, 0 }, hi[3] = { 1, 1, 1 };
|
|
std::map<unsigned, unsigned>::const_iterator ri = S.mat_ramp.find(matname);
|
|
if (ri != S.mat_ramp.end()) {
|
|
std::map<unsigned, VRamp>::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;
|
|
static unsigned bake_gen = 0; /* unique per bake so GL re-uploads */
|
|
img.w = t.w; img.h = t.h; img.ver = ++bake_gen;
|
|
img.rgba.resize((size_t)t.w * t.h * 4);
|
|
for (size_t i = 0; i < npx; i++) {
|
|
unsigned char raw =
|
|
bpp == 16 ? t.px[i * 2 + 1] :
|
|
bpp == 8 ? t.px[i] :
|
|
(unsigned char)(((t.px[i / 2] >> ((i & 1) * 4)) & 0xF) * 17);
|
|
float a = raw / 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) {
|
|
VMatG gmat;
|
|
gmat.diff[0] = 1.0f; gmat.diff[1] = 0.0f; gmat.diff[2] = 1.0f; /* magenta */
|
|
gmat.amb[0] = 1.0f; gmat.amb[1] = 0.0f; gmat.amb[2] = 1.0f;
|
|
VRamp rp = { { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f } }; /* identity */
|
|
unsigned matname = 0;
|
|
std::map<unsigned, unsigned>::const_iterator gmi = S.ggmat.find(gg);
|
|
if (gmi != S.ggmat.end()) {
|
|
matname = gmi->second;
|
|
std::map<unsigned, VMatG>::const_iterator mi = S.mat.find(matname);
|
|
if (mi != S.mat.end()) gmat = mi->second;
|
|
std::map<unsigned, unsigned>::const_iterator rmi =
|
|
S.mat_ramp.find(matname);
|
|
if (rmi != S.mat_ramp.end()) {
|
|
std::map<unsigned, VRamp>::const_iterator ri =
|
|
S.ramp.find(rmi->second);
|
|
if (ri != S.ramp.end()) rp = ri->second;
|
|
}
|
|
}
|
|
unsigned texkey = matname ? bake_material_tex(matname) : 0;
|
|
std::map<unsigned, std::vector<unsigned> >::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<unsigned, std::vector<float> >::const_iterator vi =
|
|
S.verts.find(geo);
|
|
std::map<unsigned, std::vector<std::vector<int> > >::const_iterator
|
|
pi = S.polys.find(geo);
|
|
if (vi == S.verts.end() || pi == S.polys.end()) continue;
|
|
const std::vector<float> &vv = vi->second;
|
|
const std::vector<float> *uv = NULL;
|
|
const std::vector<float> *nr = NULL;
|
|
std::map<unsigned, std::vector<float> >::const_iterator ui =
|
|
S.uvs.find(geo);
|
|
if (ui != S.uvs.end() && ui->second.size() * 3 == vv.size() * 2)
|
|
uv = &ui->second;
|
|
std::map<unsigned, std::vector<float> >::const_iterator ni =
|
|
S.nrms.find(geo);
|
|
if (ni != S.nrms.end() && ni->second.size() == vv.size()) {
|
|
/* only meaningful if not all-zero (stride < 8 stores zeros) */
|
|
for (size_t z = 0; z < ni->second.size(); z++)
|
|
if (ni->second[z] != 0.0f) { nr = &ni->second; break; }
|
|
}
|
|
for (size_t q = 0; q < pi->second.size(); q++) {
|
|
const std::vector<int> &idx = pi->second[q];
|
|
VPoly poly;
|
|
memcpy(poly.rgb, gmat.diff, sizeof poly.rgb);
|
|
memcpy(poly.amb, gmat.amb, sizeof poly.amb);
|
|
memcpy(poly.emis, gmat.emis, sizeof poly.emis);
|
|
poly.matkey = (texkey && uv) ? texkey : 0;
|
|
if (!poly.matkey) {
|
|
/* untextured: the shading ramp applies to the light; for
|
|
* textured polys the bake already ran texels through the
|
|
* ramp, so keep the constructor's identity light-ramp */
|
|
memcpy(poly.ramp0, rp.lo, sizeof poly.ramp0);
|
|
memcpy(poly.ramp1, rp.hi, sizeof poly.ramp1);
|
|
}
|
|
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 (nr) {
|
|
float nout[3];
|
|
if (world) m16_xform_dir(*world, &(*nr)[o], nout);
|
|
else { nout[0] = (*nr)[o]; nout[1] = (*nr)[o + 1];
|
|
nout[2] = (*nr)[o + 2]; }
|
|
poly.nrm.push_back(nout[0]);
|
|
poly.nrm.push_back(nout[1]);
|
|
poly.nrm.push_back(nout[2]);
|
|
}
|
|
}
|
|
if (poly.nrm.size() != poly.xyz.size()) poly.nrm.clear();
|
|
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<unsigned, M16> cache;
|
|
std::map<unsigned, bool> gg_done;
|
|
/* resolve wire lights: ambient (type 2) sums into samb; directional
|
|
* (type 3) aim = the light DCS's +Z world row (dpl3-revive). Up to two
|
|
* directional slots feed the shader; if none, rt_draw uses its rig. */
|
|
for (std::map<unsigned, VScene::VLight>::const_iterator li =
|
|
S.lights.begin(); li != S.lights.end(); ++li) {
|
|
const VScene::VLight &L = li->second;
|
|
if (L.ltype == 2) {
|
|
for (int i = 0; i < 3; i++) f.samb[i] += L.rgb[i];
|
|
} else if (L.ltype == 3 && f.nlit < 2) {
|
|
M16 w;
|
|
dcs_world(L.dcs, cache, w);
|
|
float dx = w.m[8], dy = w.m[9], dz = w.m[10]; /* +Z world row */
|
|
float n = sqrtf(dx * dx + dy * dy + dz * dz);
|
|
if (n > 1e-6f) {
|
|
int s = f.nlit++;
|
|
f.ldir[s][0] = dx / n; f.ldir[s][1] = dy / n;
|
|
f.ldir[s][2] = dz / n;
|
|
for (int i = 0; i < 3; i++) f.lcol[s][i] = L.rgb[i];
|
|
}
|
|
}
|
|
}
|
|
{ /* periodic light-decode diagnostic (stderr): how many type-6/0xe
|
|
* light nodes exist vs how many decoded, sampled throughout so it
|
|
* catches the mission-loaded scene (not just boot). */
|
|
static int fc = 0;
|
|
if (++fc % 120 == 1) {
|
|
int n6 = 0, ne = 0;
|
|
for (std::map<unsigned, unsigned>::const_iterator ti =
|
|
S.type.begin(); ti != S.type.end(); ++ti) {
|
|
if (ti->second == 6) n6++;
|
|
if (ti->second == 0xe) ne++;
|
|
}
|
|
fprintf(stderr, "VPX lights@f%d: type6=%d typeE=%d decoded=%d "
|
|
"dir=%d amb(%.2f,%.2f,%.2f)\n", fc, n6, ne,
|
|
(int)S.lights.size(), f.nlit,
|
|
f.samb[0], f.samb[1], f.samb[2]);
|
|
if (f.nlit)
|
|
fprintf(stderr, " L0 dir(%.2f,%.2f,%.2f) col(%.2f,%.2f,%.2f)\n",
|
|
f.ldir[0][0], f.ldir[0][1], f.ldir[0][2],
|
|
f.lcol[0][0], f.lcol[0][1], f.lcol[0][2]);
|
|
}
|
|
}
|
|
for (std::map<unsigned, std::vector<unsigned> >::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];
|
|
/* (hidden-instance w3 skip removed -- our w3 offset was culling
|
|
* the ground plane; revisit once the offset is confirmed) */
|
|
std::map<unsigned, unsigned>::const_iterator oi =
|
|
S.inst_object.find(inst);
|
|
if (oi == S.inst_object.end()) continue;
|
|
std::map<unsigned, std::vector<unsigned> >::const_iterator li =
|
|
S.children.find(oi->second);
|
|
if (li == S.children.end() || li->second.empty()) continue;
|
|
/* First LOD child: the host maintains the active LOD at the list
|
|
* head (the wire lod flush carries no switch distances -- LOD
|
|
* selection is host-side). */
|
|
unsigned lod = li->second[0];
|
|
std::map<unsigned, std::vector<unsigned> >::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<unsigned, unsigned>::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<float> &vl = S.verts[S.geom_node];
|
|
std::vector<float> &tl = S.uvs[S.geom_node];
|
|
std::vector<float> &nl = S.nrms[S.geom_node];
|
|
vl.clear(); tl.clear(); nl.clear();
|
|
/* record layout by stride: 5 = xyz+uv,
|
|
* 8/9 = xyz + normal(3..5) + uv(6..7) (+extra) */
|
|
size_t uvo = (S.geom_stride >= 8) ? 6 :
|
|
(S.geom_stride == 5) ? 3 : 0;
|
|
bool has_n = S.geom_stride >= 8;
|
|
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); }
|
|
if (has_n && i + 5 < S.geom_acc.size()) {
|
|
nl.push_back(S.geom_acc[i + 3]);
|
|
nl.push_back(S.geom_acc[i + 4]);
|
|
nl.push_back(S.geom_acc[i + 5]);
|
|
} else { nl.push_back(0); nl.push_back(0); nl.push_back(0); }
|
|
}
|
|
FILE *flog = tex_log_fp();
|
|
if (flog && uvo && !tl.empty()) {
|
|
float ulo = tl[0], uhi = tl[0], vlo = tl[1], vhi = tl[1];
|
|
for (size_t k = 0; k + 1 < tl.size(); k += 2) {
|
|
if (tl[k] < ulo) ulo = tl[k];
|
|
if (tl[k] > uhi) uhi = tl[k];
|
|
if (tl[k + 1] < vlo) vlo = tl[k + 1];
|
|
if (tl[k + 1] > vhi) vhi = tl[k + 1];
|
|
}
|
|
fprintf(flog, "geomuv node=%08x verts=%lu stride=%lu "
|
|
"u=[%g,%g] v=[%g,%g]\n", S.geom_node,
|
|
(unsigned long)(vl.size() / 3),
|
|
(unsigned long)S.geom_stride, ulo, uhi, vlo, vhi);
|
|
fflush(flog);
|
|
}
|
|
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) {
|
|
/* Accumulate the whole batch FIRST, then slice (mirrors the vertex
|
|
* path and Dave's proven parser). The old per-message slicing broke
|
|
* two ways on large geometries: a record straddling a burst boundary
|
|
* lost its tail AND misaligned every record after it, and a SECOND
|
|
* conn batch on the same geometry (floor = mixed tri+quad batches)
|
|
* terminated instantly against the total-poly count and dropped its
|
|
* indices -- the "holey ground" the reticle fell through. Also keep
|
|
* ALL loop indices (the old closing-dup drop halved every quad);
|
|
* genuinely closed loops just add a zero-area fan triangle. */
|
|
size_t nw = nb / 4;
|
|
for (size_t o = 0; o < nw; o++)
|
|
S.conn_acc.push_back((int)rd_u32(d + o * 4));
|
|
if (S.conn_acc.size() >= (size_t)S.conn_npolys * S.conn_loop) {
|
|
std::vector<std::vector<int> > &pl = S.polys[S.conn_node];
|
|
for (size_t o = 0; o + S.conn_loop <= S.conn_acc.size();
|
|
o += S.conn_loop) {
|
|
pl.push_back(std::vector<int>(
|
|
S.conn_acc.begin() + o,
|
|
S.conn_acc.begin() + o + S.conn_loop));
|
|
}
|
|
S.conn_acc.clear();
|
|
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 */
|
|
/* dpl_MATERIAL wire layout (verified on a live capture):
|
|
* texture ref d+8, ramp ref d+20, emissive d+24, ambient
|
|
* d+36, diffuse d+48, opacity d+60, specular+shin d+72.
|
|
* Fixed offsets: the old scan-by-node-type missed refs
|
|
* whose create hadn't arrived yet (=> default gray ramp).
|
|
* Refs resolve lazily at bake time, so order is safe.
|
|
* Boot flushes can carry heap garbage -- clamp (NaN fails
|
|
* the range test and lands on the default). */
|
|
VMatG m;
|
|
for (int i = 0; i < 3; i++) {
|
|
float e = rd_f32(d + 24 + i * 4), a = rd_f32(d + 36 + i * 4);
|
|
float df = rd_f32(d + 48 + i * 4), sp = rd_f32(d + 72 + i * 4);
|
|
m.emis[i] = (e >= 0.0f && e <= 8.0f) ? e : 0.0f;
|
|
m.amb[i] = (a >= 0.0f && a <= 8.0f) ? a : 0.6f;
|
|
m.diff[i] = (df >= 0.0f && df <= 8.0f) ? df : 0.6f;
|
|
m.spec[i] = (sp >= 0.0f && sp <= 8.0f) ? sp : 0.0f;
|
|
}
|
|
float op = rd_f32(d + 60), sh = rd_f32(d + 84);
|
|
m.opacity = (op >= 0.0f && op <= 1.0f) ? op : 1.0f;
|
|
m.shin = (sh >= 0.0f && sh <= 1024.0f) ? sh : 0.0f;
|
|
/* dpl3-revive: exact ambient==(1,0,0) AND diffuse==(1,0,0) is
|
|
* the shipped build's UNSET-material marker (on every
|
|
* .B2Z-default material), not a colour -- render as white. */
|
|
if (m.amb[0] == 1.0f && m.amb[1] == 0.0f && m.amb[2] == 0.0f &&
|
|
m.diff[0] == 1.0f && m.diff[1] == 0.0f && m.diff[2] == 0.0f) {
|
|
for (int i = 0; i < 3; i++) { m.amb[i] = 1.0f; m.diff[i] = 1.0f; }
|
|
}
|
|
S.mat[name] = m;
|
|
unsigned tref = rd_u32(d + 8), rref = rd_u32(d + 20);
|
|
if (tref && tref != 0xFFFFFFFFu) S.mat_texmap[name] = tref;
|
|
else S.mat_texmap.erase(name);
|
|
if (rref && rref != 0xFFFFFFFFu) S.mat_ramp[name] = rref;
|
|
else S.mat_ramp.erase(name);
|
|
baked_ver.erase(name); /* rebake with fresh bindings */
|
|
} else if (t == 12 && nb >= 12) { /* texture params */
|
|
/* dpl_TEXTURE: texmap ref at d+8 (then minify/magnify/
|
|
* alpha/wrap/detail/u0/v0/du/dv -- not yet used) */
|
|
unsigned mref = rd_u32(d + 8);
|
|
if (mref && mref != 0xFFFFFFFFu) S.texmap_tex[name] = mref;
|
|
FILE *tl = tex_log_fp();
|
|
if (tl && nb >= 60) {
|
|
fprintf(tl, "t12 tex=%08x texmap=%08x min=%d mag=%d "
|
|
"alpha=%d wrapu=%d wrapv=%d detail=%d u0=%g "
|
|
"v0=%g du=%g dv=%g atime=%g abhv=%d\n",
|
|
name, mref, (int)rd_u32(d + 12),
|
|
(int)rd_u32(d + 16), (int)rd_u32(d + 20),
|
|
(int)rd_u32(d + 24), (int)rd_u32(d + 28),
|
|
(int)rd_u32(d + 32), rd_f32(d + 36),
|
|
rd_f32(d + 40), rd_f32(d + 44), rd_f32(d + 48),
|
|
rd_f32(d + 52), (int)rd_u32(d + 56));
|
|
fflush(tl);
|
|
}
|
|
} else if (t == 7 && nb >= 12) { /* light */
|
|
/* dpl_LIGHT wire layout still unknown (field-by-field
|
|
* serializer, not a struct dump) -- raw dump each u32 as
|
|
* hex + float until the offsets are pinned */
|
|
FILE *tl = tex_log_fp();
|
|
if (tl) {
|
|
fprintf(tl, "t7 light=%08x nb=%lu raw=", name,
|
|
(unsigned long)nb);
|
|
for (size_t o = 8; o + 3 < nb; o += 4)
|
|
fprintf(tl, " %08x/%g", rd_u32(d + o), rd_f32(d + o));
|
|
fprintf(tl, "\n");
|
|
fflush(tl);
|
|
}
|
|
} else if (t == 6 || t == 0xe) { /* lmodel / light */
|
|
{ static bool dl = false; if (!dl) { dl = true;
|
|
fprintf(stderr, "VPX lightnode t=%u nb=%lu:",
|
|
(unsigned)t, (unsigned long)nb);
|
|
for (size_t o = 8; o + 3 < nb && o < 56; o += 4)
|
|
fprintf(stderr, " [%u]%08x/%g", (unsigned)o,
|
|
rd_u32(d + o), rd_f32(d + o));
|
|
fprintf(stderr, "\n"); } }
|
|
/* dpl3-revive light decode: dcs @d+12, light_type @d+16
|
|
* (2=ambient, 3=directional), rgb @d+20..28. Directional aim
|
|
* is resolved to the light DCS's +Z world row at frame
|
|
* assembly. The sanity range rejects the game's 4 stray
|
|
* type-0xe 32B "vehicle lamp" bodies (heap-garbage rgb). */
|
|
if (nb >= 32) {
|
|
unsigned ldcs = rd_u32(d + 12);
|
|
int ltype = (int)rd_u32(d + 16);
|
|
float r = rd_f32(d + 20), g = rd_f32(d + 24),
|
|
b = rd_f32(d + 28);
|
|
if ((ltype == 2 || ltype == 3) &&
|
|
r == r && g == g && b == b && /* NaN guard */
|
|
r >= 0.0f && g >= 0.0f && b >= 0.0f &&
|
|
r <= 100.0f && g <= 100.0f && b <= 100.0f) {
|
|
VScene::VLight L;
|
|
L.dcs = ldcs; L.ltype = ltype;
|
|
L.rgb[0] = r; L.rgb[1] = g; L.rgb[2] = b;
|
|
S.lights[name] = L;
|
|
}
|
|
}
|
|
} else if (t == 2 && nb >= 8) { /* zone (raw log) */
|
|
FILE *tl = tex_log_fp();
|
|
if (tl) {
|
|
fprintf(tl, "t2 zone=%08x raw=", name);
|
|
for (size_t o = 8; o + 3 < nb && o < 40; o += 4)
|
|
fprintf(tl, " %08x", rd_u32(d + o));
|
|
fprintf(tl, "\n");
|
|
fflush(tl);
|
|
}
|
|
} else if (t == 13 && nb >= 24) { /* texmap header */
|
|
/* dpl_TEXMAP: texels ptr, u_size, v_size, bits_per_texel
|
|
* (+ REMOTE hwareSize/hwareOffs/bilinear) -- log only, the
|
|
* bake infers bpp from the upload byte/texel ratio */
|
|
FILE *tl = tex_log_fp();
|
|
if (tl) {
|
|
fprintf(tl, "t13 texmap=%08x texels=%08x u=%u v=%u bpt=%u",
|
|
name, rd_u32(d + 8), rd_u32(d + 12),
|
|
rd_u32(d + 16), rd_u32(d + 20));
|
|
if (nb >= 36)
|
|
fprintf(tl, " hwsz=%u hwoff=%u bilin=%u",
|
|
rd_u32(d + 24), rd_u32(d + 28),
|
|
rd_u32(d + 32));
|
|
fprintf(tl, "\n");
|
|
fflush(tl);
|
|
}
|
|
} 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);
|
|
}
|
|
/* retint materials baked through this ramp */
|
|
for (std::map<unsigned, unsigned>::const_iterator mi =
|
|
S.mat_ramp.begin(); mi != S.mat_ramp.end(); ++mi)
|
|
if (mi->second == name) baked_ver.erase(mi->first);
|
|
} 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);
|
|
/* dpl_VIEW fog follows back_color: enable/mode, near, far,
|
|
* r, g, b (game sends mode 5 and animates near/far/color;
|
|
* verified against a live capture). Boot-time flushes carry
|
|
* heap garbage here, so sanity-check the range. */
|
|
float fn = rd_f32(d + 76), ff = rd_f32(d + 80);
|
|
if (rd_u32(d + 72) != 0 && ff > fn && ff > 0 &&
|
|
ff < 1e6f && fn >= 0) {
|
|
S.view.fog = true;
|
|
S.view.fogn = fn; S.view.fogf = ff;
|
|
for (int i = 0; i < 3; i++) {
|
|
float c = rd_f32(d + 84 + i * 4);
|
|
S.view.fogc[i] = (c >= 0 && c <= 1) ? c : 0;
|
|
}
|
|
} else {
|
|
S.view.fog = false;
|
|
}
|
|
} 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 */
|
|
/* display mode (dpl3-revive): word3 @d+16 -- 3=normal,
|
|
* 2=billboard, 0/1=HIDDEN until armed (the parked player mech
|
|
* is w3=1 pre-translocation). Stored for the assembly skip. */
|
|
if (nb >= 20) S.inst_w3[name] = rd_u32(d + 16);
|
|
/* INTERSECT mask @d+40 (verified live: buildings/floor/sky =
|
|
* 0xffffffff, the player's own mech parts + beams + missiles
|
|
* = 0). The AUTHENTIC pick exclusion: the game's sect query
|
|
* ANDs its mask against this. */
|
|
if (nb >= 44) S.inst_mask[name] = rd_u32(d + 40);
|
|
for (size_t o = 8; o + 3 < nb; o += 4) {
|
|
unsigned val = rd_u32(d + o);
|
|
if (val && val != 0xFFFFFFFFu) {
|
|
std::map<unsigned, unsigned>::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.conn_acc.clear();
|
|
/* do NOT clear S.polys here: geometries ship MULTIPLE conn
|
|
* batches (floor/walls = a triangle batch + a quad batch);
|
|
* clearing per header kept only the LAST batch. Fresh
|
|
* geometry arrives under fresh handles, so appending is
|
|
* safe; a re-flushed batch at worst duplicates triangles,
|
|
* which the raycast tolerates. */
|
|
}
|
|
break;
|
|
case 26: /* texel upload header [node][nbytes][w][h]... (4/8/16bpp) */
|
|
if (nb >= 16) {
|
|
unsigned node = rd_u32(d), nbytes = rd_u32(d + 4);
|
|
unsigned tw = rd_u32(d + 8), th = rd_u32(d + 12);
|
|
FILE *tl = tex_log_fp();
|
|
if (tl) {
|
|
fprintf(tl, "up26 texmap=%08x nbytes=%u w=%u h=%u\n",
|
|
node, nbytes, tw, th);
|
|
fflush(tl);
|
|
}
|
|
/* w*h <= 2*nbytes admits 4bpp uploads; bake derives the
|
|
* effective bpp from the byte/texel ratio */
|
|
if (nbytes && nbytes <= (1u << 20) && tw && th &&
|
|
tw <= 1024 && th <= 1024 &&
|
|
(size_t)tw * th <= (size_t)nbytes * 2) {
|
|
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;
|
|
case 38: /* set_sect_pixel: game armed the continuous reticle pick.
|
|
* From here the board is expected to return the hit in each
|
|
* frame reply -- that's what unblocks weapons fire.
|
|
* Payload: [xp][yp][radius][mask]; keep the query mask for
|
|
* the per-instance INTERSECT test. */
|
|
sect_armed = true;
|
|
if (nb >= 16) sect_mask = rd_u32(d + 12);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/* Moller-Trumbore ray/triangle: returns hit distance t>0, or -1 (no hit). */
|
|
static float ray_tri(const float O[3], const float D[3],
|
|
const float a[3], const float b[3], const float c[3]) {
|
|
float e1[3], e2[3], p[3], q[3], s[3];
|
|
for (int i = 0; i < 3; i++) { e1[i] = b[i] - a[i]; e2[i] = c[i] - a[i]; }
|
|
p[0] = D[1]*e2[2] - D[2]*e2[1];
|
|
p[1] = D[2]*e2[0] - D[0]*e2[2];
|
|
p[2] = D[0]*e2[1] - D[1]*e2[0];
|
|
float det = e1[0]*p[0] + e1[1]*p[1] + e1[2]*p[2];
|
|
if (det > -1e-6f && det < 1e-6f) return -1.0f; /* ray parallel */
|
|
float inv = 1.0f / det;
|
|
for (int i = 0; i < 3; i++) s[i] = O[i] - a[i];
|
|
float u = (s[0]*p[0] + s[1]*p[1] + s[2]*p[2]) * inv;
|
|
if (u < 0.0f || u > 1.0f) return -1.0f;
|
|
q[0] = s[1]*e1[2] - s[2]*e1[1];
|
|
q[1] = s[2]*e1[0] - s[0]*e1[2];
|
|
q[2] = s[0]*e1[1] - s[1]*e1[0];
|
|
float v = (D[0]*q[0] + D[1]*q[1] + D[2]*q[2]) * inv;
|
|
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
|
float t = (e2[0]*q[0] + e2[1]*q[1] + e2[2]*q[2]) * inv;
|
|
return t > 1e-3f ? t : -1.0f; /* in front only */
|
|
}
|
|
|
|
/* Real reticle pick: cast the camera centre ray (the screen-0.5,0.5 reticle)
|
|
* against the live scene and return the nearest triangle's owning instance, the
|
|
* DCS it hangs under (whose app-specific the game reads as the target Entity),
|
|
* and the world hit point. Same picking Dave's renderer does, self-contained.
|
|
* Traversal mirrors scene_publish_frame: dcs -> instance -> object -> lod[0] ->
|
|
* geogroup -> geometry -> polys, transformed by the DCS world matrix. */
|
|
/* Pick outcome stats, printed to the VPXLOG every 256 calls: the aim only
|
|
* refreshes game-side when a sect reply lands, so a high miss rate IS the
|
|
* "stale aim" symptom. fail reasons: 1=no camera, 2=degenerate dir, 3=ray
|
|
* hit nothing (aim above the walls / into open sky). */
|
|
static unsigned pick_calls = 0, pick_oks = 0, pick_bridge = 0;
|
|
static unsigned pick_fails[4];
|
|
|
|
static void pick_stat_print(void) {
|
|
if (++pick_calls % 256 || !vpx_fp) return;
|
|
fprintf(vpx_fp, "# pick stats: calls=%u ok=%u (bridge=%u) "
|
|
"fail nocam=%u dir=%u nohit=%u\n",
|
|
pick_calls, pick_oks, pick_bridge,
|
|
pick_fails[1], pick_fails[2], pick_fails[3]);
|
|
fflush(vpx_fp);
|
|
}
|
|
static void pick_stat_fail(int r) { pick_fails[r & 3]++; pick_stat_print(); }
|
|
static void pick_stat_ok(bool bridge) {
|
|
pick_oks++; if (bridge) pick_bridge++;
|
|
pick_stat_print();
|
|
}
|
|
|
|
static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out,
|
|
unsigned *geom_out, float xyz_out[3]) {
|
|
/* Ray = the player's actual look: prefer the bridge's camera (cam chain
|
|
* + torso twist + hat glance, streamed back over the fifosock). The
|
|
* device's own S.view decode tracks the static view-node pose, so with
|
|
* it every pick -- and every missile/beam -- aimed at one fixed wrong
|
|
* world point. Fallback keeps weapons firing bridge-less. */
|
|
float O[3], D[3];
|
|
const bool from_bridge = bridge_cam_get(O, D);
|
|
if (!from_bridge) {
|
|
if (!S.view.has_cam) { pick_stat_fail(1); return false; }
|
|
O[0] = S.view.eye[0]; O[1] = S.view.eye[1]; O[2] = S.view.eye[2];
|
|
D[0] = -S.view.rot[6]; D[1] = -S.view.rot[7]; D[2] = -S.view.rot[8];
|
|
}
|
|
float dl = sqrtf(D[0]*D[0] + D[1]*D[1] + D[2]*D[2]);
|
|
if (dl < 1e-6f) { pick_stat_fail(2); return false; }
|
|
D[0] /= dl; D[1] /= dl; D[2] /= dl;
|
|
/* Report the TRUE nearest hit -- terrain IS a valid target (you must be
|
|
* able to fire into the ground and miss). Return ALL of the hit handles:
|
|
* instance, DCS, geogroup, geometry -- the game does GetAppSpecific on the
|
|
* DCS (=> targetEntity) AND the geogroup (=> damage zone), so a real board
|
|
* always has a geogroup on a hit; sending 0 there handed the game a null. */
|
|
float best_t = 1e30f;
|
|
unsigned best_inst = 0, best_dcs = 0, best_gg = 0, best_geo = 0;
|
|
std::map<unsigned, M16> cache;
|
|
for (std::map<unsigned, std::vector<unsigned> >::const_iterator di =
|
|
S.children.begin(); di != S.children.end(); ++di) {
|
|
std::map<unsigned, unsigned>::const_iterator ti = S.type.find(di->first);
|
|
if (ti == S.type.end() || ti->second != 5) continue; /* DCS parent */
|
|
M16 world; dcs_world(di->first, cache, world);
|
|
for (size_t ii = 0; ii < di->second.size(); ii++) {
|
|
unsigned inst = di->second[ii];
|
|
/* AUTHENTIC pick exclusion: per-instance INTERSECT mask
|
|
* (instance word 9) ANDed with the query mask from the 0x26
|
|
* arm -- exactly what the real board did. The game authors it
|
|
* for us: own mech parts, laser beams, missiles and effects
|
|
* carry mask 0 (never picked -- no beam feedback loops, no
|
|
* homing on own ordnance, no stale missile handles); terrain,
|
|
* sky, buildings and ENEMY mechs carry INTERSECT_ALL, so
|
|
* point-blank hulls are targetable again (this replaced the
|
|
* interim min-range and hidden-until-armed gates). */
|
|
std::map<unsigned, unsigned>::const_iterator mi =
|
|
S.inst_mask.find(inst);
|
|
if (mi != S.inst_mask.end() && (mi->second & sect_mask) == 0)
|
|
continue;
|
|
std::map<unsigned, unsigned>::const_iterator oi =
|
|
S.inst_object.find(inst);
|
|
if (oi == S.inst_object.end()) continue;
|
|
std::map<unsigned, std::vector<unsigned> >::const_iterator li =
|
|
S.children.find(oi->second);
|
|
if (li == S.children.end() || li->second.empty()) continue;
|
|
/* Walk ALL lod children, not just lod[0]: BT content is authored
|
|
* ADDITIVE_LODS (the arena wall = a posts lod + the actual wall
|
|
* panel in ANOTHER lod). Picking lod[0] only made the ray pass
|
|
* BETWEEN the posts of whatever the player aimed at -- 94% miss
|
|
* rate, stale/cleared targets, boo-beep on every trigger. */
|
|
for (size_t ldx = 0; ldx < li->second.size(); ldx++) {
|
|
std::map<unsigned, std::vector<unsigned> >::const_iterator ggi =
|
|
S.children.find(li->second[ldx]);
|
|
if (ggi == S.children.end()) continue;
|
|
for (size_t g = 0; g < ggi->second.size(); g++) {
|
|
unsigned gg = ggi->second[g]; /* geogroup handle */
|
|
std::map<unsigned, std::vector<unsigned> >::const_iterator gci =
|
|
S.children.find(gg); /* geogroup->geoms */
|
|
if (gci == S.children.end()) continue;
|
|
for (size_t k = 0; k < gci->second.size(); k++) {
|
|
unsigned geo = gci->second[k]; /* geometry handle */
|
|
std::map<unsigned, std::vector<float> >::const_iterator vi =
|
|
S.verts.find(geo);
|
|
std::map<unsigned,
|
|
std::vector<std::vector<int> > >::const_iterator pi =
|
|
S.polys.find(geo);
|
|
if (vi == S.verts.end() || pi == S.polys.end()) continue;
|
|
const std::vector<float> &vv = vi->second;
|
|
for (size_t r = 0; r < pi->second.size(); r++) {
|
|
const std::vector<int> &idx = pi->second[r];
|
|
if (idx.size() < 3) continue;
|
|
size_t o0 = (size_t)idx[0] * 3;
|
|
if (o0 + 2 >= vv.size()) continue;
|
|
float w0[3]; m16_xform(world, &vv[o0], w0);
|
|
for (size_t j = 1; j + 1 < idx.size(); j++) {
|
|
size_t o1 = (size_t)idx[j] * 3;
|
|
size_t o2 = (size_t)idx[j + 1] * 3;
|
|
if (o1 + 2 >= vv.size() || o2 + 2 >= vv.size()) continue;
|
|
float w1[3], w2[3];
|
|
m16_xform(world, &vv[o1], w1);
|
|
m16_xform(world, &vv[o2], w2);
|
|
float t = ray_tri(O, D, w0, w1, w2);
|
|
if (t > 0.0f && t < best_t) {
|
|
best_t = t; best_inst = inst;
|
|
best_dcs = di->first; best_gg = gg; best_geo = geo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} /* all-lods walk */
|
|
}
|
|
}
|
|
if (!best_inst) { pick_stat_fail(3); return false; }
|
|
pick_stat_ok(from_bridge);
|
|
xyz_out[0] = O[0] + D[0] * best_t;
|
|
xyz_out[1] = O[1] + D[1] * best_t;
|
|
xyz_out[2] = O[2] + D[2] * best_t;
|
|
*inst_out = best_inst; *dcs_out = best_dcs;
|
|
*gg_out = best_gg; *geom_out = best_geo;
|
|
static unsigned last_i = 0, last_d = 0;
|
|
if ((best_inst != last_i || best_dcs != last_d) && vpx_fp) {
|
|
flush_run();
|
|
fprintf(vpx_fp, "# raycast pick: inst=%08X dcs=%08X gg=%08X t=%.1f "
|
|
"pos(%.1f,%.1f,%.1f) cam=%s eye(%.1f,%.1f,%.1f)\n",
|
|
best_inst, best_dcs, best_gg, best_t,
|
|
xyz_out[0], xyz_out[1], xyz_out[2],
|
|
from_bridge ? "bridge" : "view", O[0], O[1], O[2]);
|
|
fflush(vpx_fp);
|
|
last_i = best_inst; last_d = best_dcs;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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.inst_w3.clear(); S.inst_mask.clear();
|
|
S.dcs_mat.clear();
|
|
S.dcs_parent.clear(); S.lights.clear();
|
|
S.view = VFrame();
|
|
S.geom_active = false; S.conn_active = false;
|
|
S.geom_acc.clear(); S.conn_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 bool raycast_pick(unsigned *, unsigned *, unsigned *, unsigned *, float *) { return false; }
|
|
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;
|
|
|
|
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;
|
|
/* VDB_PALDUMP=<prefix>: on each completed palette load, write the group's
|
|
* 768-byte RGB palette to <prefix>N.rgb so the display color maps can be
|
|
* visualized. */
|
|
static const char *vdb_paldump = NULL;
|
|
|
|
/* Palette groups sit at Adam's-base + 2 (see L4VB16.CPP / L4SVGA16.ASM):
|
|
* secondary 0x302-0x305, aux1 0x30A-0x30D, aux2 0x312-0x315. Within a group
|
|
* (VGA-DAC layout): +0 pixel-mask, +1 read-addr, +2 write-addr, +3 data.
|
|
* SVGAWriteFullPalette: mov dx,base; add dx,2 (write-addr); out; inc dx
|
|
* (data); rep outsb. */
|
|
static int vdb_group_of(io_port_t off) {
|
|
if (off >= 2 && off <= 5) return 0;
|
|
if (off >= 10 && off <= 13) return 1;
|
|
if (off >= 18 && off <= 21) return 2;
|
|
return -1;
|
|
}
|
|
static int vdb_group_base(int g) { return 2 + 8 * g; }
|
|
static void vdb_flush_data(void) {
|
|
if (vdb_data_group >= 0 && vdb_data_count) {
|
|
if (vpx_fp) {
|
|
/* Palette-animation probe: on each reload, diff against the last
|
|
* captured palette for this group and report how many of the 256
|
|
* entries changed (+ the first few index:old->new), so the
|
|
* "flashed" data can be seen. */
|
|
static unsigned char prev[3][768];
|
|
static bool have_prev[3] = { false, false, false };
|
|
int g = vdb_data_group;
|
|
flush_run();
|
|
if (have_prev[g]) {
|
|
int changed = 0, first[4], nf = 0;
|
|
for (int i = 0; i < 256; i++) {
|
|
if (memcmp(&vdb_pal[g].ram[i*3], &prev[g][i*3], 3) != 0) {
|
|
if (nf < 4) first[nf++] = i;
|
|
changed++;
|
|
}
|
|
}
|
|
fprintf(vpx_fp, "# VDB pal%d reload: %d/256 entries changed", g, changed);
|
|
for (int k = 0; k < nf; k++) {
|
|
int i = first[k];
|
|
fprintf(vpx_fp, " [%d]%d,%d,%d->%d,%d,%d", i,
|
|
prev[g][i*3], prev[g][i*3+1], prev[g][i*3+2],
|
|
vdb_pal[g].ram[i*3], vdb_pal[g].ram[i*3+1], vdb_pal[g].ram[i*3+2]);
|
|
}
|
|
fprintf(vpx_fp, "\n");
|
|
} else {
|
|
fprintf(vpx_fp, "# VDB pal%d loaded %lu data bytes (first)\n",
|
|
g, vdb_data_count);
|
|
}
|
|
memcpy(prev[g], vdb_pal[g].ram, 768);
|
|
have_prev[g] = true;
|
|
fflush(vpx_fp);
|
|
}
|
|
if (vdb_paldump && vdb_data_count >= 768) {
|
|
/* keep the MOST-LIT snapshot per group (max non-black bytes), so
|
|
* the real content is captured no matter when it lands or which
|
|
* phase an animated palette is sampled in. */
|
|
static unsigned pal_max_nz[3] = { 0, 0, 0 };
|
|
int g = vdb_data_group;
|
|
unsigned nz = 0;
|
|
for (int i = 0; i < 768; i++) if (vdb_pal[g].ram[i]) nz++;
|
|
if (nz >= pal_max_nz[g]) {
|
|
pal_max_nz[g] = nz;
|
|
char path[600];
|
|
snprintf(path, sizeof path, "%s%d.rgb", vdb_paldump, g);
|
|
FILE *pf = fopen(path, "wb");
|
|
if (pf) { fwrite(vdb_pal[g].ram, 1, 768, pf); fclose(pf); }
|
|
}
|
|
}
|
|
}
|
|
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)");
|
|
if (vpx_fp && CurMode) { flush_run();
|
|
fprintf(vpx_fp, "# VDB framebuffer mode: 0x%X type=%d %ux%u pitch=%u"
|
|
" REAL scan_len*2=%u addr_add=%u real_start=0x%X\n",
|
|
(unsigned)CurMode->mode, (int)CurMode->type,
|
|
(unsigned)CurMode->swidth, (unsigned)CurMode->sheight,
|
|
(unsigned)CurMode->pitch,
|
|
(unsigned)(vga.config.scan_len * 2u),
|
|
(unsigned)vga.draw.address_add,
|
|
(unsigned)vga.config.real_start); fflush(vpx_fp); }
|
|
return; }
|
|
int g = vdb_group_of(off);
|
|
if (g < 0) return;
|
|
VDBPalette &p = vdb_pal[g];
|
|
switch (off - vdb_group_base(g)) { /* 0=mask 1=read-addr 2=write-addr 3=data */
|
|
case 2: /* write-address (index) */
|
|
vdb_flush_data();
|
|
p.waddr = v; p.sub = 0;
|
|
break;
|
|
case 3: /* 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 0: /* pixel-mask */
|
|
/* RP drives this VGA-DAC pixel-mask register actively (BT leaves
|
|
* it at 0xFF); log the VALUE on change so we can see how RP gates
|
|
* the palette index, and honor it in pal_draw under VDB_APPLYMASK. */
|
|
if (v != p.mask) {
|
|
p.mask = v;
|
|
if (vpx_fp) { vdb_flush_data(); flush_run();
|
|
fprintf(vpx_fp, "# VDB pixel-mask[g%d] = 0x%02X\n", g, v);
|
|
fflush(vpx_fp); }
|
|
} else {
|
|
p.mask = v;
|
|
}
|
|
break;
|
|
case 1: /* read-address */
|
|
p.raddr = v; p.sub = 0;
|
|
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 - vdb_group_base(g)) == 3) {
|
|
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);
|
|
/* pixel-mask defaults to 0xFF (no gating) so an un-set group decodes
|
|
* normally; the game overwrites it if it drives the mask (RP does). */
|
|
for (int g = 0; g < 3; g++) vdb_pal[g].mask = 0xFF;
|
|
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 *fsk = getenv("VPX_FIFOSOCK");
|
|
if (fsk && atoi(fsk) > 0) fifo_sock_init(atoi(fsk));
|
|
|
|
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();
|
|
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')) {
|
|
const char *pd = getenv("VDB_PALDUMP");
|
|
if (pd && pd[0]) vdb_paldump = pd;
|
|
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)");
|
|
}
|