From 0f5b2e28da5b43734a34bd79df15d3746bdd0973 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 6 Jul 2026 13:34:02 -0500 Subject: [PATCH] Renderer: vendor live-bridge scripts; GPU backend unblocked (60fps) - emulator/render-bridge/: the pod->Dave's-renderer live bridge (rescued from session scratchpad): live_bridge.py (first-person cam from the 0x1f pose, arrow-key eye-height trim), fp/chase offline renders, fifobridge, diagnostics, gauge_arena[_sound].conf, and launch_pod.ps1 (one-command pod+bridge restart: pentapus VPX_EXPLODE + AWE32 sound + GL bridge). - _backend.py: renderer selection -- vrview_gl.GLRenderer (moderngl) by default, VRVIEW_SOFT=1 for the software reference; backend-agnostic frame save via last_frame. - GPU backend runs on side-by-side CPython 3.13 (moderngl/glcontext cp313 wheels; no MSVC needed): 63fps headless / 60.7 windowed vs 21fps software on the same scene, and the GL path also draws the vertex-alpha cloud dome. - vrview.py: VRVIEW_GAMMA env selects DAC gamma (1.25 live-renderer legacy vs 1.7 per the original GAMMA.C) for A/B against the real pod. Co-Authored-By: Claude Fable 5 --- dpl3-revive/patha/vrview.py | 9 +- emulator/render-bridge/_backend.py | 41 ++++++ emulator/render-bridge/cam_debug.py | 48 +++++++ emulator/render-bridge/chase_render.py | 31 ++++ emulator/render-bridge/diag.py | 15 ++ emulator/render-bridge/fifobridge.py | 82 +++++++++++ emulator/render-bridge/fp_render.py | 53 +++++++ emulator/render-bridge/gauge_arena.conf | 28 ++++ emulator/render-bridge/gauge_arena_sound.conf | 57 ++++++++ emulator/render-bridge/ground_check.py | 56 ++++++++ emulator/render-bridge/launch_pod.ps1 | 67 +++++++++ emulator/render-bridge/live_bridge.py | 136 ++++++++++++++++++ emulator/render-bridge/orbit.py | 68 +++++++++ emulator/render-bridge/phase0_hero.py | 58 ++++++++ emulator/render-bridge/phase0_render.py | 80 +++++++++++ emulator/render-bridge/playback.py | 43 ++++++ emulator/render-bridge/topdown.py | 45 ++++++ 17 files changed, 915 insertions(+), 2 deletions(-) create mode 100644 emulator/render-bridge/_backend.py create mode 100644 emulator/render-bridge/cam_debug.py create mode 100644 emulator/render-bridge/chase_render.py create mode 100644 emulator/render-bridge/diag.py create mode 100644 emulator/render-bridge/fifobridge.py create mode 100644 emulator/render-bridge/fp_render.py create mode 100644 emulator/render-bridge/gauge_arena.conf create mode 100644 emulator/render-bridge/gauge_arena_sound.conf create mode 100644 emulator/render-bridge/ground_check.py create mode 100644 emulator/render-bridge/launch_pod.ps1 create mode 100644 emulator/render-bridge/live_bridge.py create mode 100644 emulator/render-bridge/orbit.py create mode 100644 emulator/render-bridge/phase0_hero.py create mode 100644 emulator/render-bridge/phase0_render.py create mode 100644 emulator/render-bridge/playback.py create mode 100644 emulator/render-bridge/topdown.py diff --git a/dpl3-revive/patha/vrview.py b/dpl3-revive/patha/vrview.py index 965d997..2fa4cd3 100644 --- a/dpl3-revive/patha/vrview.py +++ b/dpl3-revive/patha/vrview.py @@ -430,6 +430,10 @@ class Renderer: self._last_ms = 0.0 self._psys = {} # SPECIALFX particle pools by code self.light = np.array([0.3, 0.8, 0.5]); self.light /= np.linalg.norm(self.light) + # Division 10-bit-DAC output gamma. GAMMA.C (the original source) builds + # out = (i/255)^(1/1.7); this live renderer historically used 1.25. + # VRVIEW_GAMMA selects it so the two can be compared against the real pod. + self.gamma = float(os.environ.get('VRVIEW_GAMMA', '1.25')) def dcs_matrix(self, board, h): m = None @@ -752,8 +756,9 @@ class Renderer: self.skip / 60.0) arr = np.clip(img, 0, 255).astype(np.uint8) - # Division DAC gamma (out = in^(1/1.25)) -- match render_preview.py - arr = (np.power(arr / 255.0, 1 / 1.25) * 255).astype(np.uint8) + # Division DAC output gamma (VRVIEW_GAMMA; GAMMA.C = 1.7, live-renderer + # legacy = 1.25) + arr = (np.power(arr / 255.0, 1.0 / self.gamma) * 255).astype(np.uint8) surf = pg.surfarray.make_surface(np.transpose(arr, (1, 0, 2))) self.screen.blit(surf, (0, 0)) pg.display.flip() diff --git a/emulator/render-bridge/_backend.py b/emulator/render-bridge/_backend.py new file mode 100644 index 0000000..ded5f03 --- /dev/null +++ b/emulator/render-bridge/_backend.py @@ -0,0 +1,41 @@ +"""Renderer backend selection for the render-bridge scripts. + +Default = the dpl3-revive GPU backend (vrview_gl.GLRenderer, moderngl); +VRVIEW_SOFT=1 forces the software rasterizer (vrview.Renderer), which stays +the debugging reference. Both share SceneCache/cam_matrix/draw/last_frame, +so scripts only swap the class. + +Run under the Windows CPython with the wheels installed (py -3.13): + numpy pygame-ce moderngl pillow +(the MSYS2 python has no pip; 3.14 has no moderngl/glcontext wheel yet). +""" +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), + '..', '..', 'dpl3-revive', 'patha')) + + +def pick_renderer(): + """Return (RendererClass, backend_name).""" + if os.environ.get('VRVIEW_SOFT') == '1': + from vrview import Renderer + return Renderer, 'software' + try: + from vrview_gl import GLRenderer + return GLRenderer, 'GL' + except Exception as e: + print(f"GL backend unavailable ({e}); using software rasterizer", + file=sys.stderr) + from vrview import Renderer + return Renderer, 'software' + + +def save_frame(r, out): + """Save the last rendered frame. Works headless on both backends + (GL has no screen surface -- read the FBO via last_frame).""" + arr = r.last_frame + if arr is None: + raise RuntimeError('no frame rendered yet') + from PIL import Image + Image.fromarray(arr).save(out) diff --git a/emulator/render-bridge/cam_debug.py b/emulator/render-bridge/cam_debug.py new file mode 100644 index 0000000..783f048 --- /dev/null +++ b/emulator/render-bridge/cam_debug.py @@ -0,0 +1,48 @@ +import os, sys, struct +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Msg, A +from vrview import Renderer + +data = open(sys.argv[1], 'rb').read() +board = VirtualBoard() +off = 0 +while off + 8 <= len(data): + if data[off:off + 4] != b'VPXM': + off += 1; continue + ln = struct.unpack_from('= 4: + a = struct.unpack_from(' 3 else '2' +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +from _backend import pick_renderer, save_frame +from vrboard import VirtualBoard, Msg, A +Renderer, backend = pick_renderer() +data = open(sys.argv[1], 'rb').read() +board = VirtualBoard() +off = 0 +while off + 8 <= len(data): + if data[off:off + 4] != b'VPXM': + off += 1; continue + ln = struct.unpack_from('= 4: + a = struct.unpack_from('=600: break +print("frames",b.frames,"munga",getattr(b,"munga",None)) +for attr in ("anim","anim_abs"): + v=getattr(b,attr,None) + print(attr, type(v).__name__, (len(v) if v is not None else None), (list(v)[:6] if v else None)) diff --git a/emulator/render-bridge/fifobridge.py b/emulator/render-bridge/fifobridge.py new file mode 100644 index 0000000..a8ad667 --- /dev/null +++ b/emulator/render-bridge/fifobridge.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Phase 1 core: feed OUR emulator's VPX_FIFODUMP (VPXM records = our game's +live VelociRender wire) into the friend's virtual board + renderer. Proves our +wire drives their renderer with no rebuild. Offline here; live = tail the file. + + py fifobridge.py [distmul] +""" +import os, sys, struct +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Msg, A +from vrview import Renderer + +cap = sys.argv[1] +out = sys.argv[2] if len(sys.argv) > 2 else 'fifobridge.png' +distmul = float(sys.argv[3]) if len(sys.argv) > 3 else 1.4 + +VALID = set(int(a) for a in A) + +def records(data): + off = 0 + while off + 8 <= len(data): + if data[off:off+4] != b'VPXM': + off += 1; continue + ln = struct.unpack_from('= 0x100: # init-arg string / non-message bursts + nskip += 1; continue # (real actions are all < 0x100, incl. + # the unnamed game ones: 0x1f, 0x2d...) + try: + board.handle(Msg(False, 0xff, action, body[4:])) + nfed += 1 + except Exception: + nerr += 1 +print(f'records={nrec} fed={nfed} skipped={nskip} errs={nerr}') +print(f'nodes={len(board.nodes)} geom={len(board.geom)} tex={len(board.tex)} ' + f'anim_abs={len(getattr(board,"anim_abs",{}))} munga={getattr(board,"munga",None)}') + +r = Renderer(w=832, h=512) +r.cache.maybe_rebuild(board) +c = r.cache +print(f'instances={len(c.instances)}') + +# synthetic aerial camera framing all decoded geometry + lift far clip +mins = np.array([np.inf]*3); maxs = np.array([-np.inf]*3); nv = 0 +for inst in c.instances: + M = r.chain_matrix(board, inst['chain']) + for gh in inst['geoms']: + mesh = c._mesh(board, gh) + if not mesh or 'pos' not in mesh or len(mesh['pos']) == 0: + continue + pw = mesh['pos'] @ M[:3, :3] + M[3, :3] + mins = np.minimum(mins, pw.min(0)); maxs = np.maximum(maxs, pw.max(0)); nv += len(pw) +if not np.isfinite(mins).all(): + print('NO renderable geometry'); sys.exit(1) +center = (mins+maxs)/2; radius = float(np.linalg.norm(maxs-mins)/2) or 400.0 +print(f'bounds min={mins.round(1)} max={maxs.round(1)} center={center.round(1)} radius={radius:.1f} verts={nv}') + +eye = center + np.array([radius*0.5, -radius*0.8, radius*distmul]) +fwd = center - eye; fwd /= np.linalg.norm(fwd); back = -fwd +right = np.cross([0.0,-1.0,0.0], back); right /= np.linalg.norm(right) +up = np.cross(back, right) +M = np.eye(4); M[0,:3],M[1,:3],M[2,:3],M[3,:3] = right, up, back, eye +r.cam_matrix = lambda _b, _M=M: _M +if c.view is not None and c.view in board.nodes: + vb = bytearray(board.nodes[c.view].get('body') or b'') + if len(vb) >= 56: + struct.pack_into(' [upoff] [worldup_y] [fwd_sign]""" +import os, sys, struct +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from _backend import pick_renderer, save_frame +from vrboard import VirtualBoard, Msg, A +Renderer, backend = pick_renderer() + +data = open(sys.argv[1], 'rb').read() +out = sys.argv[2] +upoff = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0 +wuy = float(sys.argv[4]) if len(sys.argv) > 4 else 1.0 # worldup y sign +fsign = float(sys.argv[5]) if len(sys.argv) > 5 else 1.0 # nose sign +board = VirtualBoard() +off = 0 +while off + 8 <= len(data): + if data[off:off + 4] != b'VPXM': + off += 1; continue + ln = struct.unpack_from('= 4: + a = struct.unpack_from('= 4: + a = struct.unpack_from('= 96: + f = struct.unpack_from('<24f', vb, 0) + print('VIEW hither(near)=%.1f yon(far)=%.1f zeye=%.2f imageplane x[%.2f,%.2f] y[%.2f,%.2f]' + % (f[12], f[13], f[9], f[5], f[7], f[6], f[8])) + +allpos = [] +for inst in c.instances: + M = r.chain_matrix(board, inst['chain']) + for g in inst['geoms']: + mesh = c._mesh(board, g) + if not mesh or 'pos' not in mesh or len(mesh['pos']) == 0: + continue + pw = mesh['pos'] @ M[:3, :3] + M[3, :3] + allpos.append(pw) +allpos = np.concatenate(allpos) +print('ALL verts: %d Y[min=%.1f max=%.1f median=%.1f]' + % (len(allpos), allpos[:, 1].min(), allpos[:, 1].max(), np.median(allpos[:, 1]))) + +# geometry near the mech in the XZ plane (would be the ground under/around it) +d = np.sqrt((allpos[:, 0] - mech[0])**2 + (allpos[:, 2] - mech[2])**2) +for rad in (100, 300, 800, 2000): + near = allpos[d < rad] + if len(near): + print(' within %4d of mech: %6d verts Y[%.1f..%.1f] med %.1f' + % (rad, len(near), near[:, 1].min(), near[:, 1].max(), np.median(near[:, 1]))) + else: + print(' within %4d of mech: 0 verts (NO geometry here)' % rad) diff --git a/emulator/render-bridge/launch_pod.ps1 b/emulator/render-bridge/launch_pod.ps1 new file mode 100644 index 0000000..8d9771e --- /dev/null +++ b/emulator/render-bridge/launch_pod.ps1 @@ -0,0 +1,67 @@ +# Launch the full pod experience: DOSBox-X pod (pentapus 7-display explode +# mode + dual-AWE32 sound) + Dave's GL renderer bridged to the live wire. +# +# .\launch_pod.ps1 # restart pod + bridge (stops previous run) +# .\launch_pod.ps1 -NoSound # skip AWE32 (saves the ~4 min SBK upload) +# .\launch_pod.ps1 -NoBridge # pod only +# .\launch_pod.ps1 -Conf # different mission/conf +# +# Pentapus mode (VPX_EXPLODE=1): all 7 cockpit displays as desktop windows -- +# 5 mono MFDs (green phosphor), portrait radar, Division main -- needs a wide +# desktop (~2830px). Per-window override: VPX_WIN/VPX_MAIN = "x,y[,w,h]". +# Eye height for the bridge camera: FP_UPOFF env (see live_bridge.py). +param( + [string]$Conf = "$PSScriptRoot\gauge_arena_sound.conf", + [string]$Work = "$env:LOCALAPPDATA\Temp\vwe-pod", + [switch]$NoBridge, + [switch]$NoSound +) +$ErrorActionPreference = 'Stop' +New-Item -ItemType Directory -Force $Work | Out-Null + +# a restart means the previous run dies first (COM1 + the fifodump are exclusive) +foreach ($pidfile in "$Work\pod_pid.txt", "$Work\bridge_pid.txt") { + if (Test-Path $pidfile) { + $old = Get-Process -Id (Get-Content $pidfile) -ErrorAction SilentlyContinue + if ($old) { Write-Host "stopping previous $([IO.Path]::GetFileNameWithoutExtension($pidfile)) ($($old.Id))"; $old | Stop-Process -Force } + } +} +# the bridge pid file records the py.exe wrapper; its python child holds the +# fifodump open (blocks the Remove-Item below) -- sweep it by command line +Get-CimInstance Win32_Process -Filter "Name like 'py%'" | + Where-Object { $_.CommandLine -match 'live_bridge' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +# fresh wire artifacts each run (the RIO tap APPENDS across runs otherwise -- +# stale init sequences from the previous run poison diagnosis) +Remove-Item "$Work\live.fifodump", "$Work\riotap.txt" -ErrorAction SilentlyContinue + +$env:VPXLOG = "$Work\vpxresp.txt" +$env:VPX_RESPOND = '1' +$env:VPX_RENDER = '1' +$env:VPX_EXPLODE = '1' # pentapus: 7 cockpit displays +$env:VPX_DUMPDIR = $Work +$env:VPX_FIFODUMP = "$Work\live.fifodump" +$env:RIO_TAP = "$Work\riotap.txt" +if ($NoSound) { + Remove-Item Env:VWE_AWE32, Env:VWE_AWE_ROM -ErrorAction SilentlyContinue +} else { + $env:VWE_AWE32 = '1' + $env:VWE_AWE_ROM = (Resolve-Path "$PSScriptRoot\..\roms\awe32.raw").Path +} + +$dosbox = (Resolve-Path "$PSScriptRoot\..\src\src\dosbox-x.exe").Path +$p = Start-Process $dosbox -ArgumentList '-conf', $Conf ` + -RedirectStandardError "$Work\pod_err.txt" ` + -RedirectStandardOutput "$Work\pod_out.txt" -PassThru +$p.Id | Set-Content "$Work\pod_pid.txt" +Write-Host "pod PID $($p.Id) conf=$([IO.Path]::GetFileName($Conf)) work=$Work" +if (-not $NoSound) { Write-Host "sound ON: boot adds ~4 min for the SoundFont upload" } + +if (-not $NoBridge) { + # live_bridge waits for the fifodump itself; start it right away + $b = Start-Process py -ArgumentList '-3.13', "$PSScriptRoot\live_bridge.py", "$Work\live.fifodump" ` + -RedirectStandardError "$Work\bridge_err.txt" ` + -RedirectStandardOutput "$Work\bridge_out.txt" -PassThru + $b.Id | Set-Content "$Work\bridge_pid.txt" + Write-Host "bridge PID $($b.Id) [GL] (arrows in its window tune eye height)" +} diff --git a/emulator/render-bridge/live_bridge.py b/emulator/render-bridge/live_bridge.py new file mode 100644 index 0000000..5bf9636 --- /dev/null +++ b/emulator/render-bridge/live_bridge.py @@ -0,0 +1,136 @@ +#!/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 +import numpy as np +from _backend import pick_renderer +from vrboard import VirtualBoard, Msg, A +Renderer, backend = pick_renderer() + +path = sys.argv[1] +board = VirtualBoard() +board.munga = False +r = Renderer(w=832, 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 height above the mech origin (cockpit). Mutable: UP/DOWN arrows = +-5, +# LEFT/RIGHT = +-1, live (the render window must be focused). 35 was tuned +# before the ground rendered and is way too high against real terrain. +UPOFF = [float(os.environ.get('FP_UPOFF', '12'))] + +def fp_cam(board, cache): + """First-person cockpit camera from the player vehicle's 0x1f pose: eye at + the mech (+up), looking along its nose (+Z row), y-up world. Beats Dave's + 11-DCS cam chain (which renders sky through our wire's convention).""" + 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())) + if h is None: + return None + f = anim[h] + R = np.array(f[:9]).reshape(3, 3) + t = np.array(f[9:12]) + # look along the mech's travel direction. The +Z row rendered BACKWARD + # (user: "geometry moving away as I drive"), so forward = -Z row. + # 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 + worldup = np.array([0.0, 1.0, 0.0]) + eye = t + worldup * UPOFF[0] + 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 + return M + +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: + step = {pg.K_UP: 5, pg.K_DOWN: -5, + pg.K_RIGHT: 1, pg.K_LEFT: -1}.get(ev.key) + if step: + UPOFF[0] = max(0.0, UPOFF[0] + step) + print(f"eye height = {UPOFF[0]:.0f}", flush=True) + r.cache.maybe_rebuild(board) + M = fp_cam(board, r.cache) + if M is not None: + r.cam_matrix = lambda _b, _M=M: _M + r.draw(board) + except KeyboardInterrupt: + raise + except Exception: + pass + +print(f"waiting for {path} ...") +while not os.path.exists(path): + time.sleep(0.2) +f = open(path, 'rb') +print("tailing live wire -> Dave's renderer; drive the pod") + +pending = b'' +frames = 0 +last_report = time.time() +while True: + chunk = f.read(1 << 20) + if chunk: + pending += chunk + off = 0 + n = len(pending) + while n - off >= 8: + if pending[off:off + 4] != b'VPXM': + off += 1 + continue + ln = struct.unpack_from('= 4: + action = struct.unpack_from(' present a frame + render(board) + frames += 1 + pending = pending[off:] + if not chunk: + try: r.pump() + except KeyboardInterrupt: break + except Exception: pass + time.sleep(0.02) + if time.time() - last_report > 4: + last_report = time.time() + print(f"frames={frames} nodes={len(board.nodes)} " + f"uploads={len(board.uploads)} tex={len(board.tex)} " + f"munga={board.munga} anim_abs={len(board.anim_abs)}") diff --git a/emulator/render-bridge/orbit.py b/emulator/render-bridge/orbit.py new file mode 100644 index 0000000..7db2fef --- /dev/null +++ b/emulator/render-bridge/orbit.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Show OUR BTL4OPT arena (decoded by the friend's board from a captured wire +stream) in a real on-screen window, orbiting a synthetic camera around it. +Decode once, then orbit forever; close the window to stop.""" +import os, sys, struct, math +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +# real window (no SDL dummy) +import numpy as np +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start +from vrview import Renderer + +cap = sys.argv[1] if len(sys.argv) > 1 else r'C:\VWE\TeslaRel410\dpl3-revive\patha\bt8.raw.bin' +upto = int(sys.argv[2]) if len(sys.argv) > 2 else 800 +W, H = 720, 450 + +data = open(cap, 'rb').read() +board = VirtualBoard() +asm = Assembler(); asm.feed(data[find_framed_start(data):]) +for m in asm: + try: board.handle(m) + except Exception: pass + if board.frames >= upto: break +print(f'decoded: nodes={len(board.nodes)} frames={board.frames}') + +W, H = 560, 350 +r = Renderer(w=W, h=H, title='VWE pod - Division render (dpl3-revive) - our BTL4OPT arena') +r.cache.maybe_rebuild(board) +c = r.cache + +# aim at the mechs (anim_abs vehicle positions); ignore ones still at origin +mechs = np.array([np.array(f[9:12]) for f in board.anim_abs.values()]) +real = mechs[np.abs(mechs).sum(1) > 1.0] +center = real.mean(0) if len(real) else np.zeros(3) +radius = 480.0 # close orbit around the mech/dome cluster +print(f'mechs={mechs.round(0).tolist()} target={center.round(1)} radius={radius}') + +# lift the far clip so the whole arena renders (yon = view float idx 13, off 52) +if c.view is not None and c.view in board.nodes: + vb = bytearray(board.nodes[c.view].get('body') or b'') + if len(vb) >= 56: + struct.pack_into(' 4 else 900.0 +high = float(sys.argv[5]) if len(sys.argv) > 5 else 350.0 + +data = open(cap, 'rb').read() +board = VirtualBoard() +asm = Assembler(); asm.feed(data[find_framed_start(data):]) +for m in asm: + try: board.handle(m) + except Exception: pass + if board.frames >= upto: break + +r = Renderer(w=832, h=512) +r.cache.maybe_rebuild(board) +c = r.cache + +# aim at the centroid of the animated vehicles (the mechs) +pts = np.array([np.array(f[9:12]) for f in board.anim_abs.values()]) +target = pts.mean(0) +print('mech positions:'); print(pts.round(1)) +print('target', target.round(1)) + +# camera: pulled back (+z), raised (-y is up in this y-down world) +eye = target + np.array([dist*0.4, -high, dist]) +fwd = target - eye; fwd /= np.linalg.norm(fwd) +back = -fwd +worldup = np.array([0.0, -1.0, 0.0]) +right = np.cross(worldup, back); right /= np.linalg.norm(right) +up = np.cross(back, right) +M = np.eye(4) +M[0,:3], M[1,:3], M[2,:3], M[3,:3] = right, up, back, eye +r.cam_matrix = lambda _b, _M=M: _M + +# lift the far clip (yon = view-body float idx 13 -> byte offset 52) so the +# whole arena renders instead of being culled at the fog wall +if c.view is not None and c.view in board.nodes: + vb = bytearray(board.nodes[c.view].get('body') or b'') + if len(vb) >= 56: + struct.pack_into(' 1e6') + +r.draw(board) +r.pygame.image.save(r.screen, out) +print('wrote', out) diff --git a/emulator/render-bridge/phase0_render.py b/emulator/render-bridge/phase0_render.py new file mode 100644 index 0000000..50a1e96 --- /dev/null +++ b/emulator/render-bridge/phase0_render.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Phase 0: replay a captured BTL4OPT VelociRender stream through the friend's +virtual board and render the decoded world from a synthetic aerial camera +(the game's own camera path isn't decoded yet -> singular matrix). Proves their +renderer reconstructs OUR game's geometry+textures from the live wire. + + py phase0_render.py [distmul] [ymul] +""" +import os, sys +PATHA = r'C:\VWE\TeslaRel410\dpl3-revive\patha' +sys.path.insert(0, PATHA) +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start +from vrview import Renderer + +cap = sys.argv[1] +upto = int(sys.argv[2]) if len(sys.argv) > 2 else 400 +out = sys.argv[3] +distmul = float(sys.argv[4]) if len(sys.argv) > 4 else 1.6 +ymul = float(sys.argv[5]) if len(sys.argv) > 5 else 1.2 # +up (y-down world -> negate below) + +data = open(cap, 'rb').read() +board = VirtualBoard() +asm = Assembler(); asm.feed(data[find_framed_start(data):]) +n = 0 +for m in asm: + try: + board.handle(m) + except Exception as e: + n += 1 + if board.frames >= upto: + break +print(f'replayed frame={board.frames} nodes={len(board.nodes)} ' + f'geom={len(board.geom)} tex={len(board.tex)} handler_errs={n}') + +r = Renderer(w=832, h=512) +r.cache.maybe_rebuild(board) +c = r.cache +print(f'instances={len(c.instances)}') + +# world-space bounds over every renderable instance triangle +mins = np.array([np.inf]*3); maxs = np.array([-np.inf]*3) +nverts = 0 +for inst in c.instances: + M = r.chain_matrix(board, inst['chain']) + for gh in inst['geoms']: + mesh = c._mesh(board, gh) + if not mesh or 'pos' not in mesh or len(mesh['pos']) == 0: + continue + pw = mesh['pos'] @ M[:3, :3] + M[3, :3] + mins = np.minimum(mins, pw.min(0)); maxs = np.maximum(maxs, pw.max(0)) + nverts += len(pw) +if not np.isfinite(mins).all(): + print('NO renderable geometry'); sys.exit(1) +center = (mins + maxs) / 2.0 +size = maxs - mins +radius = float(np.linalg.norm(size) / 2.0) or 100.0 +print(f'world bounds min={mins.round(1)} max={maxs.round(1)}') +print(f'center={center.round(1)} size={size.round(1)} radius={radius:.1f} verts={nverts}') + +# synthetic aerial look-at. DPL world is y-DOWN, so "up above" = -y. +eye = center + np.array([radius*0.6, -radius*ymul, radius*distmul]) +fwd = center - eye; fwd /= max(np.linalg.norm(fwd), 1e-6) +back = -fwd +worldup = np.array([0.0, -1.0, 0.0]) # y-down +right = np.cross(worldup, back); right /= max(np.linalg.norm(right), 1e-6) +up = np.cross(back, right) +M = np.eye(4) +M[0, :3], M[1, :3], M[2, :3], M[3, :3] = right, up, back, eye + +# widen the far clip so the aerial distance doesn't cull the whole scene +r.cam_matrix = lambda _b, _M=M: _M +if c.view is not None and c.view in board.nodes: + pass # projection still read from the view body inside draw() + +r.draw(board) +r.pygame.image.save(r.screen, out) +print('wrote', out) diff --git a/emulator/render-bridge/playback.py b/emulator/render-bridge/playback.py new file mode 100644 index 0000000..2a5703e --- /dev/null +++ b/emulator/render-bridge/playback.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Play a captured BTL4OPT VelociRender stream back through the friend's board ++ software renderer in a REAL on-screen window (chase cam follows a mech). +Loops forever; close the window to stop. + + py playback.py [capture.raw.bin] [WxH] [chase] +""" +import os, sys, time +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +# real window: do NOT set SDL_VIDEODRIVER=dummy +cap = sys.argv[1] if len(sys.argv) > 1 else r'C:\VWE\TeslaRel410\dpl3-revive\patha\bt8.raw.bin' +size = sys.argv[2] if len(sys.argv) > 2 else '640x400' +os.environ['VRVIEW_CHASE'] = sys.argv[3] if len(sys.argv) > 3 else '2' +W, H = (int(x) for x in size.lower().split('x')) + +import numpy as np +from vrboard import VirtualBoard, Assembler, A +from decode_anim import find_framed_start +from vrview import Renderer + +data = open(cap, 'rb').read() +start = find_framed_start(data) +r = Renderer(w=W, h=H, + title='VWE pod - Division render (dpl3-revive board) - our BTL4OPT arena') +r.fps = 30 + +print(f'playing {cap} ({len(data)} bytes) in a {W}x{H} window; close it to stop') +try: + while True: # loop the capture + board = VirtualBoard(); board._view = r + asm = Assembler(); asm.feed(data[start:]) + drew = 0 + for m in asm: + try: + board.handle(m) + except Exception: + pass + if (not m.iserver) and m.action == A.draw_scene: + r.draw(board) # renders (chase cam) + paces to fps + drew += 1 + print(f' looped: {drew} frames drawn, restarting') +except KeyboardInterrupt: + print('window closed') diff --git a/emulator/render-bridge/topdown.py b/emulator/render-bridge/topdown.py new file mode 100644 index 0000000..3b69506 --- /dev/null +++ b/emulator/render-bridge/topdown.py @@ -0,0 +1,45 @@ +import os, sys, struct +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Msg, A +from vrview import Renderer + +data = open(sys.argv[1], 'rb').read() +board = VirtualBoard() +off = 0 +while off + 8 <= len(data): + if data[off:off + 4] != b'VPXM': + off += 1; continue + ln = struct.unpack_from('= 4: + a = struct.unpack_from(' 3 else 1.0 # +1 or -1 for up axis +dist = float(sys.argv[4]) if len(sys.argv) > 4 else 900.0 +# camera high above the mech, looking straight down; 'north' = +z as screen up +eye = mech + np.array([0.0, updir * dist, 0.0]) +back = np.array([0.0, updir, 0.0]) # look -back = down +worldref = np.array([0.0, 0.0, 1.0]) +right = np.cross(worldref, back); right /= max(np.linalg.norm(right), 1e-6) +up = np.cross(back, right) +M = np.eye(4); M[0, :3], M[1, :3], M[2, :3], M[3, :3] = right, up, back, eye +r.cam_matrix = lambda _b, _M=M: _M +# widen far clip so the whole area shows +if c.view and c.view in board.nodes: + vb = bytearray(board.nodes[c.view].get('body') or b'') + if len(vb) >= 56: + struct.pack_into('