#!/usr/bin/env python3 """Wire Dave's dpl3-revive renderer into our LIVE running pod. Tails our device's VPX_FIFODUMP (VPXM records = the game's live VelociRender wire), feeds each message into Dave's VirtualBoard, and renders every draw_scene with his vrview software rasterizer in a real window -- using the game's own camera (the player's RIO input drives it). py live_bridge.py """ import os, sys, struct, time, math import numpy as np from _backend import pick_renderer from vrboard import VirtualBoard, Msg, A Renderer, backend = pick_renderer() path = sys.argv[1] catchup = sys.argv[2] if len(sys.argv) > 2 else None board = VirtualBoard() board.munga = False # BRIDGE_W/BRIDGE_H size the main out-the-window render window. Default AND # deploy standard = 832x512, the dPL3 board's native framebuffer res (see # LAUNCH.md; 800x600-output presentation is an open decision). # BRIDGE_BORDERLESS=1 drops the title bar for a kiosk deploy. r = Renderer(w=int(os.environ.get('BRIDGE_W', '832')), h=int(os.environ.get('BRIDGE_H', '512')), title=f"dpl3-revive renderer (Dave) -- LIVE from our pod [{backend}]") r.fps = int(os.environ.get('BRIDGE_FPS', '60' if backend == 'GL' else '30')) # eye TRIM on top of the camera position (live; render window must be # focused): UP/DOWN = height +-1, LEFT/RIGHT = seat forward/back +-0.5 # along the look direction (user: the canopy reads as "sitting too far # back" -- tune, then pin with FP_UPOFF / FP_FWDOFF). The chain camera # carries the true cockpit eye so the default trims are 0; the vehicle- # root fallback adds VEH_EYE below (12 was the hand-tuned value). UPOFF = [float(os.environ.get('FP_UPOFF', '0'))] FWDOFF = [float(os.environ.get('FP_FWDOFF', '0'))] VEH_EYE = 12.0 # the un-overridden munga cam-chain method (fp_cam overrides r.cam_matrix # every frame, so grab the bound original once) CHAIN_CAM = r.cam_matrix # torso twist: the 0x1f batch's 2f/5f joint entries are (sin,cos) of the # joint angle keyed by the joint's DCS handle (calibrated live vs MADCAT.SUB # +-130deg limits). The chain DCS values EXCLUDE joint angles, so the twist # is composed onto the chain look direction in fp_cam. JOINTS holds the # latest (sin,cos) per handle. FP_TWIST_SIGN flips, FP_TWIST=0 disables. JOINTS = {} TWIST_SIGN = float(os.environ.get('FP_TWIST_SIGN', '1')) TWIST_ON = os.environ.get('FP_TWIST', '1') != '0' def track_joints(payload): """Dave's backtracking 0x1f parse (vrboard.py), keeping the joint (sin,cos) entries his board skips.""" p = payload if len(p) < 8: return n = min(struct.unpack_from(' len(p): return None h = struct.unpack_from(' len(p): continue rest = parse(end, k - 1) if rest is not None: return [(h, off + 4, nf)] + rest return None for h, o, nf in parse(4, n) or (): if nf in (2, 5): JOINTS[h] = struct.unpack_from('<2f', p, o) # --- cockpit fixtures (cage + canopy trim) --------------------------------- # The game links the cockpit CAGE (an inward-facing shell around the mech, # open panes forward / solid wall aft) directly under the VEHICLE ROOT and # never touches it again -- but the pod behaves like a tank turret (operator, # 2026-07-11): the cage must yaw WITH the torso twist, and glance views (hat: # left/right/rear) are UNFRAMED -- real pods showed a clean world view, no # cockpit framing (which is also why hat-rear looked "black": we were # rendering the cage's solid rear wall the real views never showed). # So per frame: (a) yaw the cage chain by the camera's torso-twist angle # (CAGE_TWIST_SIGN flips, default follows FP_TWIST_SIGN); (b) when the look # direction deviates > GLANCE_DEG (45) horizontally from the twisted hull # heading, hide all cockpit fixtures (GLANCE_HIDE=0 disables). # +1: calibrated by RENDERED A/B against the +68deg twist capture (ab2_*.png, # 2026-07-13): +1 brings the shell around WITH the twist (turret behavior, # small head-off-axis parallax is authentic); -1 rotates it out of view # entirely ("canopy does not rotate" report). The earlier numeric-only # calibration mixed yaw conventions -- trust the pictures. CAGE_SIGN = float(os.environ.get('CAGE_TWIST_SIGN', '1')) # REAR glance (hat-down / LookBehind) drops the canopy for a CLEAN rear view # (operator 2026-07-14: original hardware gave a clear rear, NO canopy # framing, on hat-down; left/right glances DO keep the canopy). Hide the # fixtures when the look deviates more than this from the twisted hull # heading. Left/right glances ~50 deg; rear ~180 deg -> 110 separates them. REAR_DEG = float(os.environ.get('REAR_GLANCE_DEG', '110')) GLANCE_HIDE = os.environ.get('GLANCE_HIDE', '1') != '0' GLANCE_DEG = float(os.environ.get('GLANCE_DEG', '45')) _ckpt = {'insts': None, 'fixh': frozenset(), 'cage': None, 'twist_dcs': frozenset(), 'shroud': None} def eyepoint_refresh(cache): """EYEPOINT (hat-glance) camera compose -- EXPLICIT OPT-IN ONLY. Wire forensics 2026-07-13: the game's glance (BT411 EyepointRotation / gyro eye-joint chain) never reached the 0x1f stream in the test egg -- the zero-translation anim nodes are STATIC rear-facing mounts (yaw 180 always), so auto-detection would permanently flip the camera. Until a live narrated capture identifies the real glance signal, the compose activates only with GLANCE_DCS= naming the node explicitly.""" ov = os.environ.get('GLANCE_DCS') _ckpt['eye'] = int(ov, 16) if ov else None def glance_probe(cache): """THE decisive glance test (rev 2026-07-14). CORRECTION: runtime DCS updates -- incl. the eyepoint reflush (BT411 FUN_0048e440 -> transmit action 0x1f, NOT action 3) -- ride the 0x1f articulation stream. The eye DCS = the cam-chain LEAF (the node the game list-adds to the view). This logs the leaf's live yaw AND flags any large-swing 0x1f mover that is OUTSIDE the cam chain (which we'd parse but never apply). A NARRATED 5s hat hold vs this line is the whole diagnosis: leaf yaw plateaus with the hold = the game IS emitting the glance and we should render it (find why we don't); leaf flat + an out-of-chain mover plateaus = we're dropping it (apply that handle); both flat = the game isn't emitting (upstream). Reports every ~4s alongside the frame line.""" chain = cache.cam_chain or [] if not chain: return leaf = chain[0] f = board.anim_abs.get(leaf) yaw = None if f and len(f) >= 12: yaw = math.degrees(math.atan2(f[6], -f[8])) now = time.time() if now - getattr(glance_probe, 'last', 0) > 4: glance_probe.last = now inchain = set(chain) movers = [] base = getattr(glance_probe, 'base', {}) for h, sc in JOINTS.items(): if h in inchain: continue ang = math.degrees(math.atan2(sc[0], sc[1])) b = base.setdefault(h, ang) if abs(ang - b) > 30: movers.append('%x=%+.0f' % (h, ang)) glance_probe.base = base # which OWN-mech instances are actually being DRAWN (inst_visible)? # the rear-black occluder is one of these -- identify it live. import vrview as _vv root = chain[-1] drawn = [] for inst in cache.instances: if root in inst['chain'] and _vv.inst_visible(board.nodes, inst): drawn.append('%x(r%.0f)' % (inst['handle'], inst.get('radius', 0))) print(f"GLANCE-PROBE: eye(leaf {leaf:x}) yaw=" f"{('%+.1f' % yaw) if yaw is not None else 'n/a'}" f" fp_cam.out_yaw={getattr(fp_cam, 'out_yaw', None)}" f" own drawn: {drawn}" f" movers: {movers[:4]}", flush=True) def cockpit_refresh(cache): """(Re)identify cockpit fixtures after a SceneCache rebuild: gated own-chain instances of small radius (beams are gated too but r=2000). The cage = the shell hanging DIRECTLY under the root (chain length 2). Keyed by HANDLE (instance dicts are recreated every rebuild) and keep-last-good: a rebuild that transiently fails detection (e.g. empty cam_chain mid-stream) must not wipe a previously found cage.""" if _ckpt['insts'] is cache.instances: return _ckpt['insts'] = cache.instances root = cache.cam_chain[-1] if cache.cam_chain else None fixh, cage, tdcs, shroud = set(), None, set(), None if root is not None: for inst in cache.instances: ch = inst['chain'] if not (root in ch and inst.get('gated') and not inst.get('billboard')): continue # ALL own gated instances twist with the turret (canopy, shroud, # AND the laser beams -- gun mounts orbit the same root axis; # fixes "lasers fire where the chassis faces", 2026-07-13)... tdcs.add(ch[0]) if inst.get('radius', 0) >= 100: continue # ...but beams never glance-HIDE fixh.add(inst['handle']) if len(ch) == 2 and inst.get('radius', 0) > 5: cage = ch[0] # ...and the big root-linked inward box = the mission-fade # SHROUD (9fd/BTPOVStartEndRenderable). It surrounds the mech # (open front, SOLID rear) so forward looks fine but a rear # glance / seat-back sees its black wall. It is a start/end # fade effect, NOT gameplay geometry -> hide it always # (operator 2026-07-14: "large black cube behind the cage"). shroud = inst['handle'] if fixh: changed = (fixh != _ckpt['fixh']) or (cage != _ckpt['cage']) _ckpt['fixh'], _ckpt['cage'] = frozenset(fixh), cage _ckpt['twist_dcs'] = frozenset(tdcs) _ckpt['shroud'] = shroud if changed: print(f"cockpit fixtures: {['%x' % h for h in sorted(fixh)]} " f"cage_dcs={'%x' % cage if cage else None} " f"shroud={'%x' % shroud if shroud else None} " f"twist_dcs={['%x' % d for d in sorted(tdcs)]} " f"root={root:x} ninst={len(cache.instances)}", flush=True) elif _ckpt['fixh']: print(f"cockpit refresh found nothing (root=" f"{'%x' % root if root else None}, ninst=" f"{len(cache.instances)}) -- keeping previous", flush=True) def hook_chain_matrix(r): """Wrap Renderer.chain_matrix: cockpit-fixture chains get the camera's torso-twist yaw composed ABOUT THE VEHICLE-ROOT AXIS (M' = M inv(Mr) Y Mr). Measured 2026-07-13: the canopy is hull-locked natively (its yaw tracks the hull exactly through twists) while the camera = hull yaw + JOINTS twist, so the fixture needs +twist -- but pivoted at the root: the earlier model-space pre-multiply (Y @ M) yawed the canopy about its OWN local origin, swinging it sideways in an arc ("moves further than the view").""" orig = r.chain_matrix def cm(board_, chain_, **kw): M = orig(board_, chain_, **kw) if chain_ and chain_[0] in _ckpt['twist_dcs']: a = CAGE_SIGN * getattr(fp_cam, 'twist_angle', 0.0) now = time.time() if now - cm.last > 3.0: cm.last = now print(f"fixture twist: dcs={chain_[0]:x} angle={a:+.2f}", flush=True) root = chain_[-1] f = board_.anim_abs.get(root) if a else None if a and f is not None and len(f) >= 12: Mr = np.eye(4) Mr[:3, :3] = np.array(f[:9], float).reshape(3, 3) Mr[3, :3] = f[9:12] cs, sn = math.cos(a), math.sin(a) Y = np.eye(4) Y[0, 0], Y[0, 2], Y[2, 0], Y[2, 2] = cs, -sn, sn, cs try: M = np.asarray(M, float) @ np.linalg.inv(Mr) @ Y @ Mr except np.linalg.LinAlgError: pass return M cm.last = 0.0 r.chain_matrix = cm print(f"chain_matrix hook installed on {type(r).__name__}", flush=True) def fp_cam(board, cache): """First-person cockpit camera, best source first: 1. HEAD-LOOK (default): the munga cam DCS chain's translation row = the true cockpit eye, its +Z row = the look direction (follows torso twist). The chain's full rotation is singular through our wire (two rows collapse to +-Y), so only eye + Z row are trusted and the basis is rebuilt y-up. FP_CAM=vehicle forces the fallback. 2. Fallback: the player vehicle's 0x1f root pose -- eye at hull + VEH_EYE, forward = -Z row (FP_FWD_SIGN flips). Side products (function attrs): .root (vehicle root DCS), .twist_angle (composed torso-twist yaw), .glance (look deviates > GLANCE_DEG from the twisted hull heading -- drives the unframed-glance cockpit hide).""" anim = board.anim_abs chain = cache.cam_chain h = None if chain and chain[-1] in anim: h = chain[-1] elif anim: h = max(anim, key=lambda k: float(np.abs(np.array(anim[k][9:12])).sum())) # h may be None before the FIRST vehicle articulation arrives (a freshly # dropped mech that hasn't moved sends no 0x1f yet -- both -egg and the # console flow). The CHAIN camera below works fine without it; bailing # out here left the raw default chain convention on screen, which put # the pre-movement view inside the mech's own geometry. R = t = None if h is not None: fp_cam.root = h # player vehicle root DCS (for the pick backchannel) f = anim[h] R = np.array(f[:9]).reshape(3, 3) t = np.array(f[9:12]) worldup = np.array([0.0, 1.0, 0.0]) eye = fwd = None fp_cam.path = 'vehicle' # telemetry: which camera source won if os.environ.get('FP_CAM', 'chain') != 'vehicle': try: Mc = np.asarray(CHAIN_CAM(board), float) ec, fc = Mc[3, :3], Mc[2, :3] # eye row; look = +Z row here n = np.linalg.norm(fc) if (np.isfinite(ec).all() and n > 1e-6 and (t is None or np.linalg.norm(ec - t) < 100.0)): eye = ec + worldup * UPOFF[0] fwd = fc / n fp_cam.path = 'chain' except Exception: pass if eye is None and R is None: return None if eye is None: # vehicle-root fallback: -Z row rendered forward (user-verified); # FP_FWD_SIGN flips it back if needed. fwd = float(os.environ.get('FP_FWD_SIGN', '-1')) * R[2] n = np.linalg.norm(fwd) if n < 1e-6: return None fwd = fwd / n eye = t + worldup * (VEH_EYE + UPOFF[0]) # NOTE: a bridge-level "head-glance axis fix" (swap the hat glance's yaw<-> # pitch) was tried 2026-07-07 and REVERTED: the hat glance, torso twist and # the stick-Y torso pitch all compose into this one look-vector and can't be # separated here (we only have the vehicle ROOT pose, not the torso joints), # so the swap also flipped the stick-Y vertical aim into yaw. The hat # left/right->up/down permutation must be fixed at the source (device # head-DCS decode -- the chain rotation is degenerate: X/Y rows collapse to # +-Y) or in the vRIO input mapping, without touching the look-vector. # torso twist: yaw the look direction by any chain-member joint angle # (jointtorso lives IN the cam chain; the shadow joint does not) twist_total = 0.0 if TWIST_ON: for jh in (chain or ()): sc = JOINTS.get(jh) if sc is None: continue th = TWIST_SIGN * math.atan2(sc[0], sc[1]) twist_total += th cs, sn = math.cos(th), math.sin(th) fwd = np.array([cs * fwd[0] + sn * fwd[2], fwd[1], -sn * fwd[0] + cs * fwd[2]]) # the EYE rides the torso too: swing it along the same turret arc about # the vehicle-root axis (operator 2026-07-13: the pilot head and cockpit # shell are rigid on the rotating torso -- ZERO shell/view parallax; the # eye-position arc is the authentic residual motion of the world view) if twist_total and t is not None: er = eye - t cs, sn = math.cos(twist_total), math.sin(twist_total) eye = t + np.array([cs * er[0] + sn * er[2], er[1], -sn * er[0] + cs * er[2]]) fp_cam.twist_angle = twist_total # hat-glance: compose the EYEPOINT rotation (eyepoint_refresh finds the # node; the game flushes it via 0x1f only while deflected, LookBehind = # yaw pi) ON TOP of the twist -- turret model: the glance is the pilot's # head relative to the twisted torso. The eyepoint hangs on a link # branch OUTSIDE the cam chain, so the chain camera alone never sees it # (wire-proven 2026-07-13). GLANCE_YAW_SIGN / GLANCE_PITCH_SIGN flip. fp_cam.glance_yaw = 0.0 eh = _ckpt.get('eye') ef = board.anim_abs.get(eh) if eh is not None else None if ef is not None and len(ef) >= 12: gy = math.atan2(ef[6], -ef[8]) * \ float(os.environ.get('GLANCE_YAW_SIGN', '1')) gp = math.asin(max(-1.0, min(1.0, ef[7]))) * \ float(os.environ.get('GLANCE_PITCH_SIGN', '1')) if abs(gy) > 0.005 or abs(gp) > 0.005: fp_cam.glance_yaw = gy cs, sn = math.cos(gy), math.sin(gy) fwd = np.array([cs * fwd[0] + sn * fwd[2], fwd[1], -sn * fwd[0] + cs * fwd[2]]) if abs(gp) > 0.005: right = np.cross(worldup, fwd) rn = np.linalg.norm(right) if rn > 1e-6: right /= rn cp, sp = math.cos(gp), math.sin(gp) fwd = fwd * cp + np.cross(right, fwd) * sp # glance detection: horizontal angle between the look and the TWISTED # hull heading; > GLANCE_DEG = a hat glance is held (drives the # unframed-glance cockpit hide). Stick-Y pitch never trips this # (horizontal-only; TORSO.SUB vertical limits are +10/-30 deg anyway). fp_cam.glance = False fp_cam.glance_dev = 0.0 # look deviation from twisted hull heading (deg) if R is not None: vf = float(os.environ.get('FP_FWD_SIGN', '-1')) * R[2] cs, sn = math.cos(twist_total), math.sin(twist_total) vf = np.array([cs * vf[0] + sn * vf[2], 0.0, -sn * vf[0] + cs * vf[2]]) lf = np.array([fwd[0], 0.0, fwd[2]]) nv, nl = np.linalg.norm(vf), np.linalg.norm(lf) if nv > 1e-6 and nl > 1e-6: cosang = max(-1.0, min(1.0, float(np.dot(vf, lf) / (nv * nl)))) fp_cam.glance = cosang < math.cos(math.radians(GLANCE_DEG)) fp_cam.glance_dev = math.degrees(math.acos(cosang)) eye = eye + fwd * FWDOFF[0] # seat forward/back trim back = -fwd right = np.cross(worldup, back) rn = np.linalg.norm(right) if rn < 1e-6: return None right /= rn up = np.cross(back, right) # FP_RIGHT_SIGN=-1 mirrors the image X (verified: exact fliplr). Tried as a # yaw fix 2026-07-06 but the inverted-yaw report was about the NATIVE C++ # render, not this bridge -- the mirror just flipped the arena layout and # made the bridge yaw wrong too. Keep +1 (the validated look). right *= float(os.environ.get('FP_RIGHT_SIGN', '1')) M = np.eye(4) M[0, :3], M[1, :3], M[2, :3], M[3, :3] = right, up, back, eye fp_cam.out_yaw = math.degrees(math.atan2(fwd[0], -fwd[2])) return M hook_chain_matrix(r) # cage twist rides every instance-chain evaluation def send_cam(M): # Backchannel on the fifosock: hand the device OUR camera (the # user-validated cockpit view, twist + glance included) so its reticle # raycast aims where the player actually looks. The device's own view # decode sits at the static view-node pose, which aimed every pick -- # and thus every missile volley and laser beam -- at one fixed wrong # world point (the "missiles fly off to a fixed spot" bug). if sock is None: return eye, fwd = M[3, :3], -M[2, :3] # 7th field: player vehicle root DCS -- the device skips instances on # this articulation subtree (own arms/torso/muzzle-flash), which the ray # grazes intermittently and which retargeted mid-volley missiles. root = getattr(fp_cam, 'root', 0) or 0 try: sock.send(f"CAM {eye[0]:.3f} {eye[1]:.3f} {eye[2]:.3f} " f"{fwd[0]:.6f} {fwd[1]:.6f} {fwd[2]:.6f} {root:x}\n".encode()) except OSError: pass def render(board): try: pg = r.pygame for ev in pg.event.get(): # drain before r.draw; tune eye height if ev.type == pg.QUIT: raise KeyboardInterrupt if ev.type == pg.KEYDOWN: hstep = {pg.K_UP: 1, pg.K_DOWN: -1}.get(ev.key) fstep = {pg.K_RIGHT: 0.1, pg.K_LEFT: -0.1}.get(ev.key) sstep = {pg.K_EQUALS: 0.05, pg.K_MINUS: -0.05, pg.K_KP_PLUS: 0.05, pg.K_KP_MINUS: -0.05}.get(ev.key) if hstep: UPOFF[0] += hstep if fstep: FWDOFF[0] += fstep if sstep: try: import vrview_gl vrview_gl.HUDSCALE[0] = max( 0.2, vrview_gl.HUDSCALE[0] + sstep) print(f"HUD scale = {vrview_gl.HUDSCALE[0]:.2f}", flush=True) except Exception: pass if ev.key == pg.K_w: # scene wireframe (GL backend) try: import vrview_gl vrview_gl.WIREFRAME[0] = not vrview_gl.WIREFRAME[0] print(f"wireframe " f"{'ON' if vrview_gl.WIREFRAME[0] else 'OFF'}", flush=True) except Exception: pass if ev.key == pg.K_v: # IR/thermal (pvision) manual toggle try: import vrview_gl vrview_gl.PVISION[0] = not vrview_gl.PVISION[0] print(f"IR/thermal (pvision) " f"{'ON' if vrview_gl.PVISION[0] else 'OFF'}", flush=True) except Exception: pass if hstep or fstep: print(f"eye trim: height {UPOFF[0]:+.1f} " f"forward {FWDOFF[0]:+.1f}", flush=True) r.cache.maybe_rebuild(board) cockpit_refresh(r.cache) eyepoint_refresh(r.cache) glance_probe(r.cache) M = fp_cam(board, r.cache) if M is not None: r.cam_matrix = lambda _b, _M=M: _M send_cam(M) # Glance-hide RETIRED 2026-07-14: (1) it referenced the stale _ckpt # key 'fix' (renamed to 'fixh'), throwing KeyError EVERY glance frame # -> render aborted -> screen froze during a hold and snapped back on # release (the "glances don't render" bug -- fp_cam was fine, the # crash was here). (2) Pilots confirm the canopy IS visible when # glancing (a11/MAX_COP has no rear geometry -> rear reads unframed # naturally), so hiding fixtures was anti-authentic anyway. # HIDE set: (a) the mission-fade SHROUD (9fd) always -- start/end fade # effect, not gameplay geometry; (b) on a REAR glance, the CANOPY too # (a11) -- hat-down gave a CLEAN rear view with NO framing on the # original hardware (operator 2026-07-14). Left/right glances keep the # canopy (dev ~50 < REAR_DEG); only the rear look (dev ~180) drops it. hide = set() if _ckpt.get('shroud') is not None: hide.add(_ckpt['shroud']) if getattr(fp_cam, 'glance_dev', 0.0) > REAR_DEG: hide |= set(_ckpt.get('fixh', ())) # canopy + shroud if hide: saved = r.cache.instances r.cache.instances = [i for i in saved if i['handle'] not in hide] try: r.draw(board) finally: r.cache.instances = saved else: r.draw(board) # AUTOSAVE: dump the live-rendered frame every ~20 frames so a held # glance can be grabbed and compared (diagnostic 2026-07-14). if os.environ.get('BRIDGE_AUTOSAVE') and frames % 20 == 0: try: from _backend import save_frame save_frame(r, os.environ['BRIDGE_AUTOSAVE']) except Exception: pass except KeyboardInterrupt: raise except Exception as e: global _render_errs _render_errs = globals().get('_render_errs', 0) + 1 if _render_errs <= 3: import traceback print(f"render error #{_render_errs}: {e}", flush=True) traceback.print_exc() sys.stdout.flush() # wire source: a fifodump file to tail, or "tcp:" = the device's # VPX_FIFOSOCK live tee (same VPXM records, no file-poll quantum; recv blocks # until data arrives so wire-to-render latency is the socket itself). tcp_port = int(path[4:]) if path.startswith('tcp:') else None sock = None def read_chunk(): global sock if tcp_port is None: return f.read(1 << 20) if sock is None: import socket as sk while True: s = sk.socket() try: s.connect(('127.0.0.1', tcp_port)) s.settimeout(0.02) # idle cap: keeps the event pump alive s.setsockopt(sk.IPPROTO_TCP, sk.TCP_NODELAY, 1) print("fifosock connected", flush=True) sock = s break except OSError: time.sleep(0.3) try: c = sock.recv(1 << 20) except TimeoutError: return b'' except OSError: sock = None return b'' if c == b'': print("fifosock closed; reconnecting", flush=True) sock = None return c if tcp_port is None: print(f"waiting for {path} ...") while not os.path.exists(path): time.sleep(0.2) f = open(path, 'rb') elif catchup and os.path.exists(catchup): # a socket client joining mid-mission missed the scene-create records; # replay the archival fifodump first (no rendering), then ride the tee. # Records between our EOF and the socket accept are lost -- poses are # absolute so the state self-heals within a frame. data = open(catchup, 'rb').read() o = fed = 0 while o + 8 <= len(data): if data[o:o + 4] != b'VPXM': o += 1; continue ln = struct.unpack_from(' len(data): break body = data[o + 8:o + 8 + ln]; o += 8 + ln if len(body) >= 4: a = struct.unpack_from(' Dave's renderer; drive the pod") pending = b'' frames = 0 skipped = 0 last_report = time.time() while True: chunk = read_chunk() if chunk: pending += chunk # Slice ALL complete records out of the buffer first, so we know which # draw_scene is the newest. Rendering every draw_scene in arrival order # means any render slowdown plays the mission slower than real time and # the view drifts seconds behind the game (backpressure hides in the # socket, not in len(pending)). State records all still apply, in order; # only superseded frame PRESENTS are skipped -- the view latches to the # freshest frame no matter how slow GL is. records = [] off = 0 n = len(pending) while n - off >= 8: if pending[off:off + 4] != b'VPXM': off += 1 continue ln = struct.unpack_from('= 4 and struct.unpack_from(' present a frame if i == last_draw: render(board) frames += 1 else: skipped += 1 if not chunk: try: r.pump() except KeyboardInterrupt: break except Exception: pass if tcp_port is None: time.sleep(0.02) # socket mode already waited in recv if time.time() - last_report > 4: last_report = time.time() # narrated-test aid: announce articulation handles first seen since # the last report (glance hunts: "holding hat-left NOW" vs this line) wa = set(board.anim_abs) - globals().get('_seen_anim', set()) wj = set(JOINTS) - globals().get('_seen_joints', set()) globals()['_seen_anim'] = set(board.anim_abs) globals()['_seen_joints'] = set(JOINTS) if wa or wj: print(f"wire: NEW anim={['%x' % h for h in sorted(wa)]} " f"joints={['%x' % h for h in sorted(wj)]}", flush=True) print(f"frames={frames} skipped={skipped} nodes={len(board.nodes)} " f"uploads={len(board.uploads)} tex={len(board.tex)} " f"munga={board.munga} anim_abs={len(board.anim_abs)} " f"backlog={len(pending)}B " f"twist={getattr(fp_cam, 'twist_angle', 0.0):+.2f} " f"glance={int(getattr(fp_cam, 'glance', False))} " f"gyaw={math.degrees(getattr(fp_cam, 'glance_yaw', 0.0)):+.0f} " f"joints={len(JOINTS)} cam={getattr(fp_cam, 'path', '?')}", flush=True)