Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.
What's here:
- parser/ B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/ reverse-engineered format + the definitive VelociRender wire
protocol (from the original DIVISION source, matches our live
VPX node/action tables exactly).
- source-ref/ read-only copies of the original DIVISION C (BIZREAD.C,
DPLTYPES.H, DPL.H) that define the formats.
- patha/ the "virtual VelociRender board": vrboard.py (24-action protocol
server), vrview.py (numpy software rasterizer, the reference),
vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/ WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin real BTL4OPT arena wire captures (kept for offline renderer
testing/regression against OUR game).
.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).
Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
6.2 KiB
Python
141 lines
6.2 KiB
Python
#!/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 <prefix>.raw.bin / <prefix>.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('<II', reply, 0)[1]
|
|
try: nm = A(act).name
|
|
except ValueError: nm = f"action_{act:#x}"
|
|
emit(f" -> reply {nm} ({len(reply)}B)")
|
|
return len(data)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|