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:
@@ -10,6 +10,7 @@ image/
|
||||
!game-mech-decoded.png
|
||||
!game-live-gl.png
|
||||
!game-live-textured.png
|
||||
!game-live-lit.png
|
||||
dbx_out.txt
|
||||
vpx*.txt
|
||||
sweep_*.txt
|
||||
|
||||
@@ -175,8 +175,29 @@ uploads it to GL keyed by material, and maps it with the wire UVs (stride-5
|
||||
verts: floats 3–4; stride-8/9: floats 6–7). Result: the arena renders with
|
||||
the ravine's actual brown dirt textures, live (`game-live-textured.png`).
|
||||
|
||||
Still to come: lighting from the wire normals, per-frame articulation once
|
||||
the RIO holds sync, LOD selection by distance.
|
||||
### 3f. Lighting + the shipped-in debug crash
|
||||
|
||||
Lighting: stride-8/9 vertices carry a **normal** at floats 3–5 (uv at 6–7).
|
||||
The backend transforms them by the instance world matrix (rotation only) and
|
||||
lights with a single directional "sun" (`GL_LIGHT0`, smooth shading,
|
||||
two-sided, `GL_COLOR_MATERIAL` so texture/material color survives). Terrain
|
||||
and hulls now shade instead of reading flat.
|
||||
|
||||
LOD note: the `lod` flush carries **no switch distances** — the host game
|
||||
keeps the active LOD at the head of the object's child list and re-orders it
|
||||
over the wire, so `children[0]` is correct and LOD changes as the sim runs
|
||||
come through for free.
|
||||
|
||||
**The crash that halted the game with the RIO in sync was not ours.** Fault
|
||||
`Exception 0E at 0047E1D1` writing to `0xFFFFFFFF` is a debug landmine the
|
||||
build shipped with: the RIO serial driver's `DISABLE_AND_DIE` macro
|
||||
(`PCSPAK.ASM`, gated by `DIE_ON_ERROR equ 1`) deliberately faults on any
|
||||
serial-transmit anomaly. Error code 3 = a body char > 0x7F during a RIO
|
||||
packet (`PCSPAK.ASM:1630`) — i.e. a serial glitch during a board reset. The
|
||||
release build (`DIE_ON_ERROR equ 0`) compiles it out and takes the recovery
|
||||
path (`and al,07Fh`). `patch_btl4opt.py` finds all 12 sites by their 14-byte
|
||||
signature and NOPs them — exactly the release behavior — with a `.orig`
|
||||
backup. After patching, the game survives RIO resets (1200+ frames).
|
||||
|
||||
## 3c. The full game runs through the live renderer (sync abort fixed)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Neutralize the shipped-in debug crash macros in BTL4OPT.EXE.
|
||||
|
||||
The Rel 4.10 BattleTech binary was built with the RIO serial driver's
|
||||
`DIE_ON_ERROR equ 1` diagnostic enabled (CODE/RP/MUNGA_L4/PCSPAK.ASM). Its
|
||||
`DISABLE_AND_DIE` macro deliberately faults ("crash loudly to the debugger")
|
||||
on any serial-transmit anomaly:
|
||||
|
||||
push eax; push edx
|
||||
mov edx, 0FFFFFFFFh ; crash loudly
|
||||
mov eax, <error-code>
|
||||
mov [edx], eax ; write to 0xFFFFFFFF -> Exception 0E
|
||||
|
||||
On the real pod a clean cockpit harness never tripped these; through the
|
||||
DOSBox-X serial passthrough a RIO glitch (e.g. during a board reset) hits
|
||||
error 3 (PCSPAK.ASM:1630, "body char > 0x7F") and halts the whole game. The
|
||||
release build (`DIE_ON_ERROR equ 0`) compiles the macro to nothing and lets
|
||||
the normal recovery path (`and al,07Fh`) continue.
|
||||
|
||||
This tool finds every DISABLE_AND_DIE site by its exact 14-byte signature and
|
||||
replaces it with NOPs -- exactly the release-build behavior -- writing a
|
||||
<name>.orig backup first. Idempotent.
|
||||
|
||||
Usage: patch_btl4opt.py <path-to-BTL4OPT.EXE>
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
SIG = re.compile(
|
||||
rb'\x50\x52\xBA\xFF\xFF\xFF\xFF\xB8(.)\x00\x00\x00\x89\x02', re.DOTALL)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit(__doc__)
|
||||
path = sys.argv[1]
|
||||
data = bytearray(open(path, 'rb').read())
|
||||
sites = list(SIG.finditer(bytes(data)))
|
||||
if not sites:
|
||||
print("no DISABLE_AND_DIE sites found (already patched?)")
|
||||
return
|
||||
import os
|
||||
bak = path + '.orig'
|
||||
if not os.path.exists(bak):
|
||||
open(bak, 'wb').write(data)
|
||||
print(f"backup written: {bak}")
|
||||
for m in sites:
|
||||
code = m.group(1)[0]
|
||||
va = 0x401000 + m.start() - 0xe00 # CODE VA 0x401000 @ file 0xe00
|
||||
print(f" NOP VA 0x{va:06x} (error code {code})")
|
||||
data[m.start():m.end()] = b'\x90' * (m.end() - m.start())
|
||||
open(path, 'wb').write(data)
|
||||
print(f"patched {len(sites)} sites")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user