From dabc204d0d600f0a6678c13156534efceec50ad4 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sat, 4 Jul 2026 16:41:16 -0500 Subject: [PATCH] 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 --- emulator/RENDERER-COLLAB.md | 148 +++++++++ emulator/vpx-device/vpxlog.cpp | 534 ++++++++++++++++++++++++++++++--- 2 files changed, 639 insertions(+), 43 deletions(-) create mode 100644 emulator/RENDERER-COLLAB.md diff --git a/emulator/RENDERER-COLLAB.md b/emulator/RENDERER-COLLAB.md new file mode 100644 index 0000000..cc81b3b --- /dev/null +++ b/emulator/RENDERER-COLLAB.md @@ -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=` 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((fogEnd−d)/(fogEnd−fogStart), 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=` (host env): full port-I/O log incl. every FIFO byte — + replayable; parse with the action/type tables above. +- `VPX_DUMPDIR=` + touch `/DUMP`: per-head BMP dumps (`win.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=`: 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. diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 13adf64..9ebd350 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -434,6 +434,7 @@ static VDBPalette vdb_pal[3]; #include #include #include +#include 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 xyz; /* x,y,z triples */ std::vector uv; /* u,v pairs (empty = untextured) */ - std::vector nrm; /* nx,ny,nz triples (empty = unlit) */ + std::vector 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 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 > uvs; /* geometry -> u,v */ std::map > nrms; /* geometry -> nx,ny,nz */ std::map > > polys; - std::map mat; /* material -> RGB */ + std::map mat; /* material -> shading */ std::map ggmat; /* geogroup -> material */ std::map > 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 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 gltex; /* matkey -> GL name */ static std::map gltex_ver; + static std::map > 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 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 >::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=: 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 baked_ver; /* material -> baked tex ver */ @@ -969,9 +1273,24 @@ static unsigned bake_material_tex(unsigned matname) { 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; + 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::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::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::const_iterator gmi = S.ggmat.find(gg); if (gmi != S.ggmat.end()) { matname = gmi->second; - std::map::const_iterator mi = S.mat.find(matname); - if (mi != S.mat.end()) col = mi->second; + std::map::const_iterator mi = S.mat.find(matname); + if (mi != S.mat.end()) gmat = mi->second; + std::map::const_iterator rmi = + S.mat_ramp.find(matname); + if (rmi != S.mat_ramp.end()) { + std::map::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 >::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 &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::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::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::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;