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>
This commit is contained in:
@@ -13,8 +13,11 @@
|
||||
param(
|
||||
[string]$Conf = "$PSScriptRoot\gauge_arena_sound.conf",
|
||||
[string]$Work = "$env:LOCALAPPDATA\Temp\vwe-pod",
|
||||
[string]$BridgePos = '2020,20', # Division head slot (explode layout)
|
||||
[switch]$NoBridge,
|
||||
[switch]$NoSound
|
||||
[switch]$NoSound,
|
||||
[switch]$ShowNative # also show the native Division window
|
||||
# (wire-decode diagnostic; black clear)
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
New-Item -ItemType Directory -Force $Work | Out-Null
|
||||
@@ -40,8 +43,15 @@ $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:VPX_FIFODUMP = "$Work\live.fifodump" # archival/replay copy
|
||||
$env:VPX_FIFOSOCK = '8621' # live tee the bridge rides
|
||||
$env:RIO_TAP = "$Work\riotap.txt"
|
||||
# Dave's bridge is the out-the-window view; the native Division window is a
|
||||
# decode diagnostic (-ShowNative), cleared black so missing geometry doesn't
|
||||
# masquerade as sky.
|
||||
if ($ShowNative) { Remove-Item Env:VPX_NOMAIN -ErrorAction SilentlyContinue }
|
||||
else { $env:VPX_NOMAIN = '1' }
|
||||
$env:VPX_CLEAR = '0,0,0'
|
||||
if ($NoSound) {
|
||||
Remove-Item Env:VWE_AWE32, Env:VWE_AWE_ROM -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
@@ -58,10 +68,19 @@ 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" `
|
||||
# park the bridge window on the Division head slot (SDL honors this at
|
||||
# window creation)
|
||||
$env:SDL_VIDEO_WINDOW_POS = $BridgePos
|
||||
# bridge rides the socket tee (retries until the device listens); the
|
||||
# fifodump path is its catch-up source if it ever (re)starts mid-mission.
|
||||
# Launch with pyw (windowless pythonw), NOT py: py opens a console window
|
||||
# that parks over the displays, and Start-Process ignores -WindowStyle once
|
||||
# stdout/stderr are redirected (UseShellExecute=false). pyw has no console
|
||||
# at all; stdout/stderr still land in the bridge_*.txt logs, and the GL
|
||||
# render window appears normally at SDL_VIDEO_WINDOW_POS.
|
||||
$b = Start-Process pyw -ArgumentList '-3.13', "$PSScriptRoot\live_bridge.py", 'tcp:8621', "$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)"
|
||||
Write-Host "bridge PID $($b.Id) [GL, windowless] (arrows in the RENDER window tune eye height)"
|
||||
}
|
||||
|
||||
@@ -8,27 +8,74 @@ game's own camera (the player's RIO input drives it).
|
||||
|
||||
py live_bridge.py <live.fifodump>
|
||||
"""
|
||||
import os, sys, struct, time
|
||||
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
|
||||
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'))]
|
||||
# eye-height TRIM on top of the camera position (UP/DOWN arrows = +-5,
|
||||
# LEFT/RIGHT = +-1, live; render window must be focused). The chain camera
|
||||
# carries the true cockpit eye so the default trim is 0; the vehicle-root
|
||||
# fallback adds VEH_EYE below (12 was the hand-tuned value).
|
||||
UPOFF = [float(os.environ.get('FP_UPOFF', '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)
|
||||
|
||||
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)."""
|
||||
"""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)."""
|
||||
anim = board.anim_abs
|
||||
chain = cache.cam_chain
|
||||
h = None
|
||||
@@ -41,16 +88,47 @@ def fp_cam(board, cache):
|
||||
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]
|
||||
eye = fwd = None
|
||||
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
|
||||
np.linalg.norm(ec - t) < 100.0): # sane cockpit offset
|
||||
eye = ec + worldup * UPOFF[0]
|
||||
fwd = fc / n
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
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])
|
||||
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]])
|
||||
back = -fwd
|
||||
right = np.cross(worldup, back)
|
||||
rn = np.linalg.norm(right)
|
||||
@@ -86,20 +164,86 @@ def render(board):
|
||||
r.draw(board)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
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()
|
||||
|
||||
print(f"waiting for {path} ...")
|
||||
while not os.path.exists(path):
|
||||
time.sleep(0.2)
|
||||
f = open(path, 'rb')
|
||||
# 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
|
||||
last_report = time.time()
|
||||
while True:
|
||||
chunk = f.read(1 << 20)
|
||||
chunk = read_chunk()
|
||||
if chunk:
|
||||
pending += chunk
|
||||
off = 0
|
||||
@@ -120,6 +264,9 @@ while True:
|
||||
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
|
||||
render(board)
|
||||
frames += 1
|
||||
@@ -128,9 +275,10 @@ while True:
|
||||
try: r.pump()
|
||||
except KeyboardInterrupt: break
|
||||
except Exception: pass
|
||||
time.sleep(0.02)
|
||||
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()
|
||||
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)}")
|
||||
f"munga={board.munga} anim_abs={len(board.anim_abs)}", flush=True)
|
||||
|
||||
Reference in New Issue
Block a user