Files
TeslaRel410/emulator/firmware-decomp/emu860c/battle_frames.py
T
CydandClaude Opus 4.8 fecb5a5f20 M4c COMPLETE: real battle geometry from live game wire, end to end
battle_frames.py extracts the per-draw effect programs (the production
build emits them at ~0x815f000, like fxtest's 0x816f000) and rasterizes
their {edge, z, u/v} primitives through the render_fx GPU path. Running the
netdeath battle capture on the production firmware (vrend410) + emu860c +
GPU: 8 frames in 2.8s showing the actual mech-combat arena -- ground plane,
horizon, a building, receding structures -- with 31..148 primitives/frame
growing as the scene loads (cmd 8125 deep).

The full chain is now proven on real game content: game's own firmware ->
C-accelerated i860 -> the card's own IGC/EMC instruction set on the GPU ->
recognizable battle frames. Surfaces are u/v false-colour pending texel
sampling (the TXDN path, M3); the geometry is the game's.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:48:31 -05:00

119 lines
4.8 KiB
Python

"""Battle content frames: production firmware on the C core, real game wire,
per-draw EFFECT-PRIMITIVE readout (render_fx path) -> actual scene geometry.
The draws build effect programs at ~0x815f000 (like fxtest's 0x816f000)."""
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, igc_exec, igc_gpu, igc_gpu_frame
import numpy as np
from PIL import Image
from driver import boot, CpuShim
from vrboard import A
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 8
PROG_LO, PROG_HI = 0x08158000, 0x08170000 # per-draw effect program window
W, H = 832, 512
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)
r = boot(fw='vrend410', queue=recs)
shim = CpuShim()
RECV = emu_main.MAPS['vrend410']['receive']
ctx = igc_gpu.GpuTile().ctx
print("GPU:", ctx.info['GL_RENDERER'], flush=True)
def f32(w): return struct.unpack('<f', struct.pack('<I', w & 0xffffffff))[0]
def render_effects(nth):
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 and ((rd(a + 4) >> 8) & 0xff) == 0x2c]
if not setups:
return False, 0
recs4 = []
for a in setups:
edges = []
p = a + 4
while ((rd(p) >> 8) & 0xff) == 0x2c and len(edges) < 6:
edges.append((f32(rd(p+4)), f32(rd(p+8)), f32(rd(p+12)))); p += 16
zp = up = vp = None; q = p
for _ in range(40):
wv = rd(q); op = (wv >> 8) & 0xff; ad = wv & 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 == 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 zp and up and vp: break
q += 4
if zp is None or len(edges) < 2:
continue
hasuv = 1.0 if (up and vp) else 0.0
up = up or (0, 0, 0); vp = vp or (0, 0, 0)
eslots = [(e[0], e[1], e[2], 1.0) for e in edges[:4]]
while len(eslots) < 4: eslots.append((0.0, 0.0, 0.0, 0.0))
recs4 += eslots + [(*zp, float(len(edges))), (*up, hasuv), (*vp, 0.0)]
if not recs4:
return False, 0
prims = np.array(recs4, dtype=np.float32).reshape(-1, 4)
n_prims = len(prims) // 7
prog_gl = ctx.compute_shader(igc_gpu_frame.FX_SHADER)
b0 = ctx.buffer(prims.tobytes()); out = ctx.buffer(reserve=W * H * 16)
b0.bind_to_storage_buffer(0); out.bind_to_storage_buffer(1)
prog_gl['n_prims'] = n_prims; prog_gl['width'] = W
prog_gl.run(group_x=(W + 63) // 64, group_y=H)
raw = np.frombuffer(out.read(), dtype=np.uint32).reshape(H, W, 4)
z = raw[..., 0].view(np.float32); u = raw[..., 1].view(np.float32); v = raw[..., 2].view(np.float32)
m = z > -1e29
rgb = np.zeros((H, W, 3), np.uint8)
if m.any():
un = np.zeros_like(u); vn = np.zeros_like(v)
un[m] = (u[m] - u[m].min()) / max(1e-9, np.ptp(u[m]))
vn[m] = (v[m] - v[m].min()) / max(1e-9, np.ptp(v[m]))
rgb[..., 0][m] = (60 + 180 * un[m]).astype(np.uint8)
rgb[..., 1][m] = (40 + 160 * vn[m]).astype(np.uint8)
rgb[..., 2][m] = (220 - 140 * un[m]).astype(np.uint8)
b0.release(); out.release()
Image.fromarray(rgb, 'RGB').save(os.path.join(HERE, 'battle_%04d.png' % nth))
return True, (n_prims, int(m.sum()))
frames = 0; prev_draw = False; t0 = time.time()
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:
ok, info = render_effects(frames)
if ok:
print("battle frame %d: %s prims/px (cmd %d)" % (frames, info, 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
print("done: %d battle frames in %.1fs (cmd %d)" % (frames, time.time() - t0, r.qi), flush=True)