VPX device: dpl3-revive shading port + wire lights; fix game-path yaw mirror

- rt_draw shading replaced with the dpl3-revive software-rasterizer model
  (vrview.py draw(), proven on our own arena captures): lit = sceneAmb +
  sum |N.L|*col double-sided; c = emis + diff*lit (tex*base*1.275 when
  textured); Division 10-bit-DAC gamma 1/1.25 on output. Default light rig
  matches theirs (ambient 0.35 + one 0.65 sun) until wire lights arrive.
- Real wire lights decoded per their SceneCache.rebuild(): lmodel type-6 /
  light type-0xe bodies (dcs @d+12, type @d+16: 2=ambient 3=directional,
  rgb @d+20), directional aim = light DCS +Z row, sanity-clamped; frame
  carries summed ambient + up to 2 suns.
- Projection: the hardcoded post-projection X flip (SMPTE-era) made the
  game view steer mirrored (turn right, view swings left). Game path
  (ydown) now projects without it -- matches dpl3-revive, which has no
  lateral mirror; demo/test path keeps the flip. User-verified: native
  window steering now correct.

Fork source (emulator/src/src/hardware/vpxlog.cpp) in sync; rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-06 13:34:17 -05:00
co-authored by Claude Fable 5
parent 0f5b2e28da
commit 1872799c7a
+159 -23
View File
@@ -488,14 +488,20 @@ struct VFrame {
bool has_cam;
bool ydown; /* game world is y-down (DCS reflections) */
float rot[9], eye[3]; /* row-major rotation; eye = R*(p - e) */
/* real wire lights (dpl3-revive: lmodel type-2 ambient + type-3 dir suns).
* samb = summed scene ambient; nlit dir lights (dir toward light + col). */
float samb[3];
int nlit;
float ldir[2][3], lcol[2][3];
std::vector<VPoly> polys;
VFrame() : valid(false), nearp(2), farp(12000), fog(false),
fogn(0), fogf(0), vw(832), vh(512),
has_cam(false), ydown(false) {
has_cam(false), ydown(false), nlit(0) {
bg[0] = bg[1] = bg[2] = 0;
fogc[0] = fogc[1] = fogc[2] = 0;
win[0] = -1; win[1] = -0.6153846f; win[2] = 1; win[3] = 0.6153846f;
win[4] = 1.3f;
samb[0] = samb[1] = samb[2] = 0.0f;
}
};
@@ -515,8 +521,14 @@ static struct VScene {
std::map<unsigned, std::vector<unsigned> > children;
/* game (full DPL) hierarchy: instance placement + articulation */
std::map<unsigned, unsigned> inst_object; /* instance -> object */
std::map<unsigned, unsigned> inst_w3; /* instance -> display mode word3 */
std::map<unsigned, M16> dcs_mat; /* dcs -> local matrix */
std::map<unsigned, unsigned> dcs_parent; /* dcs_link child->parent */
/* wire lights: dpl3-revive decode -- lmodel (type 6) / light (type 0xe)
* body: dcs @d+12, light_type @d+16 (2=ambient, 3=directional), rgb
* @d+20..28. directional aim = the light DCS's +Z world row. */
struct VLight { unsigned dcs; int ltype; float rgb[3]; };
std::map<unsigned, VLight> lights; /* light node -> params */
/* texture chain: material -> texmap -> texture, colorized by the
* material's ramp (type 14: lo/hi RGB; texels are intensities) */
std::map<unsigned, VTex> tex; /* texture node -> texels */
@@ -639,24 +651,34 @@ static const char *rsh_vs_src =
" vUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;\n"
" gl_Position = gl_ProjectionMatrix * v;\n"
"}\n";
/* Shading model ported 1:1 from the dpl3-revive software rasterizer
* (patha/vrview.py draw(), validated against our own arena captures):
* lit = sceneAmbient + sum_i |dot(N, Ldir_i)| * Lcol_i (DPL double-sided)
* base = matEmis + matDiff * lit
* c = hasTex ? tex * base * 1.275 : base (tex*base/200, base was *255)
* out = pow(fog(c), 1/1.25) (Division 10-bit-DAC gamma)
* Material ambient and the light-ramp are intentionally unused here -- the
* proven Division look folds ambient into the diffuse response and does not
* ramp the base colour (the ramp only re-colours intensity texels at bake). */
static const char *rsh_fs_src =
"varying vec3 vN; varying vec2 vUV; varying float vDist;\n"
"uniform vec3 uAmbScene, uMatAmb, uMatDiff, uMatEmis, uFogColor, uViewFwd;\n"
"uniform vec3 uAmbScene, uMatDiff, uMatEmis, uFogColor;\n"
"uniform vec3 uLightDir0, uLightCol0, uLightDir1, uLightCol1;\n"
"uniform vec3 uRamp0, uRamp1, uFog;\n" /* uFog: start, end, immune */
"uniform vec3 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"
" vec3 lit = uAmbScene\n"
" + uLightCol0 * abs(dot(N, uLightDir0))\n"
" + uLightCol1 * abs(dot(N, uLightDir1));\n"
" vec3 base = uMatEmis + uMatDiff * lit;\n"
" vec3 tex = texture2D(uTex, vUV).rgb;\n"
" vec3 c = mix(base, tex * base * 1.275, uHasTex);\n"
" float fr = clamp((uFog.y - vDist) / max(uFog.y - uFog.x, 0.001),\n"
" 0.0, 1.0);\n"
" fr = max(fr, uFog.z);\n"
" gl_FragColor = vec4(mix(uFogColor, c, fr), 1.0);\n"
" c = mix(uFogColor, c, fr);\n"
" gl_FragColor = vec4(pow(clamp(c, 0.0, 1.0), vec3(1.0/1.25)), 1.0);\n"
"}\n";
static GLuint rsh_compile(GLenum kind, const char *src,
@@ -731,15 +753,17 @@ static void rsh_init(void) {
u1i(gu(pr, "uTex"), 0);
rsh.use(0);
rsh.prog = pr;
fprintf(stderr, "VPX render: gallery shading model active (GLSL)\n");
fprintf(stderr, "VPX render: dpl3-revive shading model active (GLSL)\n");
}
/* scene lighting until the type-7 light node decode lands (step 2):
* defaults keep the previous hardcoded sun; VPX_AMBIENT="r,g,b" and
* VPX_SUN="r,g,b,pitch,yaw" (degrees, y-down world) override for tuning */
static float rsh_amb[3] = { 0.45f, 0.45f, 0.45f };
/* dpl3-revive default light rig (vrview.py): scene ambient 0.35 + one sun 0.65
* when the wire carries no lights (real light-node decode = next increment). */
static float rsh_amb[3] = { 0.35f, 0.35f, 0.35f };
static float rsh_sundir[3] = { -0.3412f, 0.8286f, -0.3899f }; /* travel dir */
static float rsh_suncol[3] = { 0.80f, 0.80f, 0.78f };
static float rsh_suncol[3] = { 0.65f, 0.65f, 0.65f };
static void rsh_env(void) {
static bool done = false;
if (done) return;
@@ -769,10 +793,13 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
double wd = f.win[4] != 0 ? f.win[4] : 1.3;
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
/* Division screen x runs opposite to GL eye x (SMPTE pattern comes
* out mirrored otherwise) -- flip x after projection. The game world
* is additionally y-down (its DCS matrices carry a reflection). */
glScalef(-1.0f, f.ydown ? -1.0f : 1.0f, 1.0f);
/* Demo/test path: Division screen x runs opposite to GL eye x
* (SMPTE pattern comes out mirrored otherwise) -- flip x after
* projection. Game path (ydown): NO x flip -- dpl3-revive's proven
* pipeline has no lateral mirror, and the flip made yaw read
* inverted (turn right, view swings left); the world's y-down DCS
* reflection still needs the y flip. */
glScalef(f.ydown ? 1.0f : -1.0f, f.ydown ? -1.0f : 1.0f, 1.0f);
glFrustum(f.win[0] * n / wd, f.win[2] * n / wd,
f.win[1] * n / wd, f.win[3] * n / wd, n, fa);
glMatrixMode(GL_MODELVIEW);
@@ -801,6 +828,29 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
glDisable(GL_FOG);
glDisable(GL_LIGHTING);
rsh.use(rsh.prog);
/* real wire lights (lmodel) when the frame carries any; else the
* dpl3-revive default rig (scene ambient 0.35 + one sun 0.65). */
bool wl = f.nlit > 0 ||
(f.samb[0] + f.samb[1] + f.samb[2]) > 0.0f;
if (wl) {
rsh.u3f(rsh.uAmbScene, f.samb[0], f.samb[1], f.samb[2]);
rsh.u3f(rsh.uLightDir0,
f.nlit >= 1 ? f.ldir[0][0] : 0.0f,
f.nlit >= 1 ? f.ldir[0][1] : 1.0f,
f.nlit >= 1 ? f.ldir[0][2] : 0.0f);
rsh.u3f(rsh.uLightCol0,
f.nlit >= 1 ? f.lcol[0][0] : 0.0f,
f.nlit >= 1 ? f.lcol[0][1] : 0.0f,
f.nlit >= 1 ? f.lcol[0][2] : 0.0f);
rsh.u3f(rsh.uLightDir1,
f.nlit >= 2 ? f.ldir[1][0] : 0.0f,
f.nlit >= 2 ? f.ldir[1][1] : 1.0f,
f.nlit >= 2 ? f.ldir[1][2] : 0.0f);
rsh.u3f(rsh.uLightCol1,
f.nlit >= 2 ? f.lcol[1][0] : 0.0f,
f.nlit >= 2 ? f.lcol[1][1] : 0.0f,
f.nlit >= 2 ? f.lcol[1][2] : 0.0f);
} else {
rsh.u3f(rsh.uAmbScene, rsh_amb[0], rsh_amb[1], rsh_amb[2]);
rsh.u3f(rsh.uLightDir0, rsh_sundir[0], rsh_sundir[1],
rsh_sundir[2]);
@@ -808,6 +858,7 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
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]);
@@ -866,8 +917,10 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
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);
/* the i860 board point-sampled (VRENDER SCANLINE.SS: one fld.l per
* pixel, no bilinear tap) -- NEAREST is authentic (dpl3-revive) */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, it->second.w, it->second.h,
@@ -1420,6 +1473,48 @@ static void scene_publish_frame(void) {
* dcs_link articulation tree. */
std::map<unsigned, M16> cache;
std::map<unsigned, bool> gg_done;
/* resolve wire lights: ambient (type 2) sums into samb; directional
* (type 3) aim = the light DCS's +Z world row (dpl3-revive). Up to two
* directional slots feed the shader; if none, rt_draw uses its rig. */
for (std::map<unsigned, VScene::VLight>::const_iterator li =
S.lights.begin(); li != S.lights.end(); ++li) {
const VScene::VLight &L = li->second;
if (L.ltype == 2) {
for (int i = 0; i < 3; i++) f.samb[i] += L.rgb[i];
} else if (L.ltype == 3 && f.nlit < 2) {
M16 w;
dcs_world(L.dcs, cache, w);
float dx = w.m[8], dy = w.m[9], dz = w.m[10]; /* +Z world row */
float n = sqrtf(dx * dx + dy * dy + dz * dz);
if (n > 1e-6f) {
int s = f.nlit++;
f.ldir[s][0] = dx / n; f.ldir[s][1] = dy / n;
f.ldir[s][2] = dz / n;
for (int i = 0; i < 3; i++) f.lcol[s][i] = L.rgb[i];
}
}
}
{ /* periodic light-decode diagnostic (stderr): how many type-6/0xe
* light nodes exist vs how many decoded, sampled throughout so it
* catches the mission-loaded scene (not just boot). */
static int fc = 0;
if (++fc % 120 == 1) {
int n6 = 0, ne = 0;
for (std::map<unsigned, unsigned>::const_iterator ti =
S.type.begin(); ti != S.type.end(); ++ti) {
if (ti->second == 6) n6++;
if (ti->second == 0xe) ne++;
}
fprintf(stderr, "VPX lights@f%d: type6=%d typeE=%d decoded=%d "
"dir=%d amb(%.2f,%.2f,%.2f)\n", fc, n6, ne,
(int)S.lights.size(), f.nlit,
f.samb[0], f.samb[1], f.samb[2]);
if (f.nlit)
fprintf(stderr, " L0 dir(%.2f,%.2f,%.2f) col(%.2f,%.2f,%.2f)\n",
f.ldir[0][0], f.ldir[0][1], f.ldir[0][2],
f.lcol[0][0], f.lcol[0][1], f.lcol[0][2]);
}
}
for (std::map<unsigned, std::vector<unsigned> >::const_iterator di =
S.children.begin(); di != S.children.end(); ++di) {
if (S.type.count(di->first) == 0 || S.type[di->first] != 5) continue;
@@ -1427,6 +1522,8 @@ static void scene_publish_frame(void) {
dcs_world(di->first, cache, world);
for (size_t i = 0; i < di->second.size(); i++) {
unsigned inst = di->second[i];
/* (hidden-instance w3 skip removed -- our w3 offset was culling
* the ground plane; revisit once the offset is confirmed) */
std::map<unsigned, unsigned>::const_iterator oi =
S.inst_object.find(inst);
if (oi == S.inst_object.end()) continue;
@@ -1564,6 +1661,13 @@ static void scene_burst(const unsigned char *p, size_t n) {
float op = rd_f32(d + 60), sh = rd_f32(d + 84);
m.opacity = (op >= 0.0f && op <= 1.0f) ? op : 1.0f;
m.shin = (sh >= 0.0f && sh <= 1024.0f) ? sh : 0.0f;
/* dpl3-revive: exact ambient==(1,0,0) AND diffuse==(1,0,0) is
* the shipped build's UNSET-material marker (on every
* .B2Z-default material), not a colour -- render as white. */
if (m.amb[0] == 1.0f && m.amb[1] == 0.0f && m.amb[2] == 0.0f &&
m.diff[0] == 1.0f && m.diff[1] == 0.0f && m.diff[2] == 0.0f) {
for (int i = 0; i < 3; i++) { m.amb[i] = 1.0f; m.diff[i] = 1.0f; }
}
S.mat[name] = m;
unsigned tref = rd_u32(d + 8), rref = rd_u32(d + 20);
if (tref && tref != 0xFFFFFFFFu) S.mat_texmap[name] = tref;
@@ -1602,11 +1706,38 @@ static void scene_burst(const unsigned char *p, size_t n) {
fprintf(tl, "\n");
fflush(tl);
}
} else if ((t == 6 || t == 2) && nb >= 8) { /* lmodel / zone */
} else if (t == 6 || t == 0xe) { /* lmodel / light */
{ static bool dl = false; if (!dl) { dl = true;
fprintf(stderr, "VPX lightnode t=%u nb=%lu:",
(unsigned)t, (unsigned long)nb);
for (size_t o = 8; o + 3 < nb && o < 56; o += 4)
fprintf(stderr, " [%u]%08x/%g", (unsigned)o,
rd_u32(d + o), rd_f32(d + o));
fprintf(stderr, "\n"); } }
/* dpl3-revive light decode: dcs @d+12, light_type @d+16
* (2=ambient, 3=directional), rgb @d+20..28. Directional aim
* is resolved to the light DCS's +Z world row at frame
* assembly. The sanity range rejects the game's 4 stray
* type-0xe 32B "vehicle lamp" bodies (heap-garbage rgb). */
if (nb >= 32) {
unsigned ldcs = rd_u32(d + 12);
int ltype = (int)rd_u32(d + 16);
float r = rd_f32(d + 20), g = rd_f32(d + 24),
b = rd_f32(d + 28);
if ((ltype == 2 || ltype == 3) &&
r == r && g == g && b == b && /* NaN guard */
r >= 0.0f && g >= 0.0f && b >= 0.0f &&
r <= 100.0f && g <= 100.0f && b <= 100.0f) {
VScene::VLight L;
L.dcs = ldcs; L.ltype = ltype;
L.rgb[0] = r; L.rgb[1] = g; L.rgb[2] = b;
S.lights[name] = L;
}
}
} else if (t == 2 && nb >= 8) { /* zone (raw log) */
FILE *tl = tex_log_fp();
if (tl) {
fprintf(tl, "t%d %s=%08x raw=", (int)t,
t == 6 ? "lmodel" : "zone", name);
fprintf(tl, "t2 zone=%08x raw=", name);
for (size_t o = 8; o + 3 < nb && o < 40; o += 4)
fprintf(tl, " %08x", rd_u32(d + o));
fprintf(tl, "\n");
@@ -1670,6 +1801,10 @@ static void scene_burst(const unsigned char *p, size_t n) {
for (int r = 0; r < 3; r++) mm.m[r * 4 + 3] = 0.0f;
mm.m[15] = 1.0f;
} else if (t == 4) { /* instance: object ref */
/* display mode (dpl3-revive): word3 @d+16 -- 3=normal,
* 2=billboard, 0/1=HIDDEN until armed (the parked player mech
* is w3=1 pre-translocation). Stored for the assembly skip. */
if (nb >= 20) S.inst_w3[name] = rd_u32(d + 16);
for (size_t o = 8; o + 3 < nb; o += 4) {
unsigned val = rd_u32(d + o);
if (val && val != 0xFFFFFFFFu) {
@@ -1753,7 +1888,8 @@ static void scene_burst(const unsigned char *p, size_t n) {
static void scene_reset(void) {
S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear();
S.ggmat.clear(); S.children.clear();
S.inst_object.clear(); S.dcs_mat.clear(); S.dcs_parent.clear();
S.inst_object.clear(); S.inst_w3.clear(); S.dcs_mat.clear();
S.dcs_parent.clear(); S.lights.clear();
S.view = VFrame();
S.geom_active = false; S.conn_active = false;
S.geom_acc.clear();