#!/usr/bin/env python3 """ vrrun.py -- live responding board for the custom-DOSBox-X FLYK run. Phase machine matched to what disassembly of shipped FLYK proved: BOOT : swallow the raw transputer .BTL stream (not framed). When it ends (idle gap after a substantial burst), send ONE vr_net frame -- this is all start_velocirender's velocirender_input() needs to stop timing out (it loops on iserver msgs, returns on the first non-iserver frame with count >= 8; the content is ignored). No iserver tag => no exit(666) risk. FRAMED : feed everything to vrboard -- discards the framed vr_860* MNG download, replies to vr_init, then handles the scene (create/flush/geometry/ textures/draw). Every message + reply is logged. Raw bytes also go to capture.raw.bin / capture.hex.log for offline analysis. python vrrun.py [prefix] # listen 127.0.0.1:8620; Ctrl-C to stop # writes .raw.bin / .log python vrrun.py [prefix] --view # + real-time board-side window: moderngl GPU # backend (vrview_gl.py, 832x512) by default, # renders each draw_scene from live wire state, # ack paced to <=60 Hz so FLYK runs real-time python vrrun.py [prefix] --view --soft # software rasterizer (vrview.py, # the debugging reference; also VRVIEW_SOFT=1) """ import os, socket, struct, time, sys from vrboard import VirtualBoard, Assembler, pack_vr, A def make_renderer(soft=False): """GPU backend by default; --soft / VRVIEW_SOFT=1 or any GL failure falls back to the software rasterizer (the reference implementation).""" if not soft and os.environ.get('VRVIEW_SOFT') != '1': try: from vrview_gl import GLRenderer return GLRenderer() except Exception as e: print(f"[view] GPU backend unavailable ({e}) -- software fallback") from vrview import Renderer return Renderer() HOST, PORT = '127.0.0.1', 8620 RSET = b'\x00RSET' IDLE = 0.5 # seconds of no writes => a phase boundary MIN_BOOT = 4096 # bytes received before we treat an idle as "BTL done" def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] prefix = args[0] if args else "capture" view = None if "--view" in sys.argv: view = make_renderer("--soft" in sys.argv) binf = open(prefix + ".raw.bin", "wb") log = open(prefix + ".log", "w") def emit(s): log.write(s + "\n"); log.flush(); print(s) board = VirtualBoard(); asm = Assembler() board._view = view phase = "BOOT"; recv_since_reset = 0; handshake_sent = False srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)); srv.listen(1) emit(f"vrrun listening on {HOST}:{PORT}") conn, addr = srv.accept(); emit(f"vrlink connected from {addr}") conn.settimeout(IDLE) def send_handshake(): msg = pack_vr(A.init, b'\x00' * 8) # vr_net, count = 12 (action+8) -- clears the >=8 check conn.sendall(msg) emit(f"[BOOT] BTL done ({recv_since_reset}B). Sent 1 vr_net handshake frame " f"({len(msg)}B) -> velocirender_input should succeed.") with conn: while True: try: data = conn.recv(8192) except socket.timeout: if phase == "BOOT" and recv_since_reset > MIN_BOOT and not handshake_sent: send_handshake(); handshake_sent = True; phase = "FRAMED" if view is not None: view.pump() # keep the window alive while the link idles continue except ConnectionResetError: emit("(peer reset -- FLYK closed)"); break if not data: break binf.write(data); binf.flush() # peel RSET control frames (vrlink injects these only on port-0x160 writes, # which FLYK does only during boot -- so ignore the marker once past BOOT) while phase == "BOOT" and RSET in data: i = data.index(RSET) pre, data = data[:i], data[i+len(RSET):] if pre: recv_since_reset += _consume(pre, phase, asm, board, conn, emit) emit("[reset] link reset strobe"); recv_since_reset = 0 phase = "BOOT"; handshake_sent = False; asm.buf.clear() if data: recv_since_reset += _consume(data, phase, asm, board, conn, emit) emit(f"connection closed. frames drawn={board.frames}, " f"nodes={len(board.nodes)}, geom={len(board.geom)}, tex={len(board.tex)}") def _consume(data, phase, asm, board, conn, emit): """Return count of bytes accounted; frame+dispatch when past boot.""" if phase == "BOOT": return len(data) # swallow BTL raw view = getattr(board, '_view', None) asm.feed(data) for msg in asm: board.log.clear() is_draw = (not msg.iserver and msg.action == A.draw_scene) try: reply = board.handle(msg) except Exception as e: emit(f" [board error on {msg!r}: {e}]"); reply = b'' quiet = is_draw and board.frames > 3 # don't spam the log at 60 Hz if not quiet: for line in board.log: emit(" " + line) if is_draw and view is not None: # render this frame board-side, then ack -- FLYK's loop is paced by us try: view.draw(board) except KeyboardInterrupt: raise except Exception as e: if board.frames % 300 == 1: emit(f" [render error: {e}]") if board.frames % 300 == 0: emit(f" [frame {board.frames}]") if reply: conn.sendall(reply) if not quiet: act = struct.unpack_from(' reply {nm} ({len(reply)}B)") return len(data) if __name__ == '__main__': main()