"""M4c-device: the LIVE seam. Connects to the DOSBox-X C012 device's VPX_FIFOSOCK tee (vpxlog.cpp), consumes the game's wire AS IT ARRIVES, runs the production firmware live, and renders faithful frames in real time via the verified GPU raster (gpu_raster.Renderer -- bit-identical to the M5 CPU reference). Topology (matches the real hardware path): DOSBox-X (game) -> vpxlog.cpp C012 device -> VPX_FIFOSOCK (listens :PORT) <- live_render.py (connects) -> emu860c firmware -> GPU -> frames The firmware and the socket run CONCURRENTLY: when the firmware reaches its receive point with the queue drained, we pump the socket for more wire; a draw boundary renders + presents. Frames -> live_NNNN.png (and a window if --present). python live_render.py sock: [max_frames] [--present] Validate without the live pod: feed a real capture through a local socket with feed_sock.py (mimics vpxlog's FIFOSOCK server) -- the frames come out identical to the offline frame_*.png. """ 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, igc_gpu import numpy as np from PIL import Image from driver import boot, CpuShim from vrboard import A from texstore import build_texstore from gpu_raster import Renderer, W, H from hud2d_overlay import HudTracker, composite_hud ANAME = {int(a): a.name for a in A} emu860.Mem.log = lambda self, *a, **k: None SPEC = sys.argv[1] MAXF = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else 0 # 0 = unbounded PRESENT = '--present' in sys.argv # --pin : pre-load the FULL texture set (disables incremental rebuild). # Isolates the live firmware+raster seam from texture-arrival timing so the live # frames can be checked bit-for-bit against the offline reference. PIN = None if '--pin' in sys.argv: PIN = sys.argv[sys.argv.index('--pin') + 1] # --catchup : replay the ARCHIVAL dump (vpxlog's VPX_FIFODUMP -- a # second, independent sink of the same wire, written since pod boot) before # going live. Required when connecting mid-mission: the socket tee only # forwards NEW traffic to a fresh client, so without this our firmware boots # with an EMPTY scene graph -- draw_scene only says "render what's already # loaded," it carries no geometry itself, so a from-scratch board shows black. CATCHUP = None if '--catchup' in sys.argv: CATCHUP = sys.argv[sys.argv.index('--catchup') + 1] def parse_fifodump(path): data = open(path, 'rb').read() out = []; off = 0 while True: i = data.find(b'VPXM', off) if i < 0: break ln = struct.unpack_from('= 4: a = struct.unpack_from(' 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(' len(self.buf): return False ln = struct.unpack_from(' deadline: return False def present(img, nth): if win is not None: # live window: skip the per-frame PNG write (a real disk-I/O cost # that was adding latency for no benefit once a window is showing) import pygame surf = pygame.surfarray.make_surface(np.transpose(img, (1, 0, 2))) win.blit(surf, (0, 0)); pygame.display.flip() for ev in pygame.event.get(): if ev.type == pygame.QUIT: return False else: Image.fromarray(img, 'RGB').save(os.path.join(HERE, 'live_%04d.png' % nth)) return True frames = 0 skipped = 0 prev_draw = False t0 = time.time() fps_t = t0 stopped = False while not stopped: 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: # Skip stale intermediate draws when a backlog exists: the game # runs at a fixed real tick rate (measured ~28 draw_scene/s on # this rig's FAST clock) that our render can fall behind, so # rendering EVERY draw_scene means perpetually catching up on # old content. Only render+present when we're at the live edge -- # this bounds perceived lag to render-time, not backlog depth. # Also skip unconditionally while still draining the catch-up # history (r.qi <= N_CATCHUP) -- no point rendering thousands of # historical frames just to reach the live edge. if r.qi <= N_CATCHUP or src.has_pending(): skipped += 1 else: if tex_dirty and not pinned: renderer.texlist = list(build_texstore(recs).items()) tex_dirty = False img = renderer.frame(emu860c.dump_range) if img is not None: composite_hud(img, hud, W, H) # no-op until the reticle resolves if not present(img, frames): break frames += 1 if time.time() - fps_t >= 2.0: print("live: %d frames (%d skipped), %.1f fps (cmd %d)" % (frames, skipped, frames / (time.time() - t0), r.qi), flush=True) fps_t = time.time() if MAXF and frames >= MAXF: break prev_draw = False # make sure the next record is present before the firmware consumes it while r.qi >= len(r.queue): if not feed_more(block=True): stopped = True break if stopped: break act, pl = r.queue[r.qi] hud.feed(act, pl) # every record, in wire order, exactly if ANAME.get(act) == 'draw_scene': # once -- matches consumption, not arrival prev_draw = True if h(shim) == 'done': break dt = time.time() - t0 print("live done: %d frames (%d skipped) in %.1fs = %.1f fps (cmd %d)" % (frames, skipped, dt, frames / max(dt, 1e-9), r.qi), flush=True)