Files
TeslaRel410/dpl3-revive/patha/vrboard.py
T
CydandClaude Opus 4.8 2bb2ff7302 Weapons fire; hat-glance + MFD fixes; windowless bridge
vpx-device/vpxlog.cpp -- WEAPONS FIRE. The game's fire gate is
targetReticle.targetEntity (mech+0x388), filled every frame by a Division-board
reticle pick (dpl_RapidSectPixel) our HLE never answered, so every shot
misfired. The device now casts the camera centre ray against the live scene
(Moller-Trumbore) and returns the full hit -- instance + DCS + geogroup +
geometry -- piggybacked on the draw_scene reply; terrain is a valid target so
you can fire and miss. Sending geogroup=0 was hanging the game (GetAppSpecific
on a null); returning the real geogroup fixed it. Pick is default-on with a
single VPX_NO_PICK escape hatch. Also swaps the upper-left/right MFD explode
windows (screen location only -- decode untouched, real cause TBD on a pod).

dpl3-revive/patha/vrview.py -- HAT-GLANCE fix at the source. One cockpit DCS
(0xa2c) flushes a rank-2 rotation (shifted body -> wrong read window) that
collapsed the camera chain to rank 2 and smeared the head glance onto the wrong
axis (left/right hat read as pitch). chain_matrix(fix_degenerate) treats a
degenerate chain DCS as identity: preserves the look exactly and keeps stick-Y
torso pitch working (an earlier bridge-level yaw/pitch swap broke stick-Y and
was reverted). User-verified live: hat all 4 dirs + stick-Y both correct.

render-bridge/launch_pod.ps1 -- launch the bridge with pyw (windowless) so no
console window parks over the cockpit displays (Start-Process ignores
-WindowStyle once stdout/stderr are redirected).

render-bridge/live_bridge.py -- surface bridge render errors, flush the status
line; reverted glance-swap note.

Also vendored HUD (dpl2d) renderer work in vrboard/vrview_gl and RENDERER-COLLAB
/ RIO notes.

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

603 lines
31 KiB
Python

#!/usr/bin/env python3
"""
vrboard.py -- virtual VelociRender board (protocol core for Path A).
This is the piece the DOSBox-X C012 device wraps: it consumes the byte stream the
DOS host (FLYK.EXE via VR_COMMS.C) writes to the link, reassembles messages, dispatches
the 24 vr_actions, and produces reply bytes. The rendering guts (geometry/texture
ingest, DPL transform, raster) plug in where marked TODO -- those are the dpl3-revive
parser/viewer pipeline.
Framing is implemented straight from sda4/DPL3/VR_COMMS.C and VR_PROT.H.
See spec/VELOCIRENDER_PROTOCOL.md.
Run directly for a self-test that round-trips a realistic command sequence:
python vrboard.py
"""
import struct
from enum import IntEnum
# ---- vr_action enum (VR_PROT.H, positional/0-based) -------------------------
class A(IntEnum):
init=0; create=1; delete=2; flush=3
sect_pixel=4; sect_vector=5
dcs_nest=6; dcs_link=7; dcs_prune=8
draw_scene=9; draw_scene_complete=10
list_add=11; list_remove=12
morph=13; version=14; statistics=15; readpixels=16
hspcode=17; code860=18; data860=19; bss860=20; args860=21
set_geom_verts=22; set_texmap_texels=23
BOOT_ACTIONS = {A.hspcode, A.code860, A.data860, A.bss860, A.args860}
# ---- envelope (VR_COMMS.C velocirender_transmit / receive_protocol) ----------
# length word = [flags : top bits][id : bits 16-23][count : low 16]
# count = bytes following the length word (= 4 action + N payload for vr_net)
# bit31 = iserver control message (else vr_net render command)
# bit30 = 0x40000000: set on EVERY length word by the shipped Aug-1995 HPDAVE build
# (verify_framing.py found `or ebp,0x40000000` in FLYK's transmit + a global
# pre-set to 0x40000000). Absent in the 1994 VR_COMMS.C source. Exact receive-
# side semantics TBD (confirm in the dynamic capture); we MIRROR it so our
# replies are byte-identical to what FLYK's board would send.
LW_FLAG = 0x40000000
def pack_vr(action, payload=b''):
lw = LW_FLAG | (0xff << 16) | (len(payload) + 4) # broadcast id 0xff
return struct.pack('<II', lw, int(action)) + payload
def pack_iserver(target, payload):
if len(payload) & 1: # host pads iserver msgs to even length
payload += b'\x00'
lw = LW_FLAG | 0x80000000 | (target << 16) | len(payload)
return struct.pack('<I', lw) + payload
class Msg:
__slots__ = ('iserver', 'sender', 'action', 'payload')
def __init__(self, iserver, sender, action, payload):
self.iserver, self.sender, self.action, self.payload = iserver, sender, action, payload
def __repr__(self):
if self.iserver:
return f"<iserver sender={self.sender} {len(self.payload)}B>"
return f"<{A(self.action).name:16} {len(self.payload):4}B>"
class Assembler:
"""Byte stream (host->board) -> complete Msg objects. Models the C012 input FIFO."""
def __init__(self):
self.buf = bytearray()
def feed(self, data):
self.buf += data
def __iter__(self):
return self
def __next__(self):
if len(self.buf) < 4:
raise StopIteration
(lw,) = struct.unpack_from('<I', self.buf, 0)
iserver = bool(lw & 0x80000000)
sender = (lw >> 16) & 0xff
nb = lw & 0xffff # bytes following the length word
if len(self.buf) < 4 + nb:
raise StopIteration # wait for more bytes
body = bytes(self.buf[4:4 + nb])
del self.buf[:4 + nb]
if iserver:
return Msg(True, sender, None, body)
action = struct.unpack_from('<I', body, 0)[0]
return Msg(False, sender, action, body[4:])
# ---- the board ---------------------------------------------------------------
class VirtualBoard:
"""Mirror of VRENDER/VR_REMOT.C:remote_velocirender(). Returns reply bytes (or b'')."""
def __init__(self, renderer=None):
self.renderer = renderer # dpl3-revive pipeline (optional)
self.next_handle = 0x1000 # board-side node handles
self.nodes = {} # handle -> {'type','body'}
self.edges = [] # (op, parent, child) scene-graph log
self.geom = {} # handle -> list of raw vertex/conn blocks
self.tex = {} # handle -> {'u','v','mode','texels'}
self._geom_acc = None # in-progress set_geom_verts
self._tex_acc = None # in-progress action-23 upload
self._conn_acc = None # in-progress action-0x19 upload
self._texel_acc = None # in-progress action-0x1a upload
self.uploads = {} # handle -> [ {hdr, scale, data} ]
self.conns = {} # handle -> [ {n_conns, per, flags, data} ]
self.anim = {} # handle -> latest 0x1d (9f mat + 3f t, COMPOSES)
self.anim_abs = {} # handle -> latest 0x1f record (REPLACES)
self.anim4 = {} # handle -> latest 0x1b (16f 4x4)
self.morphs = {} # morphed geom -> (a, b, alpha)
self.names = {} # 0x8000xxxx name id -> handle (0x22)
self.sfx = {} # SPECIALFX defs by code (0x1c)
self.dl2d = {} # 2D display lists (0x29/0x2b): handle -> [tag words]
self._dl2d_acc = None # in-progress chunked 0x2b flush
self.munga = False # game-build dialect (rpl4opt/btl4opt)
self.frames = 0
self.log = []
def _alloc(self):
h = self.next_handle; self.next_handle += 1; return h
def handle(self, msg):
"""Process one Msg; return reply bytes (b'' = no reply)."""
if msg.iserver:
self.log.append(f"iserver {len(msg.payload)}B (serviced, discarded)")
return b''
try:
a = A(msg.action)
except ValueError:
# shipped build has actions beyond the 1994 enum. 0x2a is the velocirender_sync
# barrier -- it expects an echo reply. Others (e.g. 0x1c, an 80B data op) are
# fire-and-forget; replying to them desyncs the next sync.
if msg.action in (0x2a, 0x2d):
# velocirender_sync barrier: 0x2a in the FLYK demos, 0x2d in the
# MUNGA game builds. Disasm of rpl4opt's vr_sync (hunt_pe.py,
# 0x497080): reply ACTION is never checked -- the reply PAYLOAD
# word0 must echo the request's word0 (a sync cookie; cmp ebx,
# [ebp-0x28] / exit(9) on mismatch). FLYK's 0x2a cookie is 0, so
# the old zero reply was accidentally correct.
# 0x2d is also the MUNGA dialect marker -- the init argv is
# IDENTICAL between builds ('/device~0x150~...' tildes included),
# so argv sniffing misclassified FLYK and yawed every camera.
if msg.action == 0x2d and not self.munga:
self.munga = True
self.log.append("[MUNGA dialect: sync 0x2d seen]")
cookie = msg.payload[:4] if len(msg.payload) >= 4 else b'\x00' * 4
self.log.append(f"sync {msg.action:#x} cookie={cookie.hex()} -> ack")
return (struct.pack('<II', LW_FLAG | (0xff << 16) | 8, msg.action)
+ cookie)
if msg.action == 0x19:
return self.do_add_connections(msg.payload)
if msg.action == 0x1a:
return self.do_texels(msg.payload)
if msg.action == 0x1d and len(msg.payload) >= 56:
# per-frame transform (shipped flush_artics):
# [dcs_type=1 matrix][dcs handle][3x3 row-major][tx ty tz]
h = struct.unpack_from('<I', msg.payload, 4)[0]
self.anim[h] = struct.unpack_from('<12f', msg.payload, 8)
return b''
if msg.action == 0x1f and len(msg.payload) >= 8:
# MUNGA batched flush_artics: [n_records] then n records of
# [dcs handle][payload], where payload is EITHER a full pose
# (12f: 3x3 row-major + t, ABSOLUTE -- replaces the flush
# matrix; composing doubles it) OR a joint angle (sin,cos:
# 2f in BTL4 v1.1, 5f sin,cos,0,0,0 in RPL4 v1.2). Record
# sizes are version-dependent, so parse by BACKTRACKING: every
# record starts with a valid DCS handle, and float bit
# patterns don't collide with small handle ints. (A fixed-
# size guess corrupted mid-packet records -- BT camera chain
# picked up det=0 matrices and rendered nothing.)
p = msg.payload
n = min(struct.unpack_from('<I', p, 0)[0], 64)
is_dcs = lambda h: self.nodes.get(h, {}).get('type') == 5
def parse(off, k):
if k == 0:
return [] if off == len(p) else None
if off + 4 > len(p):
return None
h = struct.unpack_from('<I', p, off)[0]
if not is_dcs(h):
return None
for nf in (12, 2, 5):
end = off + 4 + nf * 4
if end > len(p):
continue
rest = parse(end, k - 1)
if rest is not None:
return [(h, off + 4, nf)] + rest
return None
recs = parse(4, n)
for h, off, nf in recs or ():
if nf == 12:
self.anim_abs[h] = struct.unpack_from('<12f', p, off)
# joint sin/cos records: axis semantics unknown -- the
# flushed matrix stands (mech limbs won't articulate yet)
return b''
if msg.action == 0x1c and len(msg.payload) >= 64:
# install_sfx: SPECIALFX def, fields per the .SCN column comments:
# [code][texture ref][type][velocity][size][off_y][bias]
# [cook_r][cook_g][cook_b][variance][gravity][o_cool][cool]
# [frags][rpt][host ptrs...]
w = struct.unpack_from('<3I', msg.payload, 0)
f = struct.unpack_from('<11f', msg.payload, 12)
fr, rpt = struct.unpack_from('<2I', msg.payload, 56)
self.sfx[w[0]] = {
'tex': w[1], 'type': w[2], 'velocity': f[0], 'size': f[1],
'off_y': f[2], 'bias': f[3], 'cook': f[4:7], 'variance': f[7],
'gravity': f[8], 'o_cool': f[9], 'cool': f[10],
'frags': fr, 'rpt': rpt}
self.log.append(f"install_sfx code={w[0]} type={w[2]} "
f"vel={f[0]:.2f} frags={fr} rpt={rpt}")
return b''
if msg.action == 0x18 and len(msg.payload) >= 12:
return self.do_get_geom_vertices(msg.payload)
if msg.action == 0x23 and len(msg.payload) >= 8:
# fire/pick at screen (u,v) -- observed on joystick trigger:
# [u:f][v:f][host ptr][-1]. EXPERIMENT: reply with the instance
# under the crosshair (sect-style); may drive the damage-FX chain.
u, v = struct.unpack_from('<2f', msg.payload, 0)
h = 0
view = getattr(self, '_view', None)
if view is not None:
try:
h = view.pick_screen(self, u, v)
except Exception as e:
self.log.append(f"0x23 pick error: {e}")
self.log.append(f"fire/pick 0x23 ({u:.2f},{v:.2f}) -> {h:#x}")
# CONFIRMED fire-and-forget: replying desyncs the host stream
# (fxfire run stalled immediately after the first reply, while
# unanswered 0x23s in the prior run were harmless). The pick
# result stays board-side telemetry only.
return b''
if msg.action == 0x22 and len(msg.payload) >= 8:
# name binding: [handle][0x8000xxxx board-side name id]
# (board createName; fire-and-forget)
h, nm = struct.unpack_from('<II', msg.payload, 0)
self.names[nm] = h
return b''
if msg.action == 0x1b and len(msg.payload) >= 72:
# per-frame full-matrix DCS update: [zone][dcs][4x4 row-major]
# (SDEMO; fire-and-forget; row-3 junk words = uninitialized host mem)
h = struct.unpack_from('<I', msg.payload, 4)[0]
self.anim4[h] = struct.unpack_from('<16f', msg.payload, 8)
return b''
if msg.action == 0x29 and len(msg.payload) >= 4:
# dpl2d_NewDisplayList: [remote handle][0] (create/reset a 2D
# overlay display list -- the HUD layer; fire-and-forget)
h = struct.unpack_from('<I', msg.payload, 0)[0]
self.dl2d[h] = []
return b''
if msg.action == 0x2b and len(msg.payload) >= 20:
# dpl2d_FlushDisplayList: the host streams the dpl2d_DISPLAY
# chunk structs verbatim (DPL_2D.H: remote, next, tail, size,
# open, data[30]). First chunk carries remote=handle and
# tail=total chunk count; continuation chunks have remote=0.
# Data words concatenate across chunks (tag args split freely
# at chunk boundaries); tags per DPL2DTAG.H.
hdr, _nxt, tail, size = struct.unpack_from('<4I', msg.payload, 0)
size = min(size, (len(msg.payload) - 20) // 4)
dat = list(struct.unpack_from(f'<{size}I', msg.payload, 20))
if hdr:
self._dl2d_acc = [hdr, max(tail, 1) - 1, dat]
elif self._dl2d_acc is not None:
self._dl2d_acc[1] -= 1
self._dl2d_acc[2] += dat
if self._dl2d_acc is not None and self._dl2d_acc[1] <= 0:
self.dl2d[self._dl2d_acc[0]] = self._dl2d_acc[2]
self._dl2d_acc = None
return b''
self.log.append(f"EXTRA action {msg.action:#x} ({len(msg.payload)}B) -- no reply")
return b''
p = msg.payload
fn = getattr(self, f"do_{a.name}", None)
if fn is None:
self.log.append(f"{a.name}: no handler (discarded)")
return b''
return fn(p) or b''
# -- node lifecycle --
def do_init(self, p):
# NOTE: the init argv CANNOT distinguish dialects -- shipped FLYK and
# rpl4opt send byte-identical '/device~0x150~/video~svga~...' strings.
# The marker is the first vr_sync: 0x2d = MUNGA, 0x2a = FLYK.
self.munga = False
self.log.append(f"init argv={p.rstrip(bytes(1)).decode('latin1','replace')!r}")
return pack_vr(A.init, struct.pack('<I', 1))
def do_create(self, p):
typ = struct.unpack_from('<I', p, 0)[0]
# create is fire-and-forget in BOTH shipped dialects: the host assigns
# the handle locally and sends it (2nd word). MUNGA confirmed by absence
# of the 1994 "unexpected action %d returned in velocirender_create"
# string in rpl4opt (an experimental create-ack desynced the stream).
h = struct.unpack_from('<I', p, 4)[0] if len(p) >= 8 else self._alloc()
self.nodes[h] = {'type': typ, 'body': b''}
self.log.append(f"create type={typ:#x} handle={h:#x} (no reply)")
return b''
def do_delete(self, p):
h = struct.unpack_from('<I', p, 0)[0]; self.nodes.pop(h, None)
self.log.append(f"delete handle {h:#x}")
return pack_vr(A.delete, p[:4])
def do_flush(self, p):
h = struct.unpack_from('<I', p, 0)[0] # payload starts at &n->remote
n = self.nodes.setdefault(h, {'type': None, 'body': b''}); n['body'] = p[4:]
self.log.append(f"flush handle {h:#x} ({len(p)-4}B struct body)")
return b'' # no reply
# -- scene graph plumbing --
def _edge(self, op, p):
x, y = struct.unpack_from('<II', p, 0); self.edges.append((op, x, y))
self.log.append(f"{op} {x:#x} {y:#x}"); return b''
def do_dcs_nest(self, p): return self._edge('nest', p)
def do_dcs_link(self, p): return self._edge('link', p)
def do_dcs_prune(self, p): return self._edge('prune', p)
def do_list_add(self, p): return self._edge('list_add', p)
def do_list_remove(self, p):return self._edge('list_remove', p)
# -- geometry (header msg, then vert blocks, then conn blocks; one ack) --
def do_set_geom_verts(self, p):
if self._geom_acc is None and len(p) == 20:
h, vb, cb, nv, nc = struct.unpack('<5I', p)
self._geom_acc = {'h': h, 'vb': vb, 'cb': cb, 'blocks': [], 'need': vb + cb}
self.log.append(f"set_geom_verts handle {h:#x}: {vb} vert + {cb} conn blocks, {nv}v {nc}c")
return b''
acc = self._geom_acc
acc['blocks'].append(p)
if len(acc['blocks']) >= acc['need']:
self.geom[acc['h']] = acc['blocks']
self.log.append(f" geometry {acc['h']:#x} complete ({len(acc['blocks'])} blocks) -> ack")
self._geom_acc = None
return pack_vr(A.set_geom_verts, struct.pack('<I', 1))
return b''
# -- shipped action 23: bulk upload stream (geometry vertices, and -- per the
# 1994 enum name -- texmap texels; the header self-describes the stream).
# Header 36B = 9 words:
# [target][k?][count][stride_words][n_conns][vert_type][geo_type][conn_sz][scale:f]
# then ceil(count*stride*4 / 508) framed data messages; ONE ack when done.
# (Decoded live from FLYK 2026-07-04: ocean = 4 verts x 9 floats
# [x y z r g b a u v], vert_type=0x15, geo_type=5, scale=GEOMSCALE.)
def do_set_texmap_texels(self, p):
if self._tex_acc is None:
if len(p) != 36:
self.log.append(f"upload23: unexpected header {len(p)}B: {p[:36].hex()}")
return b''
w = struct.unpack_from('<8I', p, 0)
(scale,) = struct.unpack_from('<f', p, 32)
need = w[2] * w[3] * 4
self._tex_acc = {'h': w[0], 'hdr': w, 'scale': scale,
'need': need, 'data': bytearray()}
self.log.append(
f"upload23 handle {w[0]:#x}: count={w[2]} stride={w[3]}w "
f"conns={w[4]} vtype={w[5]:#x} gtype={w[6]} csz={w[7]} scale={scale:.3f} "
f"({need}B expected)")
return b''
acc = self._tex_acc
acc['data'] += p
if len(acc['data']) >= acc['need']:
self.uploads.setdefault(acc['h'], []).append(acc)
self._tex_acc = None
# MUNGA bulk ops are ack-less: the host follows each with
# vr_sync(cookie=action) and the cookie echo IS the ack (an extra
# 0x1a ack surfaced as "unexpected action 26 in velocirender_sync")
if self.munga:
self.log.append(f" upload23 {acc['h']:#x} complete "
f"({len(acc['data'])}B) [no ack: MUNGA syncs]")
return b''
self.log.append(f" upload23 {acc['h']:#x} complete ({len(acc['data'])}B) -> ack")
return pack_vr(A.set_texmap_texels, struct.pack('<I', 1))
return b''
# -- shipped action 0x19: add geometry connections --
# Header 16B = [geom_handle][n_conns][indices_per_conn][flags?], then framed
# index words (n_conns * per x int32); ONE ack when complete.
# (dpl_AddGeometryConnections; decoded live 2026-07-04: ocean quad =
# [0x1f][1][4][0] + indices 0,1,3,2; fish pmesh = [h][70][3][0] + 70 tris.)
def do_add_connections(self, p):
if self._conn_acc is None:
if len(p) != 16:
self.log.append(f"conn19: unexpected header {len(p)}B: {p.hex()}")
return b''
h, nc, per, fl = struct.unpack_from('<4I', p, 0)
self._conn_acc = {'h': h, 'n_conns': nc, 'per': per, 'flags': fl,
'need': nc * per * 4, 'data': bytearray()}
self.log.append(f"conn19 handle {h:#x}: {nc} conns x {per} idx, flags={fl}")
return b''
acc = self._conn_acc
acc['data'] += p
if len(acc['data']) >= acc['need']:
self.conns.setdefault(acc['h'], []).append(acc)
self._conn_acc = None
if self.munga: # see upload23: sync IS the ack
self.log.append(f" conn19 {acc['h']:#x} complete "
f"({len(acc['data'])}B) [no ack: MUNGA syncs]")
return b''
self.log.append(f" conn19 {acc['h']:#x} complete ({len(acc['data'])}B) -> ack")
return pack_vr(0x19, struct.pack('<I', 1))
return b''
# -- shipped action 0x1a: texmap texel upload (1994 set_texmap_texels shape) --
# Header 32B = [texmap_handle][n_texels][u_size][v_size][mode][3 junk ptr words],
# then n_texels x int32 in 256B (64-texel) chunks; ONE ack when complete.
# Texel word = [pad,B,G,R] bytes (SVT xbgr). mode 1 = 8-bit path.
# (Decoded live from SDEMO 2026-07-04: [0x21][0x4000][128][128][1]...)
def do_texels(self, p):
if self._texel_acc is None:
if len(p) != 32:
self.log.append(f"texel1a: unexpected header {len(p)}B: {p.hex()}")
return b''
h, nt, u, v, mode = struct.unpack_from('<5I', p, 0)
self._texel_acc = {'h': h, 'u': u, 'v': v, 'mode': mode,
'need': nt * 4, 'data': bytearray()}
self.log.append(f"texel1a handle {h:#x}: {u}x{v} mode={mode} ({nt} texels)")
return b''
acc = self._texel_acc
acc['data'] += p
if len(acc['data']) >= acc['need']:
self.tex[acc['h']] = acc
self._texel_acc = None
if self.munga: # see upload23: sync IS the ack
self.log.append(f" texel1a {acc['h']:#x} complete "
f"({len(acc['data'])}B) [no ack: MUNGA syncs]")
return b''
self.log.append(f" texel1a {acc['h']:#x} complete ({len(acc['data'])}B) -> ack")
return pack_vr(0x1a, struct.pack('<I', 1))
return b''
# -- morph (action 13): [morphed][a][b][alpha:f][flags] -- vertex-blend a->b
# into the morphed geometry (SDEMO fish schools, .V2Z). Fire-and-forget.
def do_morph(self, p):
if len(p) >= 20:
mo, a, b = struct.unpack_from('<3I', p, 0)
alpha = struct.unpack_from('<f', p, 12)[0]
self.morphs[mo] = (a, b, alpha)
return b''
# -- render + readback --
def do_draw_scene(self, p):
db = struct.unpack_from('<I', p, 0)[0]; self.frames += 1
# TODO: hand self.nodes/edges/geom/tex to the dpl3-revive renderer here.
self.log.append(f"draw_scene(double_buffered={db}) -> frame {self.frames}, async ack")
return pack_vr(A.draw_scene, struct.pack('<I', 1)) # frame-ack (dpl_frame_replied)
def do_readpixels(self, p):
# [x, y, n_pixels] -> pixel words from the last rendered frame,
# fragmented into <=508B action-echoed messages (mirrors upload
# packetization). Texel word convention [pad,B,G,R]. EXPERIMENTAL:
# no shipped binary has exercised this path yet.
view = getattr(self, '_view', None)
if len(p) < 12 or view is None or getattr(view, 'last_frame', None) is None:
self.log.append("readpixels: no frame available -> zero pixels")
return pack_vr(A.readpixels, b'\x00\x00\x00\x00')
x, y, n = struct.unpack_from('<3I', p, 0)
arr = view.last_frame # (H, W, 3) uint8
H, W = arr.shape[:2]
flat = arr.reshape(-1, 3)
start = min(y * W + x, len(flat))
n = min(n, len(flat) - start)
px = flat[start:start + n]
words = bytearray(n * 4)
words[1::4] = px[:, 2].tobytes() # B
words[2::4] = px[:, 1].tobytes() # G
words[3::4] = px[:, 0].tobytes() # R
out = bytearray()
for off in range(0, len(words), 508):
out += pack_vr(A.readpixels, bytes(words[off:off + 508]))
self.log.append(f"readpixels {x},{y} n={n} -> {len(out)}B in fragments")
return bytes(out)
# -- action 0x18 = get_geom_vertices (host reads vertex data back) --
# Located by LE-fixup + capstone hunt in shipped FLYK: transmit with
# eax=0x18, 0x28-byte request, reply MUST echo action 0x18; host receives
# into a 0x100-byte buffer per chunk. Request word0 = geometry handle,
# then vertex0/count-style fields. Reply = echoed-action frames carrying
# the geometry's current vertex floats (<=240B data per frame).
# EXPERIMENTAL until a binary calls it live.
def do_get_geom_vertices(self, p):
w = struct.unpack_from('<%dI' % (len(p) // 4), p, 0)
gh = w[0]
ups = self.uploads.get(gh) or []
if not ups:
self.log.append(f"get_geom_vertices {gh:#x}: no data -> empty reply")
return pack_vr(0x18, b'')
stride = ups[-1]['hdr'][3]
total = 0
for up in ups:
h = up['hdr']
total = max(total, h[1] + h[2], h[7])
buf = bytearray(total * stride * 4)
for up in ups:
h = up['hdr']
if h[3] != stride:
continue
n = min(h[2] * stride * 4, len(up['data']))
buf[h[1] * stride * 4: h[1] * stride * 4 + n] = up['data'][:n]
# optional sub-range: request words 1/2 as vertex0/count when sane
v0 = w[1] if len(w) > 1 and w[1] < total else 0
cnt = w[2] if len(w) > 2 and 0 < w[2] <= total - v0 else total - v0
data = bytes(buf[v0 * stride * 4:(v0 + cnt) * stride * 4])
out = bytearray()
for off in range(0, len(data), 240):
out += pack_vr(0x18, data[off:off + 240])
if not out:
out = pack_vr(0x18, b'')
self.log.append(f"get_geom_vertices {gh:#x} v0={v0} n={cnt} "
f"stride={stride} -> {len(out)}B")
return bytes(out)
# -- intersection queries: ray-triangle over the live scene (via the view) --
# 1994 payloads: sect_pixel = dpl_POINT[4f] + screen x,y (24B);
# sect_vector = dpl_POINT[4f] + x0 y0 z0 x1 y1 z1 (40B).
# Reply = int32 instance handle (0 = miss). Games use these for collision.
def do_sect_vector(self, p):
h = 0
view = getattr(self, '_view', None)
if view is not None and len(p) >= 40:
f = struct.unpack_from('<6f', p, 16)
o = f[0:3]
d = (f[3] - f[0], f[4] - f[1], f[5] - f[2])
try:
h = view.pick(self, o, d)
except Exception as e:
self.log.append(f"sect_vector pick error: {e}")
self.log.append(f"sect_vector -> instance {h:#x}")
return pack_vr(A.sect_vector, struct.pack('<I', h))
def do_sect_pixel(self, p):
# dpl_POINT[4f] + screen x,y. Coordinates may be pixels (832x512 board
# frame) or already-normalized -- normalize by magnitude and unproject
# through the live camera (pick_screen).
h = 0
view = getattr(self, '_view', None)
if view is not None and len(p) >= 24:
x, y = struct.unpack_from('<2f', p, 16)
u = x / 832.0 if abs(x) > 1.5 else x
v = y / 512.0 if abs(y) > 1.5 else y
try:
h = view.pick_screen(self, u, v)
except Exception as e:
self.log.append(f"sect_pixel pick error: {e}")
self.log.append(f"sect_pixel ({x:.1f},{y:.1f}) -> {h:#x}")
return pack_vr(A.sect_pixel, struct.pack('<I', h))
def do_statistics(self, p):
# 1994 board reply: single int32 = 1 (VR_REMOT.C velocirender_statistics)
return pack_vr(A.statistics, struct.pack('<I', 1))
def do_boot(self, p): return b''
# discard boot-time firmware downloads
for _a in BOOT_ACTIONS:
setattr(VirtualBoard, f"do_{_a.name}", lambda self, p, _n=_a.name: (self.log.append(f"{_n}: firmware {len(p)}B discarded"), b'')[1])
# ---- self-test: synthesize a realistic sequence with the host's exact framing ----
def _selftest():
import io
board = VirtualBoard()
asm = Assembler()
replies = []
def host_send(raw): # host -> board wire bytes
asm.feed(raw)
for msg in asm:
r = board.handle(msg)
if r: replies.append((msg, r))
# boot firmware (discarded) + init
host_send(pack_vr(A.code860, b'\x00' * 40))
host_send(pack_vr(A.init, b'video=svga|qual=0x14|\n\x00'))
# build a tiny scene: a DCS (transform) with a 4x4 matrix, a geometry node
host_send(pack_vr(A.create, struct.pack('<I', 0x0006))) # dcs type
dcs_handle = struct.unpack('<I', replies[-1][1][8:12])[0]
host_send(pack_vr(A.create, struct.pack('<I', 0x000a))) # geometry type
geo_handle = struct.unpack('<I', replies[-1][1][8:12])[0]
# flush the DCS with an identity matrix as its struct body
import struct as _s
identity = b''.join(_s.pack('<f', v) for v in
(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1))
host_send(pack_vr(A.flush, struct.pack('<I', dcs_handle) + identity))
host_send(pack_vr(A.dcs_nest, struct.pack('<II', dcs_handle, geo_handle)))
# upload 3 verts + 1 triangle: header, then 3 vert blocks (32B each), 1 conn block (16B)
host_send(pack_vr(A.set_geom_verts, struct.pack('<5I', geo_handle, 3, 1, 3, 1)))
for i in range(3):
host_send(pack_vr(A.set_geom_verts, _s.pack('<8f', i, i, 0, 0,0,1, 0,0)))
host_send(pack_vr(A.set_geom_verts, _s.pack('<4I', 0, 1, 2, 0)))
# draw + readback
host_send(pack_vr(A.draw_scene, struct.pack('<I', 1)))
host_send(pack_vr(A.readpixels, struct.pack('<3I', 0, 0, 320*240)))
print("=== board dispatch trace ===")
for line in board.log: print(" " + line)
print(f"\n=== replies to host: {len(replies)} ===")
for msg, r in replies:
(lw, act) = struct.unpack_from('<II', r, 0)
print(f" {msg!r:22} -> reply {A(act).name} ({len(r)-8}B payload)")
# sanity assertions
assert board.frames == 1
assert dcs_handle in board.nodes and board.nodes[dcs_handle]['body'] == identity
assert geo_handle in board.geom and len(board.geom[geo_handle]) == 4
assert ('nest', dcs_handle, geo_handle) in board.edges
assert asm.buf == bytearray(), "assembler left partial bytes"
print("\nOK: framing round-trips, handles/flush/geometry/scene-graph all consistent.")
if __name__ == '__main__':
_selftest()