VPX render: port the restoration gallery shading model; renderer collab doc

Device renderer (vpxlog.cpp, commit-copy of the fork source):
- View-flush fog decode (enable/near/far/rgb at d+72.., mode 5 linear,
  garbage-guarded; VPX_NOFOG=1 diagnostic) applied per fragment.
- Full dpl_MATERIAL parse: emissive/ambient/diffuse/opacity/specular/
  shininess (opacity+specular parsed, not yet applied); fixed-offset
  texture/ramp refs with lazy resolution, rebake on material/ramp reflush.
- Gallery shading model (restoration/gallery_template.html) as a GLSL 1.10
  program over the existing immediate-mode path, fixed-function fallback:
  c = (matAmb*sceneAmb + matDiff*mix(ramp.lo,ramp.hi,acc))*tex + matEmis,
  linear fog with immunity knob. Textured polys use an identity light-ramp
  (texels already baked through the material ramp). Newell flat normals for
  polys without wire normals. Interim sun/ambient until the light node is
  decoded: VPX_AMBIENT / VPX_SUN env overrides.

RENDERER-COLLAB.md: shared working doc with the external-renderer team --
verified FIFO action/node tables, struct offsets, scene assembly and
coordinate conventions, available data taps, open questions (incl. the
type-7 object-vs-light flush conflict).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-04 16:41:16 -05:00
co-authored by Claude Fable 5
parent e2005502ff
commit dabc204d0d
2 changed files with 639 additions and 43 deletions
+148
View File
@@ -0,0 +1,148 @@
# Division Renderer Collaboration Log
Shared working doc for the two renderer efforts against the VPX/Division
hardware interface:
- **In-process renderer** — GL HLE inside the DOSBox-X fork's VPX device
(`src/src/hardware/vpxlog.cpp`, commit-copy `vpx-device/vpxlog.cpp`).
Maintained by Cyd + Claude (this repo's sessions).
- **External renderer** — separate approach by the graphics developer +
their Claude instance. *(Fill in your section below.)*
**How to use this file:** each session (human or Claude) reads it at start,
appends to its own section and the Log, and commits. Don't rewrite the other
team's sections — append questions/answers instead. Commit messages prefixed
`collab:` so both sides can `git log --grep=collab:` for changes.
---
## Ground-truth sources (in priority order)
1. `sda4/DPL3/LIBDPL/DPLTYPES.H` + `DPL.H` — the dVS/DPL node structs and API.
2. `sda4/DPL3/VRENDER/` — Division's own i860 renderer source (rasterizer
semantics; the ramp/texture/light questions below should be settled here).
3. `ALPHA_1/REL410/BT/BTDPL.INI` — per-arena ground truth the game actually
loads: `fog=start end r g b`, `light=r g b pitch yaw roll`, `ambient=`,
`clip=`, `objectpath=` (arena content dirs), keyed by egg map/time/weather.
4. `restoration/` — WebGL re-render of the on-disk scene formats
(`divformats.py` parsers, `gallery_template.html` shading model). Proven
against 26 recovered scenes; its mesh shader is the reference shading
model (see below).
5. Live wire captures: `VPXLOG=<file>` logs every port I/O; scratch tooling
(`dump_nodes.py`-style parsers) has decoded node flushes from fifodumps.
## VPX board interface — verified on live captures
Register offsets (host side of the i860 board FIFO link):
`0` inputData (board→host), `1` outputData (host→board), `2` inputStatus
(bit0 = byte available), `3` outputStatus (always ready), `4/5` FIFO data
port link B low/high bytes, `16` resetRoot, `17` analyseRoot.
### Action codes (FIFO message stream)
| action | meaning |
|---|---|
| 1 | create `[type][name]` |
| 3 | flush `[name][type][struct]` — struct offsets below are into the payload `d`, where `d+8` = struct offset 0 |
| 7 | dcs_link `[parent][child]` — articulation tree |
| 9 | draw_scene — frame commit |
| 11 | list_add `[parent][child]` |
| 23 | set_geom_verts header `[name][0][n_verts][stride_floats]` + vertex bursts; stride 3..16 floats/vert: xyz, +uv at 3 (stride 5) or 6 (stride ≥8), +normal at 3 when stride ≥8 |
| 25 | set_geom_conns header `[name][n_polys][loop_len][0]` + index words (closing duplicate dropped) |
| 26 | texel upload header `[node][nbytes][w][h]` + texel bursts (4/8/16 bpp, bpp inferred from byte/texel ratio) |
| 31 | per-frame camera `[?][view][3x3 rows][eye]` |
### Node types (create/flush `type` field)
| type | node | verified layout (flush payload `d`) |
|---|---|---|
| 2 | zone | raw-logged only |
| 3 | view | win l/b/r/t/dist `d+24..40`, vw/vh `d+44/48`, near/far `d+52/56`, back_color `d+60..68`, **fog enable/mode `d+72`, near `d+76`, far `d+80`, rgb `d+84..92`** (mode 5 = linear; game ANIMATES fog and re-flushes ~27×; boot flushes carry heap garbage — sanity-check floats) |
| 4 | instance | object ref scanned at `d+8..` (resolves to a type-7 node) |
| 5 | dcs | 4×4 row-major at floats 4..19 (`d+16..`); row 3 = translation; world = local × parent (row-vector convention) |
| 6 | lmodel | raw-logged only |
| 7 | **object** (instance target in the scene-graph walker) — **CONFLICT:** the flush handler currently raw-dumps `t==7` as a *light* candidate ("t7 light=" lines in the tex log). Both can't be right; the real dpl_LIGHT wire type is still unidentified. Reconcile against DPLTYPES.H + a live dump. |
| 9 | geogroup | material ref at `d+64` |
| 11 | material | texture ref `d+8`, ramp ref `d+20`, emissive `d+24..32`, ambient `d+36..44`, diffuse `d+48..56`, opacity `d+60`, specular `d+72..80`, shininess `d+84` (84-byte struct; refs resolve lazily — a ref may precede its create) |
| 12 | dpl_TEXTURE params | texmap ref `d+8`, then minify/magnify/alpha/wrap_u/wrap_v/detail (u32s) + u0/v0/du/dv/anim_time (floats) + anim_bhv — logged (`t12` lines), **semantics not yet applied** |
| 13 | texmap header | texels ptr, u_size, v_size, bits_per_texel (+ hwareSize/hwareOffs/bilinear) |
| 14 | ramp | lo RGB `d+8..16`, hi RGB `d+20..28`; texels are intensities colorized through the material's ramp |
### Scene assembly & conventions
- Draw hierarchy: DCS (type 5) children (via list_add) → instance (4) →
object (7) → **first LOD child** (host keeps active LOD at list head; the
wire carries no switch distances) → geogroups (9) → geometry.
- World is **y-down** (DCS matrices carry reflections); Division screen x is
mirrored vs GL eye x (flip x after projection).
- Vertices reach the renderer already in world space; the modelview holds
only the camera (action 31: row-major world→eye rotation + eye pos,
`eye_p = R·(p e)`).
- Frustum from view win[]: `glFrustum(win[l/r/b/t] · near / win[4], …)`.
- **The camera only goes live after the first analog (throttle/stick)
input** — until then the game submits no worklist and the screen is the
back_color clear. Hands-off runs look like "solid sky"; check the radar
head (win0 BMP dump) to confirm the mission actually loaded.
## In-process renderer — status (2026-07-04)
The device now runs the restoration gallery's shading model verbatim as a
GLSL 1.10 program (fixed-function fallback; stderr prints
`VPX render: gallery shading model active (GLSL)`):
```
acc = Σ_i lightCol_i · max(dot(N, lightDir_i), 0) (world space, 2 dir lights)
lit = mix(ramp.lo, ramp.hi, clamp(acc, 0, 1))
c = (matAmb·sceneAmb + matDiff·lit) · tex + matEmis
out = mix(fogColor, c, max(clamp((fogEndd)/(fogEndfogStart), 0, 1), immune))
```
Interim until the light node is decoded: hardcoded sun + sceneAmb 0.45,
overridable via `VPX_AMBIENT="r,g,b"` / `VPX_SUN="r,g,b,pitch,yaw"` (deg,
y-down). Fog comes from the type-3 view flush. Opacity/specular parsed but
not applied. Polys without wire normals get Newell flat face normals.
**Ramp interpretation (open):** wire textures are intensity texels baked
through the material ramp at upload; the gallery applies the ramp to *light*
(disk textures are RGB). The device currently avoids double-application by
using an identity light-ramp on textured polys. VRENDER source should settle
whether Division ramps texels, light, or both.
## Data taps available to an external renderer (today)
- `VPXLOG=<path>` (host env): full port-I/O log incl. every FIFO byte —
replayable; parse with the action/type tables above.
- `VPX_DUMPDIR=<dir>` + touch `<dir>/DUMP`: per-head BMP dumps (`win<g>.bmp`,
g=0 radar, 3/4 MFD heads, 5-9 exploded singles).
- Device tex log: `t7`/`t12`/`t13` raw flush lines for undecoded nodes.
- `RIO_TAP=<path>`: serial wire tap (not renderer-relevant, but proves rig
liveness).
- **Offer:** if a structured per-frame scene export (JSON/binary worklist
after graph assembly, i.e. post-DCS world-space polys + materials) would
feed your renderer better than raw FIFO, ask here — straightforward to add
behind an env var.
## Open questions (either side, answer in place with date)
1. Real dpl_LIGHT wire type + layout (type 7 conflict above). BTDPL.INI
`light=` lines give expected values to match against.
2. Type-12 texture param semantics (wrap/clamp/uv-window/animate) — cause of
smeared wall textures?
3. Ramp on texel vs ramp on light (VRENDER).
4. Terrain + sky in arena missions: what content/nodes carry them? (Live
arena1 renders structures but almost no ground; sky is back_color only.)
5. Opacity blending order/mode (glass cockpits, effects).
6. Scene ambient: which node carries it at runtime (lmodel type 6?).
## External renderer — approach & status
*(Yours — please fill in: approach, inputs consumed, what you need from the
device side, decisions taken. Append, date-stamp.)*
## Log
- **2026-07-04 (Claude, in-process side):** doc created. Gallery shading
model ported into the device and verified live in arena1 (lit faces,
plausible wire fog). Full material parse landed. Next in-process steps:
light node decode, type-12 params, restoration-toolkit reference render of
arena1 content.
+491 -43
View File
@@ -434,6 +434,7 @@ static VDBPalette vdb_pal[3];
#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) |
@@ -444,13 +445,34 @@ static float rd_f32(const unsigned char *p) {
}
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];
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 = unlit) */
std::vector<float> nrm; /* nx,ny,nz triples (empty = flat normal) */
unsigned matkey; /* material name for texture lookup, 0 = none */
VPoly() : matkey(0) {}
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. */
@@ -460,14 +482,18 @@ struct VFrame {
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) */
std::vector<VPoly> polys;
VFrame() : valid(false), nearp(2), farp(12000), vw(832), vh(512),
VFrame() : valid(false), nearp(2), farp(12000), fog(false),
fogn(0), fogf(0), vw(832), vh(512),
has_cam(false), ydown(false) {
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;
}
@@ -484,7 +510,7 @@ static struct VScene {
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, VCol> mat; /* material -> RGB */
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 */
@@ -556,6 +582,183 @@ 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";
static const char *rsh_fs_src =
"varying vec3 vN; varying vec2 vUV; varying float vDist;\n"
"uniform vec3 uAmbScene, uMatAmb, uMatDiff, uMatEmis, uFogColor, uViewFwd;\n"
"uniform vec3 uLightDir0, uLightCol0, uLightDir1, uLightCol1;\n"
"uniform vec3 uRamp0, uRamp1, uFog;\n" /* uFog: start, end, immune */
"uniform float uHasTex; uniform sampler2D uTex;\n"
"void main() {\n"
" vec3 N = normalize(vN);\n"
" if (dot(N, uViewFwd) > 0.0) N = -N;\n"
" vec3 acc = uLightCol0 * max(dot(N, -uLightDir0), 0.0)\n"
" + uLightCol1 * max(dot(N, -uLightDir1), 0.0);\n"
" vec3 lit = mix(uRamp0, uRamp1, clamp(acc, 0.0, 1.0));\n"
" vec3 tex = mix(vec3(1.0), texture2D(uTex, vUV).rgb, uHasTex);\n"
" vec3 c = (uMatAmb * uAmbScene + uMatDiff * lit) * tex + uMatEmis;\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"
" gl_FragColor = vec4(mix(uFogColor, c, fr), 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: gallery 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 */
static float rsh_amb[3] = { 0.45f, 0.45f, 0.45f };
static float rsh_sundir[3] = { -0.3412f, 0.8286f, -0.3899f }; /* travel dir */
static float rsh_suncol[3] = { 0.80f, 0.80f, 0.78f };
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) {
glViewport(0, 0, cw, ch);
glClearColor(f.bg[0], f.bg[1], f.bg[2], 1.0f);
@@ -583,9 +786,50 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glShadeModel(GL_SMOOTH);
/* directional sun (world coords; modelview is loaded, so GL maps it
* into eye space). World is y-down: up = -y. */
{
/* 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);
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 };
@@ -601,6 +845,14 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
/* 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);
@@ -621,6 +873,7 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
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;
@@ -632,15 +885,52 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
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);
glColor3f(1.0f, 1.0f, 1.0f);
if (!shade) glColor3f(1.0f, 1.0f, 1.0f);
} else {
glDisable(GL_TEXTURE_2D);
glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]);
if (!shade) glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]);
}
bool lit = !p.nrm.empty();
if (lit) glEnable(GL_LIGHTING); else glDisable(GL_LIGHTING);
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]);
@@ -649,6 +939,7 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
}
glEnd();
}
if (shade) rsh.use(0);
glDisable(GL_TEXTURE_2D);
glDisable(GL_LIGHTING);
}
@@ -958,6 +1249,19 @@ static DWORD WINAPI rt_main(LPVOID) {
/* ---- 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 */
@@ -969,9 +1273,24 @@ static unsigned bake_material_tex(unsigned matname) {
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;
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()) {
@@ -982,10 +1301,15 @@ static unsigned bake_material_tex(unsigned matname) {
}
}
TexImg img;
img.w = t.w; img.h = t.h; img.ver = t.ver;
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 < (size_t)t.w * t.h; i++) {
float a = t.px[i] / 255.0f;
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] =
@@ -1001,13 +1325,23 @@ static unsigned bake_material_tex(unsigned matname) {
}
static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) {
VCol col = { { 1.0f, 0.0f, 1.0f } }; /* missing = magenta */
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, VCol>::const_iterator mi = S.mat.find(matname);
if (mi != S.mat.end()) col = mi->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 =
@@ -1037,8 +1371,17 @@ static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) {
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);
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;
@@ -1149,6 +1492,21 @@ static void scene_burst(const unsigned char *p, size_t n) {
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;
}
@@ -1185,29 +1543,90 @@ static void scene_burst(const unsigned char *p, size_t n) {
case 3: { /* flush [name][type][struct] */
if (nb < 8) break;
unsigned name = rd_u32(d), t = rd_u32(d + 4);
if (t == 11 && nb >= 92) { /* material diffuse */
VCol c = { { rd_f32(d + 48), rd_f32(d + 52), rd_f32(d + 56) } };
S.mat[name] = c;
/* texmap / ramp refs: identify fields by node type */
for (size_t o = 8; o + 3 < nb; o += 4) {
unsigned val = rd_u32(d + o);
if (!val || val == 0xFFFFFFFFu) continue;
std::map<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;
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;
}
} 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;
}
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;
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 == 2) && nb >= 8) { /* lmodel / zone */
FILE *tl = tex_log_fp();
if (tl) {
fprintf(tl, "t%d %s=%08x raw=", (int)t,
t == 6 ? "lmodel" : "zone", 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];
@@ -1215,6 +1634,10 @@ static void scene_burst(const unsigned char *p, size_t n) {
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 */
@@ -1225,6 +1648,22 @@ static void scene_burst(const unsigned char *p, size_t n) {
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);
@@ -1270,12 +1709,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) */
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 <= nbytes) {
tw <= 1024 && th <= 1024 &&
(size_t)tw * th <= (size_t)nbytes * 2) {
S.tex_node = node;
S.tex_need = nbytes;
S.tex_active = true;