Phase 3f: lighting from wire normals; defuse shipped-in debug crash

Lighting: stride-8/9 vertices carry a normal (floats 3-5, uv at 6-7). The
backend transforms normals by the instance rotation and lights with a single
directional sun (GL_LIGHT0, smooth, two-sided, GL_COLOR_MATERIAL). Terrain
and hulls shade instead of reading flat.

LOD: the lod flush has no switch distances -- the host keeps the active LOD
at the child-list head and re-orders over the wire, so children[0] is right
and LOD changes come through as the sim runs.

The crash that halted the game once the RIO drove the sim was not ours: fault
at 0047E1D1 writing 0xFFFFFFFF is the RIO driver's DISABLE_AND_DIE debug
macro (PCSPAK.ASM, DIE_ON_ERROR=1), which deliberately faults on a serial-tx
anomaly -- error 3 = body char >0x7F during a RIO packet, i.e. a glitch on a
board reset. Release build (DIE_ON_ERROR=0) compiles it out and recovers
(and al,07Fh). patch_btl4opt.py NOPs all 12 sites by signature (with .orig
backup) = release behavior; game then survives RIO resets (1200+ frames).
(ALPHA_1 is git-ignored, so the tool ships, not the patched binary.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-03 17:13:46 -05:00
co-authored by Claude Opus 4.8
parent 0bb493dd5f
commit 08f71eb471
5 changed files with 139 additions and 8 deletions
+58 -6
View File
@@ -439,6 +439,7 @@ struct VPoly {
float rgb[3];
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) */
unsigned matkey; /* material name for texture lookup, 0 = none */
VPoly() : matkey(0) {}
};
@@ -472,6 +473,7 @@ 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<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, unsigned> ggmat; /* geogroup -> material */
@@ -512,6 +514,10 @@ static void m16_xform(const M16 &w, const float *v, float *o) {
for (int c = 0; c < 3; c++)
o[c] = v[0] * w.m[c] + v[1] * w.m[4 + c] + v[2] * w.m[8 + c] + w.m[12 + c];
}
static void m16_xform_dir(const M16 &w, const float *v, float *o) {
for (int c = 0; c < 3; c++) /* rotation only (normals) */
o[c] = v[0] * w.m[c] + v[1] * w.m[4 + c] + v[2] * w.m[8 + c];
}
/* world transform of a dcs: local * parent_world (row-vector convention) */
static void dcs_world(unsigned dcs, std::map<unsigned, M16> &cache, M16 &out,
int depth = 0) {
@@ -567,8 +573,22 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
glTranslatef(-f.eye[0], -f.eye[1], -f.eye[2]);
glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_LIGHTING);
glShadeModel(GL_FLAT);
glShadeModel(GL_SMOOTH);
/* 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 };
glLightfv(GL_LIGHT0, GL_POSITION, lpos);
glLightfv(GL_LIGHT0, GL_AMBIENT, lamb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, ldif);
glEnable(GL_LIGHT0);
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
glEnable(GL_NORMALIZE);
}
/* upload any new/changed baked material textures */
static std::map<unsigned, GLuint> gltex; /* matkey -> GL name */
static std::map<unsigned, unsigned> gltex_ver;
@@ -610,14 +630,18 @@ static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
glDisable(GL_TEXTURE_2D);
glColor3f(p.rgb[0], p.rgb[1], p.rgb[2]);
}
bool lit = !p.nrm.empty();
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]);
if (lit) glNormal3f(p.nrm[v], p.nrm[v + 1], p.nrm[v + 2]);
glVertex3f(p.xyz[v], p.xyz[v + 1], p.xyz[v + 2]);
}
glEnd();
}
glDisable(GL_TEXTURE_2D);
glDisable(GL_LIGHTING);
}
SwapBuffers(dc);
}
@@ -740,10 +764,18 @@ static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) {
if (vi == S.verts.end() || pi == S.polys.end()) continue;
const std::vector<float> &vv = vi->second;
const std::vector<float> *uv = NULL;
const std::vector<float> *nr = 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;
std::map<unsigned, std::vector<float> >::const_iterator ni =
S.nrms.find(geo);
if (ni != S.nrms.end() && ni->second.size() == vv.size()) {
/* only meaningful if not all-zero (stride < 8 stores zeros) */
for (size_t z = 0; z < ni->second.size(); z++)
if (ni->second[z] != 0.0f) { nr = &ni->second; break; }
}
for (size_t q = 0; q < pi->second.size(); q++) {
const std::vector<int> &idx = pi->second[q];
VPoly poly;
@@ -762,7 +794,17 @@ static void emit_geogroup(VFrame &f, unsigned gg, const M16 *world) {
poly.uv.push_back((*uv)[(size_t)idx[j] * 2]);
poly.uv.push_back((*uv)[(size_t)idx[j] * 2 + 1]);
}
if (nr) {
float nout[3];
if (world) m16_xform_dir(*world, &(*nr)[o], nout);
else { nout[0] = (*nr)[o]; nout[1] = (*nr)[o + 1];
nout[2] = (*nr)[o + 2]; }
poly.nrm.push_back(nout[0]);
poly.nrm.push_back(nout[1]);
poly.nrm.push_back(nout[2]);
}
}
if (poly.nrm.size() != poly.xyz.size()) poly.nrm.clear();
if (poly.xyz.size() >= 9) f.polys.push_back(poly);
}
}
@@ -790,7 +832,10 @@ static void scene_publish_frame(void) {
std::map<unsigned, std::vector<unsigned> >::const_iterator li =
S.children.find(oi->second);
if (li == S.children.end() || li->second.empty()) continue;
unsigned lod = li->second[0]; /* highest LOD */
/* First LOD child: the host maintains the active LOD at the list
* head (the wire lod flush carries no switch distances -- LOD
* selection is host-side). */
unsigned lod = li->second[0];
std::map<unsigned, std::vector<unsigned> >::const_iterator ggi =
S.children.find(lod);
if (ggi == S.children.end()) continue;
@@ -824,11 +869,13 @@ static void scene_burst(const unsigned char *p, size_t n) {
if (S.geom_acc.size() >= S.geom_need * S.geom_stride) {
std::vector<float> &vl = S.verts[S.geom_node];
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) */
std::vector<float> &nl = S.nrms[S.geom_node];
vl.clear(); tl.clear(); nl.clear();
/* record layout by stride: 5 = xyz+uv,
* 8/9 = xyz + normal(3..5) + uv(6..7) (+extra) */
size_t uvo = (S.geom_stride >= 8) ? 6 :
(S.geom_stride == 5) ? 3 : 0;
bool has_n = S.geom_stride >= 8;
for (size_t i = 0; i + 2 < S.geom_need * S.geom_stride;
i += S.geom_stride) {
vl.push_back(S.geom_acc[i]);
@@ -838,6 +885,11 @@ static void scene_burst(const unsigned char *p, size_t n) {
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); }
if (has_n && i + 5 < S.geom_acc.size()) {
nl.push_back(S.geom_acc[i + 3]);
nl.push_back(S.geom_acc[i + 4]);
nl.push_back(S.geom_acc[i + 5]);
} else { nl.push_back(0); nl.push_back(0); nl.push_back(0); }
}
S.geom_acc.clear();
S.geom_active = false;