Files
TeslaRel410/emulator/firmware-decomp/emu860c/m4b_frames.py
T
CydandClaude Opus 4.8 695aacec30 M4b: offline end-to-end seam -- live mission -> faithful frame SEQUENCE
m4b_frames.py fuses the two proven halves: battle_frames.py's live per-draw
program capture (from running-firmware C memory) + render_final.py's verified
M5 texel render (perspective divide + real-texture decode). Result: the whole
renderer chain runs offline from a real fifodump -- production VREND.MNG on the
C i860 core -> per-draw coefficient program -> faithful frame -> PNG sequence.

Verified over netdeath-20260708.fifodump: 12 frames, scene assembles draw by
draw (26->48->78->87->127 quads), frame 11 = coherent perspective-correct arena
interior (tiled floor to vanishing point, ceiling, side structures). This is
the dress rehearsal for the live DOSBox seam -- identical pipeline, wire from a
file instead of the C012 device.

FINDING: firmware is real-time-capable (3.7s), the CPU numpy per-poly render is
the bottleneck (69.9s/12 frames, ~5.8s/frame). The fix already exists: the
conformant GPU tile path (igc_gpu). Moving the per-draw raster onto it is the
M4b->M4c bridge to real-time. Full writeup in M4B-RESULTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:01:50 -05:00

168 lines
6.3 KiB
Python

"""M4b -- the offline end-to-end seam: real game wire in, FAITHFUL frame
sequence out, no DOSBox.
The whole chain at speed:
fifodump (VPXM wire) -> production VREND.MNG on the C i860 core (351x)
-> per-draw coefficient program captured LIVE from C memory
-> M5 faithful render (verified perspective divide + real-texture texel decode)
-> frame_NNNN.png sequence + timing report
This is the dress-rehearsal for the live DOSBox seam (M4c): identical pipeline,
the wire arriving from a file instead of the C012 link device. Fidelity is the
already-verified M5 path (b74b0c89 / a24210ad); what M4b proves is that the
FULL mission drives it, per draw, at speed.
python m4b_frames.py <capture.fifodump> [max_frames]
"""
import sys, os, time, struct
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE); sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha')
import emu860, emu_main, emu860c
import numpy as np
from PIL import Image
from driver import boot, CpuShim
from vrboard import A
from texstore import build_texstore
from dpl_sampler import mode_to_texflags, wrap_index, composite
ANAME = {int(a): a.name for a in A}
emu860.Mem.log = lambda self, *a, **k: None
SRC = sys.argv[1]
MAXF = int(sys.argv[2]) if len(sys.argv) > 2 else 12
PROG_LO, PROG_HI = 0x08158000, 0x08170000 # per-draw effect-program window
W, H = 832, 512
EDGE = {0x42, 0x0d, 0x2c}
# --- parse the wire once: build the queue AND the texture store from it -------
data = open(SRC, 'rb').read()
recs = []; off = 0
while True:
i = data.find(b'VPXM', off)
if i < 0:
break
ln = struct.unpack_from('<I', data, i + 4)[0]
body = data[i + 8:i + 8 + ln]; off = i + 8 + ln
if len(body) >= 4:
a = struct.unpack_from('<I', body, 0)[0]
if a < 0x100:
recs.append((a, body[4:]))
print("queued %d records" % len(recs), flush=True)
store = build_texstore(recs)
texlist = list(store.items()) # [(handle,(u,v,mode,arr)), ...]
print("%d textures decoded from the wire" % len(texlist), flush=True)
r = boot(fw='vrend410', queue=recs)
shim = CpuShim()
RECV = emu_main.MAPS['vrend410']['receive']
# grid, reused every frame
yy, xx = np.mgrid[0:H, 0:W].astype(np.float64)
def f32(w):
return struct.unpack('<f', struct.pack('<I', w & 0xffffffff))[0]
def render_faithful(nth):
"""The verified M5 render, driven by the LIVE per-draw program in C memory."""
prog = {a: emu860c.r32(a) for a in range(PROG_LO, PROG_HI, 4) if emu860c.r32(a)}
def rd(a):
return prog.get(a, 0)
setups = [a for a in sorted(prog) if rd(a) == 0x100]
if not setups:
return False, 0
zbuf = np.full((H, W), -1e30)
img = np.zeros((H, W, 3), np.uint8)
drawn = 0
for a in setups:
p = a + 4
edges = []
while ((rd(p) >> 8) & 0xff) in EDGE and len(edges) < 6:
edges.append((f32(rd(p + 4)), f32(rd(p + 8)), f32(rd(p + 12)))); p += 16
if len(edges) < 3:
continue
ep = np.ones((H, W), bool); en = np.ones((H, W), bool)
for Ae, Be, Ce in edges:
v = Ae * xx + Be * yy + Ce
ep &= v >= 0; en &= v < 0
inside = ep | en # inside = all-positive OR all-negative
if not inside.any():
continue
zp = tzp = up = vp = None; tid = None; q = p
for _ in range(70):
w = rd(q); op = (w >> 8) & 0xff; ad = w & 0xff
if op == 0x21 and zp is None:
zp = (f32(rd(q + 4)), f32(rd(q + 8)), f32(rd(q + 12)))
if op == 0x43 and ad == 32 and tzp is None:
tzp = (f32(rd(q + 4)), f32(rd(q + 8)), f32(rd(q + 12)))
if op == 0x43 and ad == 58 and up is None:
up = (f32(rd(q + 4)), f32(rd(q + 8)), f32(rd(q + 12)))
if op == 0x43 and ad == 78 and vp is None:
vp = (f32(rd(q + 4)), f32(rd(q + 8)), f32(rd(q + 12)))
if op == 0xf7 and ad in (141, 142) and tid is None:
tid = (rd(q + 4) >> 2) & 0x3f
q += 4
if zp is None:
continue
z = zp[0] * xx + zp[1] * yy + zp[2]
inside &= z > zbuf
if not inside.any():
continue
if up and vp and tzp and texlist:
h, (tu, tv, mode, arr) = texlist[(tid or 0) % len(texlist)]
wrap_u, wrap_v, cut_mode = mode_to_texflags(mode)
tzv = tzp[0] * xx + tzp[1] * yy + tzp[2]
tzv = np.where(np.abs(tzv) < 1.0, np.sign(tzv) + (tzv == 0), tzv)
# VERIFIED divide: texel8 = (texu/texz)*2048 (3 int + 8 texel bits)
ucoord = ((up[0] * xx + up[1] * yy + up[2]) / tzv * 2048).astype(np.int64)
vcoord = ((vp[0] * xx + vp[1] * yy + vp[2]) / tzv * 2048).astype(np.int64)
ui = wrap_index((ucoord * tu) >> 8, tu, wrap_u)
vi = wrap_index((vcoord * tv) >> 8, tv, wrap_v)
samp = arr[vi, ui]
drawmask = composite(img, inside, samp, cut_mode)
else:
img[inside] = (60, 60, 70)
drawmask = inside
zbuf[drawmask] = z[drawmask]
drawn += 1
if not drawn:
return False, 0
Image.fromarray(img, 'RGB').save(os.path.join(HERE, 'frame_%04d.png' % nth))
return True, drawn
# --- run the whole mission, one faithful frame per draw_scene ----------------
frames = 0
prev_draw = False
t0 = time.time()
render_t = 0.0
while frames < MAXF and r.qi < len(r.queue):
reason, _ = emu860c.run(500_000_000)
if reason == 3:
continue
if reason != 0:
print("core reason %d" % reason, flush=True); break
pc = emu860c.getstate()['pc']
h = r.hooks.get(pc)
if h is None:
print("sentinel", flush=True); break
if pc == RECV:
if prev_draw:
rt0 = time.time()
ok, drawn = render_faithful(frames)
render_t += time.time() - rt0
if ok:
print("frame %d: %d quads (cmd %d)" % (frames, drawn, r.qi), flush=True)
frames += 1
prev_draw = False
if r.qi < len(r.queue) and ANAME.get(r.queue[r.qi][0]) == 'draw_scene':
prev_draw = True
if h(shim) == 'done':
break
wall = time.time() - t0
print("done: %d faithful frames in %.1fs (%.1fs firmware, %.1fs render), cmd %d"
% (frames, wall, wall - render_t, render_t, r.qi), flush=True)