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>
168 lines
6.3 KiB
Python
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)
|