Files
TeslaRel410/emulator/firmware-decomp/emu860c/live_render.py
T
CydandClaude Opus 4.8 e0a2073dff M4c-device: the LIVE socket seam -- proven bit-identical over VPX_FIFOSOCK
live_render.py connects to vpxlog.cpp's existing VPX_FIFOSOCK tee (the DOSBox-X
C012 device), consumes the game's wire AS IT ARRIVES, and runs the firmware and
the socket CONCURRENTLY -- pumping the socket whenever the firmware's receive
point drains -- rendering each draw with the shared verified GPU raster.

gpu_raster.py factors the M4c-raster renderer out of m4b_gpu so offline and live
share ONE render path (the 6-edge fp64 raster + verified texel decode).

feed_sock.py mimics vpxlog's FIFOSOCK server (streams a real capture in delayed
chunks) so the live path is validated end-to-end WITHOUT the flaky live pod.
Result: 12 frames streamed live at 2.3 fps; with --pin (equalized texture
availability) the live frames are BIT-IDENTICAL to the offline reference
(12/12 differ>24 = 0.000%) -- the seam is faithful. The default incremental
texture model is more authentic (the board only has texels it has received);
its divergence from offline is purely the M5-B tid%ntex placeholder, orthogonal.

Live topology now closed end-to-end offline:
  game/capture -> C012 VPX_FIFOSOCK -> live_render -> firmware -> GPU -> frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:22:36 -05:00

202 lines
7.2 KiB
Python

"""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:<port> [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
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 <fifodump>: 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]
class SockSource:
"""Incremental VPXM record source over a client socket to VPX_FIFOSOCK."""
def __init__(self, port):
self.buf = b''; self.off = 0; self.eof = False
self.sock = socket.socket()
for _ in range(120):
try:
self.sock.connect(('127.0.0.1', port)); break
except OSError:
time.sleep(0.25)
else:
raise SystemExit("could not connect to VPX_FIFOSOCK :%d" % port)
self.sock.settimeout(0.2)
print("connected to VPX_FIFOSOCK :%d" % port, flush=True)
def pump(self):
"""Read whatever is available; set eof on clean close. Non-fatal timeout."""
try:
data = self.sock.recv(1 << 16)
if data:
self.buf += data
else:
self.eof = True
except socket.timeout:
pass
except OSError:
self.eof = True
def next_record(self):
i = self.buf.find(b'VPXM', self.off)
if i < 0 or i + 8 > len(self.buf):
return None
ln = struct.unpack_from('<I', self.buf, i + 4)[0]
if i + 8 + ln > 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('<I', body, 0)[0]
if action < 0x100:
return (action, body[4:])
return self.next_record() # skip non-message burst
src = SockSource(int(SPEC.split(':')[1]))
r = boot(fw='vrend410', queue=[]) # queue grows as wire arrives
shim = CpuShim()
RECV = emu_main.MAPS['vrend410']['receive']
g = igc_gpu.GpuTile()
print("GPU:", g.ctx.info['GL_RENDERER'], flush=True)
recs = [] # running wire, for the texture store
tex_dirty = False
renderer = Renderer(g.ctx, []) # texlist filled once textures arrive
pinned = False
if PIN:
pdata = open(PIN, 'rb').read(); prec = []; poff = 0
while True:
pi = pdata.find(b'VPXM', poff)
if pi < 0:
break
pln = struct.unpack_from('<I', pdata, pi + 4)[0]
pbody = pdata[pi + 8:pi + 8 + pln]; poff = pi + 8 + pln
if len(pbody) >= 4 and struct.unpack_from('<I', pbody, 0)[0] < 0x100:
prec.append((struct.unpack_from('<I', pbody, 0)[0], pbody[4:]))
renderer.texlist = list(build_texstore(prec).items())
pinned = True
print("PINNED %d textures from %s" % (len(renderer.texlist), os.path.basename(PIN)), flush=True)
win = None
if PRESENT:
try:
import pygame
pygame.init()
win = pygame.display.set_mode((W, H))
pygame.display.set_caption("VelociRender (reconstructed) -- live")
except Exception as e:
print("no window (%s); writing PNGs only" % e, flush=True)
def feed_more(block):
"""Pull at least one new record into r.queue (and recs). Returns False at EOF."""
global tex_dirty
deadline = time.time() + 10.0
while True:
rec = src.next_record()
if rec is not None:
r.queue.append(rec); recs.append(rec)
if rec[0] == 0x1a:
tex_dirty = True
return True
if src.eof:
return False
src.pump()
if not block and time.time() > deadline:
return False
def present(img, nth):
Image.fromarray(img, 'RGB').save(os.path.join(HERE, 'live_%04d.png' % nth))
if win is not None:
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
return True
frames = 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:
if tex_dirty and not pinned:
renderer.texlist = list(build_texstore(recs).items())
tex_dirty = False
img = renderer.frame(emu860c.r32)
if img is not None:
if not present(img, frames):
break
frames += 1
if time.time() - fps_t >= 2.0:
print("live: %d frames, %.1f fps (cmd %d)"
% (frames, 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
if ANAME.get(r.queue[r.qi][0]) == 'draw_scene':
prev_draw = True
if h(shim) == 'done':
break
dt = time.time() - t0
print("live done: %d frames in %.1fs = %.1f fps (cmd %d)"
% (frames, dt, frames / max(dt, 1e-9), r.qi), flush=True)