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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# M4b — offline end-to-end seam: RESULTS (2026-07-20)
|
||||
|
||||
`emu860c/m4b_frames.py` — the whole renderer chain, offline, no DOSBox:
|
||||
|
||||
```
|
||||
fifodump (VPXM wire) -> production VREND.MNG on the C i860 core (351x)
|
||||
-> per-draw coefficient program captured LIVE from C memory (0x08158000..)
|
||||
-> M5 faithful render (verified perspective divide + real-texture texel decode)
|
||||
-> frame_NNNN.png sequence + timing
|
||||
```
|
||||
|
||||
## Run (netdeath-20260708.fifodump, 12 frames)
|
||||
|
||||
```
|
||||
queued 53088 records
|
||||
13 textures decoded from the wire
|
||||
frame 0: 26 quads (cmd 4584)
|
||||
frame 2: 48 quads (cmd 5155)
|
||||
frame 4: 78 quads (cmd 6234)
|
||||
frame 6: 87 quads (cmd 8046)
|
||||
frame 10: 127 quads (cmd 10273)
|
||||
frame 11: 127 quads (cmd 10478)
|
||||
done: 12 faithful frames in 73.6s (3.7s firmware, 69.9s render), cmd 10479
|
||||
```
|
||||
|
||||
**The chain works end-to-end.** Frames show the scene *assembling* draw by draw
|
||||
(26 -> 48 -> 78 -> 87 -> 127 quads): frame 0 is ceiling + horizon before the
|
||||
floor lands; frame 11 is a coherent, perspective-correct arena interior —
|
||||
tiled floor receding to a vanishing point, paneled ceiling, side structures,
|
||||
with green/blue detail where distinctive textures (emblems/labels) land.
|
||||
|
||||
## The one finding: render is the bottleneck, not the firmware
|
||||
|
||||
- **Firmware: 3.7 s** for 10,479 wire commands producing 12 frames — real-time-capable.
|
||||
- **Render: 69.9 s** — the CPU numpy per-poly path (O(quads x 832 x 512) float ops
|
||||
per draw). ~5.8 s/frame. This is the gap to close for the live seam.
|
||||
|
||||
The fix already exists: the **GPU tile path** (`igc_gpu` / `igc_gpu_frame`,
|
||||
M1/M2-conformant) does the raster on the RTX in the compute shader. Moving the
|
||||
per-draw raster onto it — reading the full pixel fields (texz/texu/texv/texid)
|
||||
from the tile output and running the same verified texel decode as a vectorized
|
||||
post-pass — is the M4b->M4c bridge to real-time.
|
||||
|
||||
## Honest scope
|
||||
|
||||
- Verified: geometry, winding, perspective divide, texel decode, live per-draw
|
||||
program capture, full-mission drive, frame sequencing.
|
||||
- Best-effort (documented M5-B limit): exact per-surface texid->texture handle.
|
||||
Palette shifts with the mapping (grayscale here vs teal on the static pkl);
|
||||
both are real decoded textures, best-effort placement.
|
||||
|
||||
## Next (M4c)
|
||||
1. GPU raster in the per-draw loop (real-time render to match the real-time firmware).
|
||||
2. The C012 link device in DOSBox-X + socket bridge (wire from the live game
|
||||
instead of a file — identical pipeline downstream).
|
||||
3. Present path (render-bridge window / vr_readpixels).
|
||||
@@ -0,0 +1,167 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user