Files
TeslaRel410/emulator/render-bridge/live_bridge.py
T
CydandClaude Opus 4.8 5593d57d48 Renderer: IR/thermal palette, bilinear default, searchlight (fog) + night egg
- IR / predator-vision thermal: the cockpit IR button toggles it via the
  wire (dpl_Effect on action 0x1b, mode -1 ON / -2 OFF -> board.pvision).
  vrview_gl present pass remaps scene luminance to a thermal palette
  (default heat/Predator; mono|green|amber via VRVIEW_PVISION_PALETTE;
  VRVIEW_PVISION / bridge 'v' key force it on). Exact palette + HUD-exempt
  pending crew review.
- Texture filter default flipped to bilinear (operator preference); the
  i860 board itself point-sampled, so VRVIEW_FILTER=nearest reverts. (A CRT
  present-pass bleed/scanline prototype was built and rejected; removed.)
- Searchlight: no code needed -- night A/B proved it is a VIEW-FOG push-back
  (fog near 5->60, far 400->500 on), already rendered by the existing view
  fog path. Not a light cone. Added TESTNITE.EGG (arena1/night/fog) +
  gauge_arena_night_pipe.conf for the test.

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

664 lines
31 KiB
Python

#!/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 <live.fifodump>
"""
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('<I', p, 0)[0], 64)
nodes = board.nodes
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 nodes.get(h, {}).get('type') != 5:
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
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=<hex> 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:<port>" = 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('<I', data, o + 4)[0]
if o + 8 + ln > len(data):
break
body = data[o + 8:o + 8 + ln]; o += 8 + ln
if len(body) >= 4:
a = struct.unpack_from('<I', body, 0)[0]
if a < 0x100:
try: board.handle(Msg(False, 0xff, a, body[4:]))
except Exception: pass
if a == 0x1f:
try: track_joints(body[4:])
except Exception: pass
fed += 1
print(f"catchup: {fed} records from {catchup}", flush=True)
print("tailing live wire -> 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('<I', pending, off + 4)[0]
if n - off < 8 + ln:
break # incomplete record: wait for more
records.append(pending[off + 8:off + 8 + ln])
off += 8 + ln
pending = pending[off:]
last_draw = -1
for i, body in enumerate(records):
if len(body) >= 4 and struct.unpack_from('<I', body, 0)[0] == 9:
last_draw = i
for i, body in enumerate(records):
if len(body) < 4:
continue
action = struct.unpack_from('<I', body, 0)[0]
if action < 0x100:
try:
board.handle(Msg(False, 0xff, action, body[4:]))
except Exception:
pass
if action == 0x1f: # torso/limb joint angles ride here
try: track_joints(body[4:])
except Exception: pass
if action == 9: # draw_scene -> 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)