Phase 3e: live texturing + serial receive-latency fixes for the RIO

Texturing (validated live -- game-live-textured.png, the ravine's brown dirt
terrain): the wire model is Division's intensity+ramp scheme. action 26
uploads 8-bit intensity texels ([node][nbytes][w][h] + rows; type 13 =
texture); texmap (12) references the texture; material (11) references its
texmap and a ramp (14: lo/hi RGB); texel color = lerp(lo, hi, i/255). The
backend bakes RGBA per material, uploads to GL, maps with wire UVs
(stride-5: floats 3-4; stride-8/9: floats 6-7).

Serial (directserial fork options, tracked in vpx-device/serialport/):
- rxpollus:<us> -- receive poll tick (stock 1ms); 100us discovers inbound
  bytes ~10x sooner. Validated: sim advanced, camera moved with real RIO.
- rxburst:<n>  -- stock DOSBox re-serializes received bytes at emulated wire
  speed (~1ms/byte at 9600) though they already paid wire time on the real
  cable; a 15-byte analog reply gained ~14ms, blowing the RIO's few-ms ACK
  window and dropping the game into its 15-second retry fallback
  (L4CTRL.CPP limit=15.0 //0.2). rxburst:16 delivers buffered bytes 16x
  faster. game_rio.conf: realport:COM1 rxpollus:100 rxburst:16.

Crash-on-advance fixed: *_TSHD.BGF terrain shadows live only in arena
subdirs the search path misses; null shadow renderable was dereferenced once
the sim moved. All 11 copied into VIDEO/GEO in the working image; game now
runs sustained (560+ frames, textured, no crash). Details in RIO-NOTES.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-03 16:43:21 -05:00
co-authored by Claude Opus 4.8
parent 99652d894b
commit 0bb493dd5f
9 changed files with 741 additions and 32 deletions
+21 -20
View File
@@ -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
+21 -3
View File
@@ -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 34; stride-8/9: floats 67). 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)
+32 -3
View File
@@ -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:<us>`** (501000, 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:<n>`** (164): 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+11 -1
View File
@@ -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"
+12
View File
@@ -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:<us>` (50-1000) - host-port receive poll tick in microseconds
(stock = 1000). Discovers inbound bytes sooner.
- `rxburst:<n>` (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`
@@ -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 <n> 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
@@ -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:<microseconds>".
float rx_poll_ms = 1.0f;
// Burst-delivery divisor ("rxburst:<n>"). 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
+188 -5
View File
@@ -435,7 +435,16 @@ static float rd_f32(const unsigned char *p) {
}
struct VCol { float c[3]; };
struct VPoly { float rgb[3]; std::vector<float> xyz; }; /* x,y,z triples */
struct VPoly {
float rgb[3];
std::vector<float> xyz; /* x,y,z triples */
std::vector<float> 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<unsigned char> 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<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<std::vector<int> > > polys;
std::map<unsigned, VCol> mat; /* material -> RGB */
std::map<unsigned, unsigned> ggmat; /* geogroup -> material */
@@ -467,11 +480,21 @@ static struct VScene {
std::map<unsigned, unsigned> inst_object; /* instance -> object */
std::map<unsigned, M16> dcs_mat; /* dcs -> local matrix */
std::map<unsigned, unsigned> 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<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;
/* 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) {
@@ -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<unsigned, TexImg> 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<unsigned, GLuint> gltex; /* matkey -> GL name */
static std::map<unsigned, unsigned> gltex_ver;
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);
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<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;
if ((size_t)t.w * t.h > t.px.size()) return 0;
std::map<unsigned, unsigned>::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<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;
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<unsigned, unsigned>::const_iterator gmi = S.ggmat.find(gg);
if (gmi != S.ggmat.end()) {
std::map<unsigned, VCol>::const_iterator mi = S.mat.find(gmi->second);
matname = gmi->second;
std::map<unsigned, VCol>::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<unsigned, std::vector<unsigned> >::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<float> &vv = vi->second;
const std::vector<float> *uv = 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;
for (size_t q = 0; q < pi->second.size(); q++) {
const std::vector<int> &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<float> &vl = S.verts[S.geom_node];
vl.clear();
std::vector<float> &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<std::vector<int> > &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<unsigned, unsigned>::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<unsigned, unsigned>::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++)