From 28785d9486a2bdccefec5c34ac43f91ea6cf64ce Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 19 Jul 2026 15:03:00 -0500 Subject: [PATCH] M4c: live_server -- the authentic backend for the vpxlog wire feed Consumes VPXM records (fifodump file or live VPX_FIFOSOCK) and renders through emu860c + the GPU tile path. The 'new C012 device' of the plan turned out to already exist: vpxlog.cpp IS the link adapter and already streams the wire over TCP; this server is the drop-in authentic listener beside the GL bridge. Validation on the netdeath battle capture (53,088 records) found the real gap: the firmware dies with 'unrecognised or illegal action 0x2d' -- the battle games speak a newer protocol than capfw7 (cap7's booted build). Census: every boot-carrying capture ships the same old build; the 0x2d sessions were all captured mid-run. The production build is in the game files themselves: ALPHA_1/REL410/{BT,RP}/VREND.MNG (identical, 385KB, csize 0x3ac80). Next: re-derive the hook map for that build (signature anchors in emu_main) and replay the battle wire on its matching firmware. Co-Authored-By: Claude Opus 4.8 --- .../firmware-decomp/emu860c/live_server.py | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 emulator/firmware-decomp/emu860c/live_server.py diff --git a/emulator/firmware-decomp/emu860c/live_server.py b/emulator/firmware-decomp/emu860c/live_server.py new file mode 100644 index 00000000..ef03651e --- /dev/null +++ b/emulator/firmware-decomp/emu860c/live_server.py @@ -0,0 +1,188 @@ +"""M4c: the authentic backend -- consumes the vpxlog device's VPXM wire stream +(offline fifodump file, or live VPX_FIFOSOCK) and renders through emu860c + +the GPU tile path. Frames written as live_NNNN.png. + + python live_server.py [max_frames] + python live_server.py sock: [max_frames] (live) +""" +import sys, os, time, struct, socket +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 igc_exec, igc_gpu +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 +W, H = 832, 512 + + +class Source: + """Incremental VPXM record source: file or socket.""" + def __init__(self, spec): + self.buf = b'' + self.sock = None + if spec.startswith('sock:'): + port = int(spec[5:]) + self.sock = socket.socket() + while True: + try: + self.sock.connect(('127.0.0.1', port)) + break + except OSError: + time.sleep(0.5) + self.sock.settimeout(0.5) + print("connected to fifosock :%d" % port, flush=True) + else: + self.buf = open(spec, 'rb').read() + print("loaded %d bytes from %s" % (len(self.buf), spec), flush=True) + self.off = 0 + + def poll(self): + if self.sock: + try: + data = self.sock.recv(1 << 16) + if data: + self.buf += data + except socket.timeout: + pass + + def next_record(self): + while True: + i = self.buf.find(b'VPXM', self.off) + if i < 0 or i + 8 > len(self.buf): + return None + ln = struct.unpack_from(' len(self.buf): + return None + body = self.buf[i + 8:i + 8 + ln] + self.off = i + 8 + ln + if len(body) >= 4: + action = struct.unpack_from('> 28) & 0xf + if c in (1, 9) and 0x08000000 <= a < 0x08020000: + sends.append([emu860c.r32(a + 4 * k) for k in range(op & 0x7f)]) + elif c == 2: + order.append((a, len(sends))) + i += 2 + if not order: + return False + prev_cut = 0 + ntiles = 0 + for tid, cut in order: + ox = (tid & 0x1f) * 64 + oy = ((tid >> 5) & 0x1f) * 128 + instrs = [] + for pl in sends[prev_cut:cut]: + ins, _ = igc_exec.parse(pl) + instrs += ins + prev_cut = cut + if ox >= W or oy >= H or not instrs: + continue + data = g.run(instrs, ox=ox, oy=oy, pre_seed_texz_x=True) + for py_ in range(igc_gpu.TILE_H): + gy = oy + py_ + if gy >= H: + break + row = data[py_ * igc_gpu.TILE_W:(py_ + 1) * igc_gpu.TILE_W] + for px in range(igc_gpu.TILE_W): + gx = ox + px + if gx >= W: + break + texu_acc[gy, gx] = igc_gpu.GpuTile.rdbits(row[px], 57, 20) + ntiles += 1 + if not ntiles: + return False + PAL = [(180, 180, 180), (180, 180, 16), (16, 180, 180), (16, 180, 16), + (180, 16, 180), (180, 16, 16), (16, 16, 180)] + rgb = np.zeros((H, W, 3), np.uint8) + for gx in range(W): + u = int(np.median(texu_acc[:, gx])) + rgb[:, gx] = (0, 0, 0) if u < 48 else PAL[min(6, (u - 48) * 7 // (W - 48))] + Image.fromarray(rgb, 'RGB').save(os.path.join(HERE, 'live_%04d.png' % nth)) + print("frame %d: %d tiles, %d sends (cmd %d)" % + (nth, ntiles, len(sends), r.qi), flush=True) + return True + + +RECEIVE_PC = 0xf04024c0 +idle = 0 +last_note = time.time() +while frames_done < MAXF and idle < 40: + reason, steps = emu860c.run(500_000_000) + if time.time() - last_note > 20: + last_note = time.time() + st = emu860c.getstate() + print("... cmd %d, %.1fB steps, pc=%#x" % + (r.qi, st['steps'] / 1e9, st['pc']), flush=True) + if reason == 3: + continue # long processing slice; keep going + 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 == RECEIVE_PC: + if prev_was_draw: + if render_frame(frames_done): + frames_done += 1 + prev_was_draw = False + # feed the live queue + while r.qi >= len(r.queue): + rec = src.next_record() + if rec is not None: + r.queue.append(rec) + idle = 0 + break + src.poll() + idle += 1 + if idle >= 40: + break + if r.qi >= len(r.queue): + break + nxt = r.queue[r.qi][0] + if ANAME.get(nxt) == 'draw_scene': + emu860c.watch_drain() + prev_was_draw = True + if h(shim) == 'done': + break +print("done: %d frames in %.1fs (cmds %d)" % + (frames_done, time.time() - t0, r.qi), flush=True)