diff --git a/emulator/.gitignore b/emulator/.gitignore index e1eddc1..0fb37fe 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -1,20 +1,21 @@ -# Local working artifacts -dosbox-x/ -image/ -*.png -# Phase 3 evidence renders (keep: decoded from the captured VPX stream) -!divrgb-decoded.png -!divrgb-frame0.png -!divrgb-live-gl.png -!game-cockpit-decoded.png -!game-mech-decoded.png -!game-live-gl.png -dbx_out.txt -vpx*.txt -sweep_*.txt -dbx_err.txt -src/ -vpxlog.txt -sent_hex.txt -build.log -run_build.sh +# Local working artifacts +dosbox-x/ +image/ +*.png +# Phase 3 evidence renders (keep: decoded from the captured VPX stream) +!divrgb-decoded.png +!divrgb-frame0.png +!divrgb-live-gl.png +!game-cockpit-decoded.png +!game-mech-decoded.png +!game-live-gl.png +!game-live-textured.png +dbx_out.txt +vpx*.txt +sweep_*.txt +dbx_err.txt +src/ +vpxlog.txt +sent_hex.txt +build.log +run_build.sh diff --git a/emulator/PHASE3-PROGRESS.md b/emulator/PHASE3-PROGRESS.md index 8cb40bd..ffdcad5 100644 --- a/emulator/PHASE3-PROGRESS.md +++ b/emulator/PHASE3-PROGRESS.md @@ -156,9 +156,27 @@ traversal). Fixture: a 954-frame capture. The live backend (`vpxlog.cpp`) gained the same traversal: stride-aware geometry, dcs/instance/object link decode, cached world transforms per DCS, y-down projection. -Still to come: texturing (stride-8 UVs + action-26 texel maps), lighting from -the wire normals, per-frame articulation once the RIO drives the sim, LOD -selection by distance. +### 3e. Texturing (DONE) — the texture chain + +![live textured terrain](game-live-textured.png) + +The wire texture model is Division's intensity+ramp scheme (no palette ever +crosses the link): + +- **action 26** uploads texels: header `[node][nbytes][w][h]` then 8-bit + intensity rows (64×64…128×128; node type 13 = texture). +- **texmap (type 12)** flush references its texture node; **material (type + 11)** flush references its texmap *and* a **ramp (type 14)**: two RGBs + (lo/hi), e.g. dark red-brown → light tan for mech hulls. +- Final texel color = `lerp(ramp_lo, ramp_hi, intensity/255)`. + +The live backend bakes each textured material's RGBA once (texture × ramp), +uploads it to GL keyed by material, and maps it with the wire UVs (stride-5 +verts: floats 3–4; stride-8/9: floats 6–7). Result: the arena renders with +the ravine's actual brown dirt textures, live (`game-live-textured.png`). + +Still to come: lighting from the wire normals, per-frame articulation once +the RIO holds sync, LOD selection by distance. ## 3c. The full game runs through the live renderer (sync abort fixed) diff --git a/emulator/RIO-NOTES.md b/emulator/RIO-NOTES.md index f237b74..5f69063 100644 --- a/emulator/RIO-NOTES.md +++ b/emulator/RIO-NOTES.md @@ -30,13 +30,42 @@ With `core=dynamic + cycles=max` the RIO **stays in sync** (the user confirmed - `cycles=fixed 20000` / `150000` and `core=normal` were all too slow — the RIO dropped comms, faster at lower speeds. - The DOSBox-X serial path is already low-latency on **transmit** - (`directserial.cpp` `transmitByte` calls `SERIAL_sendchar` immediately). The - one remaining emulator-side latency is the **1 ms receive poll** - (`directserial.cpp:68`), reducible to ~0.25 ms if a future board needs it. + (`directserial.cpp` `transmitByte` calls `SERIAL_sendchar` immediately). - Prolific PL2303 has no adjustable `LatencyTimer` registry value (unlike FTDI); an FTDI adapter set to 1 ms latency would be the lowest-latency host option if ever needed. +## Receive-latency fork options (2026-07-03) + +Even with the fast CPU, intermittent timeouts remained and the game would +drop into its **15-second analog retry fallback** — the source shows +`limit = 15.0; // 0.2` in L4CTRL.CPP, so the shipped binary re-requests very +slowly once replies stop arriving ("the polling is really slow"). Two +emulator receive latencies were fixed with new `directserial` options: + +- **`rxpollus:`** (50–1000, stock 1000): host-port receive poll tick. + Stock DOSBox-X discovers inbound bytes on a 1 ms tick; 100 µs discovers + them ~10× sooner. First validated result: the sim advanced and the camera + moved in the render window. +- **`rxburst:`** (1–64): stock DOSBox re-serializes each received byte at + emulated wire speed (~1 ms/byte at 9600) even though the bytes already + paid their wire time on the physical cable and sit in the host buffer — a + 15-byte analog reply gained ~14 ms of artificial latency, single-handedly + blowing the RIO's few-ms ACK window. `rxburst:16` delivers buffered bytes + 16× faster. + +`game_rio.conf` uses `realport:COM1 rxpollus:100 rxburst:16`. + +## Crash-on-advance fixed: arena terrain shadows + +With the RIO in sync the sim advances and the game crashed dereferencing the +**mech terrain shadow** renderable: `*_TSHD.BGF` live only in +`VIDEO/GEO/ARENA/` + `…/POLAR/`, which the object search path misses, so +`thr_tshd.bgf` failed to load and left a null renderable that the moving sim +eventually touched. Fix: the 11 `*_TSHD.BGF` files are copied from `ARENA/` +into `VIDEO/GEO/` in the working image (reversible, one file each). After +the fix the game runs sustained (500+ frames, no crash). + ## Next wall: mission content (a game-data issue, not protocol/RIO) With the RIO feeding real input, the simulation advances — and then the game diff --git a/emulator/game-live-textured.png b/emulator/game-live-textured.png new file mode 100644 index 0000000..3671abf Binary files /dev/null and b/emulator/game-live-textured.png differ diff --git a/emulator/game_rio.conf b/emulator/game_rio.conf index c237480..e02efce 100644 --- a/emulator/game_rio.conf +++ b/emulator/game_rio.conf @@ -15,7 +15,17 @@ cputype=pentium cycles=max # RIO cockpit controls on COM1 (Phase 5 passthrough), plasma COM2 later. [serial] -serial1=directserial realport:COM1 +# Custom fork options for the RIO's few-ms ACK deadline: +# rxpollus:100 -- host-port receive poll tick in microseconds (stock=1000); +# discovers inbound bytes ~10x sooner. +# rxburst:16 -- deliver host-buffered bytes 16x faster than emulated wire +# speed. Stock DOSBox re-serializes each received byte at +# ~1ms (9600 baud) even though the bytes already paid wire +# time on the physical cable -- a 15-byte analog reply got +# +14ms of artificial latency, blowing the RIO's window and +# dropping the game into its 15-second retry fallback +# ("really slow polling"). +serial1=directserial realport:COM1 rxpollus:100 rxburst:16 serial2=disabled [autoexec] mount c "C:\VWE\TeslaRel410\ALPHA_1" diff --git a/emulator/vpx-device/serialport/README.md b/emulator/vpx-device/serialport/README.md new file mode 100644 index 0000000..f6a0981 --- /dev/null +++ b/emulator/vpx-device/serialport/README.md @@ -0,0 +1,12 @@ +# Modified DOSBox-X serial passthrough (RIO latency options) + +Drop these over `src/hardware/serialport/` in the DOSBox-X source tree +(v2026.06.02). They add two `directserial` options for the VWE RIO cockpit +board's few-millisecond ACK deadline (see ../../RIO-NOTES.md): + +- `rxpollus:` (50-1000) - host-port receive poll tick in microseconds + (stock = 1000). Discovers inbound bytes sooner. +- `rxburst:` (1-64) - deliver host-buffered received bytes n times faster + than emulated wire speed (they already paid wire time on the real cable). + +Usage: `serial1=directserial realport:COM1 rxpollus:100 rxburst:16` diff --git a/emulator/vpx-device/serialport/directserial.cpp b/emulator/vpx-device/serialport/directserial.cpp new file mode 100644 index 0000000..65b750e --- /dev/null +++ b/emulator/vpx-device/serialport/directserial.cpp @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2002-2021 The DOSBox Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +#include "dosbox.h" + +#if C_DIRECTSERIAL + +#include "logging.h" +#include "serialport.h" +#include "directserial.h" +#include "misc_util.h" +#include "pic.h" + +#include "libserial.h" + +/* This is a serial passthrough class. Its amazingly simple to */ +/* write now that the serial ports themselves were abstracted out */ + +CDirectSerial::CDirectSerial (Bitu id, CommandLine* cmd) + :CSerial (id, cmd) { + InstallationSuccessful = false; + comport = nullptr; + + rx_retry = 0; + rx_retry_max = 0; + rx_state = 0; + + std::string tmpstring; + if(!cmd->FindStringBegin("realport:",tmpstring,false)) return; + + LOG_MSG ("Serial%d: Opening %s", (int)(COMNUMBER), tmpstring.c_str()); + if(!SERIAL_open(tmpstring.c_str(), &comport)) { + char errorbuffer[256]; + SERIAL_getErrorString(errorbuffer, sizeof(errorbuffer)); + LOG_MSG("Serial%d: Serial Port \"%s\" could not be opened.", + (int)(COMNUMBER), tmpstring.c_str()); + LOG_MSG("%s",errorbuffer); + return; + } + + // rxdelay: How many milliseconds to wait before causing an + // overflow when the application is unresponsive. + if(getBituSubstring("rxdelay:", &rx_retry_max, cmd)) { + if(!(rx_retry_max<=10000)) { + rx_retry_max=0; + } + } + + // rxpollus: host-port receive polling tick in MICROSECONDS (50..1000, + // default 1000 = the stock 1ms tick). Lower values discover incoming + // bytes sooner -- needed for hard-real-time serial peripherals like the + // VWE RIO cockpit board, which drops comms on ACKs late by a few ms. + Bitu rx_poll_us = 0; + if(getBituSubstring("rxpollus:", &rx_poll_us, cmd)) { + if(rx_poll_us < 50) rx_poll_us = 50; + if(rx_poll_us > 1000) rx_poll_us = 1000; + rx_poll_ms = (float)rx_poll_us / 1000.0f; + LOG_MSG("Serial%d: receive poll tick %uus", (int)(COMNUMBER), + (unsigned)rx_poll_us); + } + + // rxburst: deliver host-buffered received bytes times faster than + // emulated wire speed (they already paid wire time on the real cable). + Bitu rx_burst = 0; + if(getBituSubstring("rxburst:", &rx_burst, cmd)) { + if(rx_burst < 1) rx_burst = 1; + if(rx_burst > 64) rx_burst = 64; + rx_burst_div = (float)rx_burst; + LOG_MSG("Serial%d: receive burst delivery x%u", (int)(COMNUMBER), + (unsigned)rx_burst); + } + + CSerial::Init_Registers(); + InstallationSuccessful = true; + rx_state = D_RX_IDLE; + setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); // receive tick +} + +CDirectSerial::~CDirectSerial () { + if(comport) SERIAL_close(comport); + // We do not use own events so we don't have to clear them. +} + +// CanReceive: true:UART part has room left +// doReceive: true:there was really a byte to receive +// rx_retry is incremented in polling events + +// in POLLING_EVENT: always add new polling event +// D_RX_IDLE + CanReceive + doReceive -> D_RX_WAIT , add RX_EVENT +// D_RX_IDLE + CanReceive + not doReceive -> D_RX_IDLE +// D_RX_IDLE + not CanReceive -> D_RX_BLOCKED, add RX_EVENT + +// D_RX_BLOCKED + CanReceive + doReceive -> D_RX_FASTWAIT, rem RX_EVENT +// rx_retry=0 , add RX_EVENT +// D_RX_BLOCKED + CanReceive + !doReceive -> D_RX_IDLE, rem RX_EVENT +// rx_retry=0 +// D_RX_BLOCKED + !CanReceive + doReceive + retry < max -> D_RX_BLOCKED, rx_retry++ +// D_RX_BLOCKED + !CanReceive + doReceive + retry >=max -> rx_retry=0 + +// to be continued... + +void CDirectSerial::handleUpperEvent(uint16_t type) { +/* +#if SERIAL_DEBUG + const char* s; + const char* s2; + switch(type) { + case SERIAL_POLLING_EVENT: s = "POLLING_EVENT"; break; + case SERIAL_RX_EVENT: s = "RX_EVENT"; break; + case SERIAL_TX_EVENT: s = "TX_EVENT"; break; + case SERIAL_THR_EVENT: s = "THR_EVENT"; break; + } + switch(rx_state) { + case D_RX_IDLE: s2 = "RX_IDLE"; break; + case D_RX_WAIT: s2 = "RX_WAIT"; break; + case D_RX_BLOCKED: s2 = "RX_BLOCKED"; break; + case D_RX_FASTWAIT: s2 = "RX_FASTWAIT"; break; + } + log_ser(dbg_aux,"Directserial: Event enter %s, %s",s,s2); +#endif + */ + + switch(type) { + case SERIAL_POLLING_EVENT: { + setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); + // update Modem input line states + switch(rx_state) { + case D_RX_IDLE: + if(CanReceiveByte()) { + if(doReceive()) { + // a byte was received + rx_state=D_RX_WAIT; + setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); + } // else still idle + } else { +#if SERIAL_DEBUG + if(!dbgmsg_poll_block) { + log_ser(dbg_aux,"Directserial: block on polling."); + dbgmsg_poll_block=true; + } +#endif + rx_state=D_RX_BLOCKED; + // have both delays (1ms + bytetime) + setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); + } + break; + case D_RX_BLOCKED: + // one timeout tick + if(!CanReceiveByte()) { + rx_retry++; + if(rx_retry>=rx_retry_max) { + // it has timed out: + rx_retry=0; + removeEvent(SERIAL_RX_EVENT); + if(doReceive()) { + // read away everything + // this will set overrun errors + while(doReceive()); + rx_state=D_RX_WAIT; + setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); + } else { + // much trouble about nothing + rx_state=D_RX_IDLE; + } + } // else wait further + } else { + // good: we can receive again +#if SERIAL_DEBUG + dbgmsg_poll_block=false; + dbgmsg_rx_block=false; +#endif + removeEvent(SERIAL_RX_EVENT); + rx_retry=0; + if(doReceive()) { + rx_state=D_RX_FASTWAIT; + setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); + } else { + // much trouble about nothing + rx_state=D_RX_IDLE; + } + } + break; + + case D_RX_WAIT: + case D_RX_FASTWAIT: + break; + } + updateMSR(); + break; + } + case SERIAL_RX_EVENT: { + switch(rx_state) { + case D_RX_IDLE: + LOG_MSG("internal error in directserial"); + break; + + case D_RX_BLOCKED: // try to receive + case D_RX_WAIT: + case D_RX_FASTWAIT: + if(CanReceiveByte()) { + // just works or unblocked + rx_retry=0; // not waiting anymore + if(doReceive()) { + if(rx_state==D_RX_WAIT) setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); + else { + // maybe unblocked + rx_state=D_RX_FASTWAIT; + setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); + } + } else { + // didn't receive anything + rx_state=D_RX_IDLE; + } + } else { + // blocking now or still blocked +#if SERIAL_DEBUG + if(rx_state==D_RX_BLOCKED) { + if(!dbgmsg_rx_block) { + log_ser(dbg_aux,"Directserial: rx still blocked (retry=%d)",rx_retry); + dbgmsg_rx_block=true; + } + } + + + + + + + else log_ser(dbg_aux,"Directserial: block on continued rx (retry=%d).",rx_retry); +#endif + setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); + rx_state=D_RX_BLOCKED; + } + + break; + } + updateMSR(); + break; + } + case SERIAL_TX_EVENT: { + // Maybe echo circuit works a bit better this way + if(rx_state==D_RX_IDLE && CanReceiveByte()) { + if(doReceive()) { + // a byte was received + rx_state=D_RX_WAIT; + setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); + } + } + ByteTransmitted(); + updateMSR(); + break; + } + case SERIAL_THR_EVENT: { + ByteTransmitting(); + setEvent(SERIAL_TX_EVENT,bytetime*1.1f); + break; + } + } + /* + #if SERIAL_DEBUG + switch(type) { + case SERIAL_POLLING_EVENT: s = "POLLING_EVENT"; break; + case SERIAL_RX_EVENT: s = "RX_EVENT"; break; + case SERIAL_TX_EVENT: s = "TX_EVENT"; break; + case SERIAL_THR_EVENT: s = "THR_EVENT"; break; + } + switch(rx_state) { + case D_RX_IDLE: s2 = "RX_IDLE"; break; + case D_RX_WAIT: s2 = "RX_WAIT"; break; + case D_RX_BLOCKED: s2 = "RX_BLOCKED"; break; + case D_RX_FASTWAIT: s2 = "RX_FASTWAIT"; break; + } + log_ser(dbg_aux,"Directserial: Event exit %s, %s",s,s2); +#endif*/ +} + +bool CDirectSerial::doReceive() { + int value = SERIAL_getextchar(comport); + if(value) { + receiveByteEx((uint8_t)(value&0xff),(uint8_t)((value&0xff00)>>8)); + return true; + } + return false; +} + +// updatePortConfig is called when emulated app changes the serial port +// parameters baudrate, stopbits, number of databits, parity. +void CDirectSerial::updatePortConfig (uint16_t divider, uint8_t lcr) { + uint8_t parity = 0; + + switch ((lcr & 0x38)>>3) { + case 0x1: parity='o'; break; + case 0x3: parity='e'; break; + case 0x5: parity='m'; break; + case 0x7: parity='s'; break; + default: parity='n'; break; + } + + uint8_t bytelength = (lcr & 0x3)+5; + + // baudrate + Bitu baudrate; + if(divider==0) baudrate=115200u; + else baudrate = 115200u / divider; + + // stopbits + uint8_t stopbits; + if (lcr & 0x4) { + if (bytelength == 5) stopbits = SERIAL_15STOP; + else stopbits = SERIAL_2STOP; + } else stopbits = SERIAL_1STOP; + + if(!SERIAL_setCommParameters(comport, (int)baudrate, (char)parity, (char)stopbits, (char)bytelength)) { +#if SERIAL_DEBUG + log_ser(dbg_aux,"Serial port settings not supported by host." ); +#endif + LOG_MSG ("Serial%d: Desired serial mode not supported (%d,%d,%c,%d)", + (int)(COMNUMBER),(int)baudrate,(int)bytelength,parity,(int)stopbits); + } + CDirectSerial::setRTSDTR(getRTS(), getDTR()); +} + +void CDirectSerial::updateMSR () { + int new_status = SERIAL_getmodemstatus(comport); + + setCTS((new_status&SERIAL_CTS)? true:false); + setDSR((new_status&SERIAL_DSR)? true:false); + setRI((new_status&SERIAL_RI)? true:false); + setCD((new_status&SERIAL_CD)? true:false); +} + +void CDirectSerial::transmitByte (uint8_t val, bool first) { + if(!SERIAL_sendchar(comport, (char)val)) + LOG_MSG("Serial%d: COM port error: write failed!", (int)COMNUMBER); + if(first) setEvent(SERIAL_THR_EVENT, bytetime/8); + else setEvent(SERIAL_TX_EVENT, bytetime); +} + + +// setBreak(val) switches break on or off +void CDirectSerial::setBreak (bool value) { + SERIAL_setBREAK(comport,value); +} + +// updateModemControlLines(mcr) sets DTR and RTS. +void CDirectSerial::setRTSDTR(bool rts, bool dtr) { + SERIAL_setRTS(comport,rts); + SERIAL_setDTR(comport,dtr); +} + +void CDirectSerial::setRTS(bool val) { + SERIAL_setRTS(comport,val); +} + +void CDirectSerial::setDTR(bool val) { + SERIAL_setDTR(comport,val); +} + +#endif diff --git a/emulator/vpx-device/serialport/directserial.h b/emulator/vpx-device/serialport/directserial.h new file mode 100644 index 0000000..4c1645b --- /dev/null +++ b/emulator/vpx-device/serialport/directserial.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2002-2021 The DOSBox Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +// include guard +#ifndef DOSBOX_DIRECTSERIAL_WIN32_H +#define DOSBOX_DIRECTSERIAL_WIN32_H + +#include "dosbox.h" + +#if C_DIRECTSERIAL + +#define DIRECTSERIAL_AVAILIBLE +#include "serialport.h" + +#include "libserial.h" + +class CDirectSerial : public CSerial { +public: + CDirectSerial(Bitu id, CommandLine* cmd); + ~CDirectSerial(); + + void updatePortConfig(uint16_t divider, uint8_t lcr) override; + void updateMSR() override; + void transmitByte(uint8_t val, bool first) override; + void setBreak(bool value) override; + + void setRTSDTR(bool rts, bool dtr) override; + void setRTS(bool val) override; + void setDTR(bool val) override; + void handleUpperEvent(uint16_t type) override; + +private: + COMPORT comport; + + Bitu rx_state = 0; +#define D_RX_IDLE 0 +#define D_RX_WAIT 1 +#define D_RX_BLOCKED 2 +#define D_RX_FASTWAIT 3 + + Bitu rx_retry; // counter of retries (every millisecond) + Bitu rx_retry_max; // how many POLL_EVENTS to wait before causing + // an overrun error. + // Host-port receive polling tick in milliseconds (default 1.0). + // Latency-critical hardware (VWE RIO cockpit board: drops comms when an + // ACK is late by a few ms) needs incoming bytes discovered faster than + // the stock 1ms tick; configurable via "rxpollus:". + float rx_poll_ms = 1.0f; + // Burst-delivery divisor ("rxburst:"). Stock DOSBox re-serializes + // received bytes at emulated wire speed (bytetime per byte) even though + // they already paid their wire time on the physical cable and are + // sitting in the host buffer. n>1 delivers buffered bytes n times + // faster, cutting multi-byte reply latency (RIO analog packets). + float rx_burst_div = 1.0f; + bool doReceive(); + +#if SERIAL_DEBUG + bool dbgmsg_poll_block = false; + bool dbgmsg_rx_block = false; +#endif + +}; + +#endif // C_DIRECTSERIAL +#endif // include guard diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index ec98559..dfcd404 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -435,7 +435,16 @@ static float rd_f32(const unsigned char *p) { } struct VCol { float c[3]; }; -struct VPoly { float rgb[3]; std::vector xyz; }; /* x,y,z triples */ +struct VPoly { + float rgb[3]; + std::vector xyz; /* x,y,z triples */ + std::vector uv; /* u,v pairs (empty = untextured) */ + unsigned matkey; /* material name for texture lookup, 0 = none */ + VPoly() : matkey(0) {} +}; +/* Baked RGBA texture (texture intensity x material ramp), keyed by material. + * Shared with the GL thread under rt_lock; ver bumps force re-upload. */ +struct TexImg { int w, h; unsigned ver; std::vector rgba; }; struct VFrame { bool valid; float bg[3]; @@ -456,9 +465,13 @@ struct VFrame { struct M16 { float m[16]; }; /* row-major 4x4, row 3 = translation */ +struct VRamp { float lo[3], hi[3]; }; +struct VTex { int w, h; unsigned ver; std::vector px; }; + static struct VScene { std::map type; /* name -> node type */ std::map > verts; /* geometry -> xyz */ + std::map > uvs; /* geometry -> u,v */ std::map > > polys; std::map mat; /* material -> RGB */ std::map ggmat; /* geogroup -> material */ @@ -467,11 +480,21 @@ static struct VScene { std::map inst_object; /* instance -> object */ std::map dcs_mat; /* dcs -> local matrix */ std::map dcs_parent; /* dcs_link child->parent */ + /* texture chain: material -> texmap -> texture, colorized by the + * material's ramp (type 14: lo/hi RGB; texels are intensities) */ + std::map tex; /* texture node -> texels */ + std::map texmap_tex; /* texmap -> texture */ + std::map mat_texmap; /* material -> texmap */ + std::map mat_ramp; /* material -> ramp */ + std::map ramp; /* ramp -> lo/hi */ VFrame view; /* view/bg/camera state */ /* multi-burst assembly (stride-aware: game verts are 3..9 floats each) */ unsigned geom_node; size_t geom_need, geom_stride; bool geom_active; std::vector geom_acc; unsigned conn_node, conn_npolys, conn_loop; bool conn_active; + /* action-26 texel upload assembly */ + unsigned tex_node; size_t tex_need; bool tex_active; + std::vector tex_acc; } S; static void m16_id(M16 &o) { @@ -516,6 +539,7 @@ static CRITICAL_SECTION rt_lock; static VFrame rt_pending; static bool rt_new = false; static unsigned long rt_frames = 0; +static std::map rt_texs; /* material -> baked RGBA (rt_lock) */ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { glViewport(0, 0, cw, ch); @@ -545,14 +569,55 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) { glDisable(GL_CULL_FACE); glDisable(GL_LIGHTING); glShadeModel(GL_FLAT); + /* upload any new/changed baked material textures */ + static std::map gltex; /* matkey -> GL name */ + static std::map gltex_ver; + EnterCriticalSection(&rt_lock); + std::map texs = rt_texs; /* small; copy out */ + LeaveCriticalSection(&rt_lock); + for (std::map::const_iterator it = texs.begin(); + it != texs.end(); ++it) { + std::map::const_iterator vi = + gltex_ver.find(it->first); + if (vi != gltex_ver.end() && vi->second == it->second.ver) continue; + GLuint id; + std::map::const_iterator gi = gltex.find(it->first); + if (gi == gltex.end()) { glGenTextures(1, &id); gltex[it->first] = id; } + else id = gi->second; + glBindTexture(GL_TEXTURE_2D, id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, it->second.w, it->second.h, + 0, GL_RGBA, GL_UNSIGNED_BYTE, &it->second.rgba[0]); + gltex_ver[it->first] = it->second.ver; + } + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + unsigned bound = 0; for (size_t i = 0; i < f.polys.size(); i++) { const VPoly &p = f.polys[i]; - glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]); + bool tex = p.matkey && gltex.count(p.matkey) && + p.uv.size() * 3 == p.xyz.size() * 2; + if (tex) { + if (bound != p.matkey) { + glBindTexture(GL_TEXTURE_2D, gltex[p.matkey]); + bound = p.matkey; + } + glEnable(GL_TEXTURE_2D); + glColor3f(1.0f, 1.0f, 1.0f); + } else { + glDisable(GL_TEXTURE_2D); + glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]); + } glBegin(GL_POLYGON); - for (size_t v = 0; v + 2 < p.xyz.size(); v += 3) + for (size_t v = 0; v + 2 < p.xyz.size(); v += 3) { + if (tex) glTexCoord2f(p.uv[v / 3 * 2], p.uv[v / 3 * 2 + 1]); glVertex3f(p.xyz[v], p.xyz[v + 1], p.xyz[v + 2]); + } glEnd(); } + glDisable(GL_TEXTURE_2D); } SwapBuffers(dc); } @@ -610,13 +675,59 @@ static DWORD WINAPI rt_main(LPVOID) { } /* ---- scene-graph message decode ----------------------------------------- */ + +/* Bake material's texture (intensity) through its ramp into RGBA and stash it + * for the GL thread. Returns the material key if textured, else 0. */ +static std::map baked_ver; /* material -> baked tex ver */ +static unsigned bake_material_tex(unsigned matname) { + std::map::const_iterator tmi = S.mat_texmap.find(matname); + if (tmi == S.mat_texmap.end()) return 0; + std::map::const_iterator txi = S.texmap_tex.find(tmi->second); + if (txi == S.texmap_tex.end()) return 0; + std::map::const_iterator ti = S.tex.find(txi->second); + if (ti == S.tex.end() || ti->second.px.empty()) return 0; + const VTex &t = ti->second; + if ((size_t)t.w * t.h > t.px.size()) return 0; + std::map::const_iterator bi = baked_ver.find(matname); + if (bi != baked_ver.end() && bi->second == t.ver) return matname; + float lo[3] = { 0, 0, 0 }, hi[3] = { 1, 1, 1 }; + std::map::const_iterator ri = S.mat_ramp.find(matname); + if (ri != S.mat_ramp.end()) { + std::map::const_iterator rr = S.ramp.find(ri->second); + if (rr != S.ramp.end()) { + memcpy(lo, rr->second.lo, sizeof lo); + memcpy(hi, rr->second.hi, sizeof hi); + } + } + TexImg img; + img.w = t.w; img.h = t.h; img.ver = t.ver; + img.rgba.resize((size_t)t.w * t.h * 4); + for (size_t i = 0; i < (size_t)t.w * t.h; i++) { + float a = t.px[i] / 255.0f; + for (int c = 0; c < 3; c++) { + float v = lo[c] + (hi[c] - lo[c]) * a; + img.rgba[i * 4 + c] = + (unsigned char)(v < 0 ? 0 : v > 1 ? 255 : v * 255.0f + 0.5f); + } + img.rgba[i * 4 + 3] = 255; + } + EnterCriticalSection(&rt_lock); + rt_texs[matname] = img; + LeaveCriticalSection(&rt_lock); + baked_ver[matname] = t.ver; + return matname; +} + static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) { VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */ + unsigned matname = 0; std::map::const_iterator gmi = S.ggmat.find(gg); if (gmi != S.ggmat.end()) { - std::map::const_iterator mi = S.mat.find(gmi->second); + matname = gmi->second; + std::map::const_iterator mi = S.mat.find(matname); if (mi != S.mat.end()) col = mi->second; } + unsigned texkey = matname ? bake_material_tex(matname) : 0; std::map >::const_iterator ci = S.children.find(gg); if (ci == S.children.end()) return; @@ -628,10 +739,16 @@ static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) { pi = S.polys.find(geo); if (vi == S.verts.end() || pi == S.polys.end()) continue; const std::vector &vv = vi->second; + const std::vector *uv = NULL; + std::map >::const_iterator ui = + S.uvs.find(geo); + if (ui != S.uvs.end() && ui->second.size() * 3 == vv.size() * 2) + uv = &ui->second; for (size_t q = 0; q < pi->second.size(); q++) { const std::vector &idx = pi->second[q]; VPoly poly; memcpy(poly.rgb, col.c, sizeof poly.rgb); + poly.matkey = (texkey && uv) ? texkey : 0; for (size_t j = 0; j < idx.size(); j++) { size_t o = (size_t)idx[j] * 3; if (o + 2 >= vv.size()) continue; @@ -641,6 +758,10 @@ static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) { poly.xyz.push_back(out[0]); poly.xyz.push_back(out[1]); poly.xyz.push_back(out[2]); + if (poly.matkey) { + poly.uv.push_back((*uv)[(size_t)idx[j] * 2]); + poly.uv.push_back((*uv)[(size_t)idx[j] * 2 + 1]); + } } if (poly.xyz.size() >= 9) f.polys.push_back(poly); } @@ -702,18 +823,38 @@ static void scene_burst(const unsigned char *p, size_t n) { S.geom_acc.push_back(rd_f32(d + o)); if (S.geom_acc.size() >= S.geom_need * S.geom_stride) { std::vector &vl = S.verts[S.geom_node]; - vl.clear(); + std::vector &tl = S.uvs[S.geom_node]; + vl.clear(); tl.clear(); + /* uv offset within a vertex record by stride: 5 = xyz+uv, + * 8/9 = xyz+normal+uv(+extra) */ + size_t uvo = (S.geom_stride >= 8) ? 6 : + (S.geom_stride == 5) ? 3 : 0; for (size_t i = 0; i + 2 < S.geom_need * S.geom_stride; i += S.geom_stride) { vl.push_back(S.geom_acc[i]); vl.push_back(S.geom_acc[i + 1]); vl.push_back(S.geom_acc[i + 2]); + if (uvo && i + uvo + 1 < S.geom_acc.size()) { + tl.push_back(S.geom_acc[i + uvo]); + tl.push_back(S.geom_acc[i + uvo + 1]); + } else { tl.push_back(0); tl.push_back(0); } } S.geom_acc.clear(); S.geom_active = false; } return; } + if (S.tex_active && action == 26) { + for (size_t o = 0; o < nb; o++) S.tex_acc.push_back(d[o]); + if (S.tex_acc.size() >= S.tex_need) { + VTex &t = S.tex[S.tex_node]; + t.px.assign(S.tex_acc.begin(), S.tex_acc.begin() + S.tex_need); + t.ver++; + S.tex_acc.clear(); + S.tex_active = false; + } + return; + } if (S.conn_active && action == 25) { std::vector > &pl = S.polys[S.conn_node]; size_t nw = nb / 4; @@ -737,6 +878,33 @@ static void scene_burst(const unsigned char *p, size_t n) { if (t == 11 && nb >= 92) { /* material diffuse */ VCol c = { { rd_f32(d + 48), rd_f32(d + 52), rd_f32(d + 56) } }; S.mat[name] = c; + /* texmap / ramp refs: identify fields by node type */ + for (size_t o = 8; o + 3 < nb; o += 4) { + unsigned val = rd_u32(d + o); + if (!val || val == 0xFFFFFFFFu) continue; + std::map::const_iterator ti = + S.type.find(val); + if (ti == S.type.end()) continue; + if (ti->second == 12) S.mat_texmap[name] = val; + else if (ti->second == 14) S.mat_ramp[name] = val; + } + } else if (t == 12 && nb >= 12) { /* texmap -> texture */ + for (size_t o = 8; o + 3 < nb; o += 4) { + unsigned val = rd_u32(d + o); + if (!val || val == 0xFFFFFFFFu) continue; + std::map::const_iterator ti = + S.type.find(val); + if (ti != S.type.end() && ti->second == 13) { + S.texmap_tex[name] = val; + break; + } + } + } else if (t == 14 && nb >= 36) { /* ramp lo/hi RGB */ + VRamp &r = S.ramp[name]; + for (int i = 0; i < 3; i++) { + r.lo[i] = rd_f32(d + 8 + i * 4); + r.hi[i] = rd_f32(d + 20 + i * 4); + } } else if (t == 9 && nb >= 80) { /* geogroup material */ S.ggmat[name] = rd_u32(d + 64); } else if (t == 3 && nb >= 104) { /* view */ @@ -792,6 +960,21 @@ static void scene_burst(const unsigned char *p, size_t n) { S.polys[S.conn_node].clear(); } break; + case 26: /* texel upload header [node][nbytes][w][h]... (8bpp) */ + if (nb >= 16) { + unsigned node = rd_u32(d), nbytes = rd_u32(d + 4); + unsigned tw = rd_u32(d + 8), th = rd_u32(d + 12); + if (nbytes && nbytes <= (1u << 20) && tw && th && + tw <= 1024 && th <= 1024 && (size_t)tw * th <= nbytes) { + S.tex_node = node; + S.tex_need = nbytes; + S.tex_active = true; + S.tex_acc.clear(); + VTex &t = S.tex[node]; + t.w = (int)tw; t.h = (int)th; + } + } + break; case 31: /* per-frame camera [?][view][3x3 rows][eye] */ if (nb >= 56) { for (int i = 0; i < 9; i++)