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:
@@ -106,6 +106,8 @@ class VirtualBoard:
|
|||||||
self.morphs = {} # morphed geom -> (a, b, alpha)
|
self.morphs = {} # morphed geom -> (a, b, alpha)
|
||||||
self.names = {} # 0x8000xxxx name id -> handle (0x22)
|
self.names = {} # 0x8000xxxx name id -> handle (0x22)
|
||||||
self.sfx = {} # SPECIALFX defs by code (0x1c)
|
self.sfx = {} # SPECIALFX defs by code (0x1c)
|
||||||
|
self.dl2d = {} # 2D display lists (0x29/0x2b): handle -> [tag words]
|
||||||
|
self._dl2d_acc = None # in-progress chunked 0x2b flush
|
||||||
self.munga = False # game-build dialect (rpl4opt/btl4opt)
|
self.munga = False # game-build dialect (rpl4opt/btl4opt)
|
||||||
self.frames = 0
|
self.frames = 0
|
||||||
self.log = []
|
self.log = []
|
||||||
@@ -238,6 +240,31 @@ class VirtualBoard:
|
|||||||
h = struct.unpack_from('<I', msg.payload, 4)[0]
|
h = struct.unpack_from('<I', msg.payload, 4)[0]
|
||||||
self.anim4[h] = struct.unpack_from('<16f', msg.payload, 8)
|
self.anim4[h] = struct.unpack_from('<16f', msg.payload, 8)
|
||||||
return b''
|
return b''
|
||||||
|
if msg.action == 0x29 and len(msg.payload) >= 4:
|
||||||
|
# dpl2d_NewDisplayList: [remote handle][0] (create/reset a 2D
|
||||||
|
# overlay display list -- the HUD layer; fire-and-forget)
|
||||||
|
h = struct.unpack_from('<I', msg.payload, 0)[0]
|
||||||
|
self.dl2d[h] = []
|
||||||
|
return b''
|
||||||
|
if msg.action == 0x2b and len(msg.payload) >= 20:
|
||||||
|
# dpl2d_FlushDisplayList: the host streams the dpl2d_DISPLAY
|
||||||
|
# chunk structs verbatim (DPL_2D.H: remote, next, tail, size,
|
||||||
|
# open, data[30]). First chunk carries remote=handle and
|
||||||
|
# tail=total chunk count; continuation chunks have remote=0.
|
||||||
|
# Data words concatenate across chunks (tag args split freely
|
||||||
|
# at chunk boundaries); tags per DPL2DTAG.H.
|
||||||
|
hdr, _nxt, tail, size = struct.unpack_from('<4I', msg.payload, 0)
|
||||||
|
size = min(size, (len(msg.payload) - 20) // 4)
|
||||||
|
dat = list(struct.unpack_from(f'<{size}I', msg.payload, 20))
|
||||||
|
if hdr:
|
||||||
|
self._dl2d_acc = [hdr, max(tail, 1) - 1, dat]
|
||||||
|
elif self._dl2d_acc is not None:
|
||||||
|
self._dl2d_acc[1] -= 1
|
||||||
|
self._dl2d_acc[2] += dat
|
||||||
|
if self._dl2d_acc is not None and self._dl2d_acc[1] <= 0:
|
||||||
|
self.dl2d[self._dl2d_acc[0]] = self._dl2d_acc[2]
|
||||||
|
self._dl2d_acc = None
|
||||||
|
return b''
|
||||||
self.log.append(f"EXTRA action {msg.action:#x} ({len(msg.payload)}B) -- no reply")
|
self.log.append(f"EXTRA action {msg.action:#x} ({len(msg.payload)}B) -- no reply")
|
||||||
return b''
|
return b''
|
||||||
p = msg.payload
|
p = msg.payload
|
||||||
|
|||||||
+192
-3
@@ -38,6 +38,138 @@ def _mat_from_dcs(body):
|
|||||||
return np.frombuffer(body[12:76], dtype='<f4').reshape(4, 4).astype(np.float64)
|
return np.frombuffer(body[12:76], dtype='<f4').reshape(4, 4).astype(np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 2D display-list HUD overlay (actions 0x29/0x2b; tags per DPL2DTAG.H) ----
|
||||||
|
# The game draws its HUD (reticle, twist/pitch ladders, warning glyphs) through
|
||||||
|
# dpl2d_* display lists composited over the view -- L4VIDRND.CPP builds them,
|
||||||
|
# the view node binds the root list at stored-body word 25. Coordinates are
|
||||||
|
# view-rect space (x0..x1, y0..y1, y up), colors 0..1 floats.
|
||||||
|
_HUD2D_TAGS = 23
|
||||||
|
_HUD2D_F = struct.Struct('<f')
|
||||||
|
|
||||||
|
|
||||||
|
def _hud2d_f(w):
|
||||||
|
return _HUD2D_F.unpack(struct.pack('<I', w))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def hud2d_prims(dl2d, root):
|
||||||
|
"""Execute display list `root` -> [(kind, (r,g,b), width, [(x,y)..])].
|
||||||
|
kind: 'strip' (connected polyline), 'segs' (independent segments, point
|
||||||
|
pairs), 'poly' (filled convex polygon), 'points'."""
|
||||||
|
prims = []
|
||||||
|
st = {'m': (1.0, 0.0, 0.0, 1.0, 0.0, 0.0), 'col': (0.0, 0.75, 0.0), 'w': 1.0}
|
||||||
|
stack = []
|
||||||
|
mode = [None] # current open path kind (or 'clip')
|
||||||
|
pts = []
|
||||||
|
|
||||||
|
def xf(x, y):
|
||||||
|
m = st['m']
|
||||||
|
return (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
|
||||||
|
|
||||||
|
def emit(kind):
|
||||||
|
if pts and kind not in (None, 'clip'):
|
||||||
|
prims.append((kind, st['col'], st['w'], list(pts)))
|
||||||
|
del pts[:]
|
||||||
|
mode[0] = None
|
||||||
|
|
||||||
|
def run(h, depth):
|
||||||
|
w = dl2d.get(h)
|
||||||
|
if w is None or depth > 8:
|
||||||
|
return
|
||||||
|
i, n = 0, len(w)
|
||||||
|
while i < n:
|
||||||
|
t = w[i]
|
||||||
|
i += 1
|
||||||
|
if t == 0: # open_polyline
|
||||||
|
emit(mode[0]); mode[0] = 'strip'
|
||||||
|
elif t == 1:
|
||||||
|
emit('strip')
|
||||||
|
elif t == 2: # open_polypoint
|
||||||
|
emit(mode[0]); mode[0] = 'points'
|
||||||
|
elif t == 3:
|
||||||
|
emit('points')
|
||||||
|
elif t == 4: # open_polygon
|
||||||
|
emit(mode[0]); mode[0] = 'poly'
|
||||||
|
elif t == 5:
|
||||||
|
emit('poly')
|
||||||
|
elif t == 6: # open_lines (point PAIRS)
|
||||||
|
emit(mode[0]); mode[0] = 'segs'
|
||||||
|
elif t == 7:
|
||||||
|
emit('segs')
|
||||||
|
elif t == 8 and i + 2 <= n: # point x y
|
||||||
|
if mode[0] is not None:
|
||||||
|
pts.append(xf(_hud2d_f(w[i]), _hud2d_f(w[i + 1])))
|
||||||
|
i += 2
|
||||||
|
elif t == 9 and i + 4 <= n: # circle x y r filled
|
||||||
|
cx, cy, r = (_hud2d_f(v) for v in w[i:i + 3])
|
||||||
|
filled = w[i + 3] != 0
|
||||||
|
i += 4
|
||||||
|
ring = [xf(cx + r * np.cos(a), cy + r * np.sin(a))
|
||||||
|
for a in np.linspace(0, 2 * np.pi, 25)]
|
||||||
|
prims.append(('poly' if filled else 'strip',
|
||||||
|
st['col'], st['w'], ring))
|
||||||
|
elif t == 10: # open_clip_polygon (unsupported:
|
||||||
|
emit(mode[0]); mode[0] = 'clip' # collect + discard)
|
||||||
|
elif t == 11: # close_clip_polygon <mode>
|
||||||
|
emit('clip'); i += 1
|
||||||
|
elif t == 12: # clip_circle x y r <mode>
|
||||||
|
i += 4
|
||||||
|
elif t in (13, 14): # clip_full / clip_none
|
||||||
|
pass
|
||||||
|
elif t == 15 and i + 3 <= n: # set_drawcolor r g b
|
||||||
|
st['col'] = tuple(_hud2d_f(v) for v in w[i:i + 3])
|
||||||
|
i += 3
|
||||||
|
elif t == 16 and i + 6 <= n: # set_matrix (2x3 affine)
|
||||||
|
st['m'] = tuple(_hud2d_f(v) for v in w[i:i + 6])
|
||||||
|
i += 6
|
||||||
|
elif t == 17 and i + 7 <= n: # concat_matrix + post flag
|
||||||
|
a = tuple(_hud2d_f(v) for v in w[i:i + 6])
|
||||||
|
post = w[i + 6] != 0
|
||||||
|
i += 7
|
||||||
|
m = st['m']
|
||||||
|
x, y = (m, a) if post else (a, m)
|
||||||
|
st['m'] = (x[0] * y[0] + x[1] * y[2],
|
||||||
|
x[0] * y[1] + x[1] * y[3],
|
||||||
|
x[2] * y[0] + x[3] * y[2],
|
||||||
|
x[2] * y[1] + x[3] * y[3],
|
||||||
|
x[4] * y[0] + x[5] * y[2] + y[4],
|
||||||
|
x[4] * y[1] + x[5] * y[3] + y[5])
|
||||||
|
elif t == 18: # push_state
|
||||||
|
stack.append(dict(st))
|
||||||
|
elif t == 19: # pop_state
|
||||||
|
if stack:
|
||||||
|
st.update(stack.pop())
|
||||||
|
elif t == 20 and i + 1 <= n: # call_displaylist
|
||||||
|
run(w[i], depth + 1)
|
||||||
|
i += 1
|
||||||
|
elif t == 21 and i + 1 <= n: # set_linewidth
|
||||||
|
st['w'] = _hud2d_f(w[i])
|
||||||
|
i += 1
|
||||||
|
elif t == 22 and i + 1 <= n: # set_alpha (HUD is opaque; skip)
|
||||||
|
i += 1
|
||||||
|
# unknown word: skip (keeps a malformed chunk from cascading)
|
||||||
|
|
||||||
|
run(root, 0)
|
||||||
|
emit(mode[0])
|
||||||
|
return prims
|
||||||
|
|
||||||
|
|
||||||
|
def hud2d_root(board, view):
|
||||||
|
"""The view node's bound 2D display list (dpl2d_SetViewDisplayList):
|
||||||
|
the word appended after the 96B view struct (stored-body offset 96).
|
||||||
|
Short view re-flushes (fog animation) truncate the stored body, so the
|
||||||
|
binding from the last full-length flush is cached per view. 0 = none."""
|
||||||
|
if view is None:
|
||||||
|
return 0
|
||||||
|
cache = getattr(board, '_hud2d_bind', None)
|
||||||
|
if cache is None:
|
||||||
|
cache = board._hud2d_bind = {}
|
||||||
|
vb = board.nodes.get(view, {}).get('body') or b''
|
||||||
|
if len(vb) >= 100:
|
||||||
|
cache[view] = struct.unpack_from('<I', vb, 96)[0]
|
||||||
|
root = cache.get(view, 0)
|
||||||
|
return root if root in getattr(board, 'dl2d', {}) else 0
|
||||||
|
|
||||||
|
|
||||||
class SceneCache:
|
class SceneCache:
|
||||||
"""Static structure distilled from board state (rebuilt when node count changes)."""
|
"""Static structure distilled from board state (rebuilt when node count changes)."""
|
||||||
|
|
||||||
@@ -470,10 +602,24 @@ class Renderer:
|
|||||||
m[:, 3] = (0, 0, 0, 1)
|
m[:, 3] = (0, 0, 0, 1)
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def chain_matrix(self, board, chain):
|
def chain_matrix(self, board, chain, fix_degenerate=False):
|
||||||
m = np.eye(4)
|
m = np.eye(4)
|
||||||
for h in chain: # leaf first: M_leaf @ ... @ M_root
|
for h in chain: # leaf first: M_leaf @ ... @ M_root
|
||||||
m = m @ self.dcs_matrix(board, h)
|
dm = self.dcs_matrix(board, h)
|
||||||
|
if fix_degenerate:
|
||||||
|
# Some pass-through DCS (e.g. 0xa2c in the BTL4 cockpit rig)
|
||||||
|
# flush a rank-2 rotation whose X and Y rows both collapse to
|
||||||
|
# +-Y (shifted body -> wrong read window). Composed into the
|
||||||
|
# camera chain this drops the whole rig to rank 2 (only the
|
||||||
|
# look-row survives) and, once the head DCS articulates, mixes
|
||||||
|
# the head glance into the wrong axis (left/right hat -> pitch).
|
||||||
|
# A degenerate rotation carries no real orientation -- treat it
|
||||||
|
# as identity. Preserves the neutral look exactly; lets the
|
||||||
|
# glance compose as a proper yaw.
|
||||||
|
R = dm[:3, :3]
|
||||||
|
if np.linalg.matrix_rank(R, tol=1e-3) < 3:
|
||||||
|
dm = dm.copy(); dm[:3, :3] = np.eye(3)
|
||||||
|
m = m @ dm
|
||||||
return m
|
return m
|
||||||
|
|
||||||
# MUNGA vehicles fly nose-along-+Z: straight-fast-flight samples of the
|
# MUNGA vehicles fly nose-along-+Z: straight-fast-flight samples of the
|
||||||
@@ -490,7 +636,7 @@ class Renderer:
|
|||||||
m = self._chase_cam(board)
|
m = self._chase_cam(board)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return m
|
return m
|
||||||
m = self.chain_matrix(board, self.cache.cam_chain)
|
m = self.chain_matrix(board, self.cache.cam_chain, fix_degenerate=True)
|
||||||
if getattr(board, 'munga', False):
|
if getattr(board, 'munga', False):
|
||||||
m = self._CAMFIX @ m
|
m = self._CAMFIX @ m
|
||||||
return m
|
return m
|
||||||
@@ -761,6 +907,9 @@ class Renderer:
|
|||||||
arr = (np.power(arr / 255.0, 1.0 / self.gamma) * 255).astype(np.uint8)
|
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)))
|
surf = pg.surfarray.make_surface(np.transpose(arr, (1, 0, 2)))
|
||||||
self.screen.blit(surf, (0, 0))
|
self.screen.blit(surf, (0, 0))
|
||||||
|
if self._draw_hud2d(board, vp):
|
||||||
|
# refresh from the screen so last_frame includes the HUD overlay
|
||||||
|
arr = np.transpose(pg.surfarray.array3d(self.screen), (1, 0, 2))
|
||||||
pg.display.flip()
|
pg.display.flip()
|
||||||
self.last_frame = arr # for vr_readpixels replies
|
self.last_frame = arr # for vr_readpixels replies
|
||||||
# live camera telemetry in the title bar (tracker diagnostics)
|
# live camera telemetry in the title bar (tracker diagnostics)
|
||||||
@@ -772,6 +921,46 @@ class Renderer:
|
|||||||
self.skip = max(1, min(10, int(self._last_ms / 50)))
|
self.skip = max(1, min(10, int(self._last_ms / 50)))
|
||||||
self.clock.tick(self.fps) # pace draw_scene acks (RETRACE-aware)
|
self.clock.tick(self.fps) # pace draw_scene acks (RETRACE-aware)
|
||||||
|
|
||||||
|
def _draw_hud2d(self, board, vp):
|
||||||
|
"""Composite the game's dpl2d HUD (reticle/ladders/carets) over the
|
||||||
|
frame -- the layer the real board mixed onto its video output.
|
||||||
|
Returns True if anything was drawn."""
|
||||||
|
root = hud2d_root(board, self.cache.view)
|
||||||
|
if not root:
|
||||||
|
return False
|
||||||
|
pg = self.pygame
|
||||||
|
W, H = self.w, self.h
|
||||||
|
x0, y0, x1, y1 = vp['x0'], vp['y0'], vp['x1'], vp['y1']
|
||||||
|
sx, sy = W / (x1 - x0), H / (y1 - y0)
|
||||||
|
g = 1.0 / self.gamma
|
||||||
|
|
||||||
|
def P(pt):
|
||||||
|
return (int((pt[0] - x0) * sx + 0.5),
|
||||||
|
int((y1 - pt[1]) * sy + 0.5)) # y up
|
||||||
|
|
||||||
|
try:
|
||||||
|
prims = hud2d_prims(board.dl2d, root)
|
||||||
|
except Exception as e:
|
||||||
|
if not getattr(self, '_hud2d_err', None):
|
||||||
|
self._hud2d_err = True
|
||||||
|
print(f'hud2d: decode failed: {e}')
|
||||||
|
return False
|
||||||
|
for kind, col, wd, pts in prims:
|
||||||
|
c8 = tuple(int(255 * max(0.0, min(1.0, v)) ** g + 0.5) for v in col)
|
||||||
|
wd = max(1, int(wd + 0.5))
|
||||||
|
sp = [P(p) for p in pts]
|
||||||
|
if kind == 'strip' and len(sp) >= 2:
|
||||||
|
pg.draw.lines(self.screen, c8, False, sp, wd)
|
||||||
|
elif kind == 'segs':
|
||||||
|
for i in range(0, len(sp) - 1, 2):
|
||||||
|
pg.draw.line(self.screen, c8, sp[i], sp[i + 1], wd)
|
||||||
|
elif kind == 'poly' and len(sp) >= 3:
|
||||||
|
pg.draw.polygon(self.screen, c8, sp)
|
||||||
|
elif kind == 'points':
|
||||||
|
for p in sp:
|
||||||
|
self.screen.fill(c8, (p, (wd, wd))) # clips at edges
|
||||||
|
return bool(prims)
|
||||||
|
|
||||||
def _particles(self, board, eye, V, vp, img, zbuf, W, H, dt):
|
def _particles(self, board, eye, V, vp, img, zbuf, W, H, dt):
|
||||||
"""Ambient SPECIALFX emitters (installed 0x1c defs): the shipped board
|
"""Ambient SPECIALFX emitters (installed 0x1c defs): the shipped board
|
||||||
stepped these autonomously. Approximation: per-def particle pools spawned
|
stepped these autonomously. Approximation: per-def particle pools spawned
|
||||||
|
|||||||
@@ -189,6 +189,25 @@ void main() { // Division DAC gamma, once over the final image
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
HUD_VS = """
|
||||||
|
#version 330
|
||||||
|
uniform vec4 u_rect; // view rect x0 y0 x1 y1 (dpl2d coordinate space)
|
||||||
|
in vec2 in_pos;
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2((in_pos.x - u_rect.x) / (u_rect.z - u_rect.x),
|
||||||
|
(in_pos.y - u_rect.y) / (u_rect.w - u_rect.y)) * 2.0 - 1.0;
|
||||||
|
gl_Position = vec4(p, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
HUD_FS = """
|
||||||
|
#version 330
|
||||||
|
uniform vec3 u_col;
|
||||||
|
out vec4 f_color;
|
||||||
|
void main() { f_color = vec4(u_col, 1.0); }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GLRenderer(vrview.Renderer):
|
class GLRenderer(vrview.Renderer):
|
||||||
"""Drop-in replacement for vrview.Renderer with a moderngl draw path."""
|
"""Drop-in replacement for vrview.Renderer with a moderngl draw path."""
|
||||||
|
|
||||||
@@ -469,6 +488,7 @@ class GLRenderer(vrview.Renderer):
|
|||||||
self._draw_geom(board, c, gh, mesh, M, Mw)
|
self._draw_geom(board, c, gh, mesh, M, Mw)
|
||||||
|
|
||||||
self._draw_particles(board, V, vp, self.skip / 60.0)
|
self._draw_particles(board, V, vp, self.skip / 60.0)
|
||||||
|
self._draw_hud2d_gl(board, vp)
|
||||||
|
|
||||||
# present: FBO -> window with the DAC gamma
|
# present: FBO -> window with the DAC gamma
|
||||||
if self.screen is not None:
|
if self.screen is not None:
|
||||||
@@ -491,6 +511,53 @@ class GLRenderer(vrview.Renderer):
|
|||||||
if not self.headless:
|
if not self.headless:
|
||||||
self.clock.tick(self.fps) # pace draw_scene acks (RETRACE-aware)
|
self.clock.tick(self.fps) # pace draw_scene acks (RETRACE-aware)
|
||||||
|
|
||||||
|
def _draw_hud2d_gl(self, board, vp):
|
||||||
|
"""Composite the game's dpl2d HUD into the FBO (pre-present, so the
|
||||||
|
DAC gamma pass covers it -- matches the software path's colors)."""
|
||||||
|
root = vrview.hud2d_root(board, self.cache.view)
|
||||||
|
if not root:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
prims = vrview.hud2d_prims(board.dl2d, root)
|
||||||
|
except Exception as e:
|
||||||
|
if not getattr(self, '_hud2d_err', None):
|
||||||
|
self._hud2d_err = True
|
||||||
|
print(f'hud2d: decode failed: {e}')
|
||||||
|
return
|
||||||
|
if not prims:
|
||||||
|
return
|
||||||
|
ctx, mgl = self.ctx, self.moderngl
|
||||||
|
if getattr(self, '_hud_prog', None) is None:
|
||||||
|
self._hud_prog = ctx.program(vertex_shader=HUD_VS,
|
||||||
|
fragment_shader=HUD_FS)
|
||||||
|
self._hud_cap = 8192
|
||||||
|
self._hud_buf = ctx.buffer(reserve=self._hud_cap, dynamic=True)
|
||||||
|
self._hud_vao = ctx.vertex_array(
|
||||||
|
self._hud_prog, [(self._hud_buf, '2f', 'in_pos')])
|
||||||
|
ctx.disable(mgl.DEPTH_TEST)
|
||||||
|
self._set(self._hud_prog, 'u_rect',
|
||||||
|
(float(vp['x0']), float(vp['y0']),
|
||||||
|
float(vp['x1']), float(vp['y1'])))
|
||||||
|
MODES = {'strip': mgl.LINE_STRIP, 'segs': mgl.LINES,
|
||||||
|
'poly': mgl.TRIANGLE_FAN, 'points': mgl.POINTS}
|
||||||
|
for kind, col, wd, pts in prims:
|
||||||
|
if len(pts) < (3 if kind == 'poly' else 1):
|
||||||
|
continue
|
||||||
|
data = np.asarray(pts, '<f4')
|
||||||
|
if data.nbytes > self._hud_cap:
|
||||||
|
self._hud_vao.release(); self._hud_buf.release()
|
||||||
|
self._hud_cap = data.nbytes * 2
|
||||||
|
self._hud_buf = ctx.buffer(reserve=self._hud_cap, dynamic=True)
|
||||||
|
self._hud_vao = ctx.vertex_array(
|
||||||
|
self._hud_prog, [(self._hud_buf, '2f', 'in_pos')])
|
||||||
|
self._hud_buf.orphan(self._hud_cap)
|
||||||
|
self._hud_buf.write(data.tobytes())
|
||||||
|
self._set(self._hud_prog, 'u_col', tuple(float(v) for v in col))
|
||||||
|
ctx.line_width = max(1.0, float(wd))
|
||||||
|
ctx.point_size = max(1.0, float(wd))
|
||||||
|
self._hud_vao.render(MODES[kind], vertices=len(pts))
|
||||||
|
ctx.line_width = 1.0
|
||||||
|
|
||||||
def _draw_geom(self, board, c, gh, mesh, M, Mw):
|
def _draw_geom(self, board, c, gh, mesh, M, Mw):
|
||||||
"""Set per-geometry uniforms and render one mesh (both passes)."""
|
"""Set per-geometry uniforms and render one mesh (both passes)."""
|
||||||
prog = self._mesh_prog
|
prog = self._mesh_prog
|
||||||
|
|||||||
@@ -146,3 +146,39 @@ device side, decisions taken. Append, date-stamp.)*
|
|||||||
plausible wire fog). Full material parse landed. Next in-process steps:
|
plausible wire fog). Full material parse landed. Next in-process steps:
|
||||||
light node decode, type-12 params, restoration-toolkit reference render of
|
light node decode, type-12 params, restoration-toolkit reference render of
|
||||||
arena1 content.
|
arena1 content.
|
||||||
|
|
||||||
|
- **2026-07-06 (Claude): actions 0x29/0x2b DECODED — the 2D HUD layer
|
||||||
|
(dpl2d display lists).** The game draws its out-the-window HUD (targeting
|
||||||
|
reticle, twist/pitch tick ladders, carets, weapon lamps, torso-twist dial)
|
||||||
|
through DPL's 2D overlay API (`dpl2d_*`, see CODE/RP/MUNGA_L4/libDPL/dpl/
|
||||||
|
DPL_2D.H + vpx/DPL2DTAG.H; game-side builder = L4VIDRND.CPP
|
||||||
|
ReticleRenderable etc.), NOT as scene-graph instances.
|
||||||
|
- `0x29` = dpl2d_NewDisplayList: `[remote handle][0]`, fire-and-forget.
|
||||||
|
- `0x2b` = dpl2d_FlushDisplayList: the host streams the `dpl2d_DISPLAY`
|
||||||
|
chunk structs verbatim (`remote, next, tail, size, open, data[30]`,
|
||||||
|
WORDS_PER_DISPLAY_CHUNK=30 → the fixed 140B payload). First chunk:
|
||||||
|
`remote`=handle, `tail`=total chunk count; continuations have remote=0.
|
||||||
|
Data words CONCATENATE across chunks (tag args split freely at chunk
|
||||||
|
boundaries); a flush REPLACES the list.
|
||||||
|
- Data = tag stream per DPL2DTAG.H (0=open_polyline … 22=set_alpha).
|
||||||
|
Arg words are float32 except call_displaylist (handle) and the
|
||||||
|
circle/clip mode/filled flags. `open_lines` points come in PAIRS
|
||||||
|
(independent segments); polygon = filled convex; matrix = 6-float 2D
|
||||||
|
affine, state push/pop covers matrix+color+alpha+linewidth.
|
||||||
|
- View binding (dpl2d_SetViewDisplayList) = one word appended after the
|
||||||
|
96B view struct in full-length view flushes (wire-payload word 25).
|
||||||
|
Root list in BT = a wrapper that calls the real HUD list; two sub-lists
|
||||||
|
re-flush ~10 Hz (the live carets).
|
||||||
|
- Coordinates are view-rect space (x0..x1/y0..y1 from the view struct,
|
||||||
|
y up); colors 0..1, composited over the frame (board video-out mix).
|
||||||
|
- Implemented in vrboard.py (`dl2d` store) + vrview.py/vrview_gl.py
|
||||||
|
(overlay pass, both backends) — validated on a live BT mission capture:
|
||||||
|
reticle centered, heading tape + yellow caret, pitch ladder, weapon
|
||||||
|
lamps, twist dial all render. Open question 0x29/0x2b in the protocol
|
||||||
|
spec §13 “fire-and-forget unknowns” can be closed.
|
||||||
|
- Related negative result for subsystem debugging: in a 25-min live
|
||||||
|
weapons-test capture the game emitted ZERO `0x23` fire/pick actions
|
||||||
|
while all four trigger buttons were pressed repeatedly (RIO tap
|
||||||
|
confirms delivery) — the fire path never reached the renderer; weapon
|
||||||
|
triggers are dying game-side (control-mode/mapping or weapon power
|
||||||
|
gating), not render-side.
|
||||||
|
|||||||
@@ -313,3 +313,38 @@ Every board packet is retransmitted until the game ACKs `FC` (within the
|
|||||||
TXMAXIDLE window; ~33ms with the patched EXE). Byte pacing at the 9600-baud
|
TXMAXIDLE window; ~33ms with the patched EXE). Byte pacing at the 9600-baud
|
||||||
wire rate (~1ms/byte) matters: vRIO blasting bytes back-to-back caused NAK/
|
wire rate (~1ms/byte) matters: vRIO blasting bytes back-to-back caused NAK/
|
||||||
restart churn during init until the user added pacing.
|
restart churn during init until the user added pacing.
|
||||||
|
|
||||||
|
## RIO button unit map (L4CTRL.HPP enum; wire unit byte == buttonGroup index)
|
||||||
|
|
||||||
|
Press = `88 <unit> <ck>`, release = `89 <unit> <ck>` (ck = sum&0x7F). The game
|
||||||
|
uses the unit byte DIRECTLY as its button index (L4CTRL.CPP RIO::ButtonPressedEvent).
|
||||||
|
|
||||||
|
| unit | button |
|
||||||
|
|-----------|--------------------------------------------------------|
|
||||||
|
| 0x00-0x07 | AuxLowerRight 8..1 (panel) |
|
||||||
|
| 0x08-0x0F | AuxLowerLeft 8..1 (panel) |
|
||||||
|
| 0x10-0x15 | Secondary 1..6 |
|
||||||
|
| 0x18-0x1D | Secondary 7..12 |
|
||||||
|
| 0x20-0x27 | AuxUpperCenter 8..1 |
|
||||||
|
| 0x28-0x2F | AuxUpperLeft 8..1 |
|
||||||
|
| 0x30-0x37 | AuxUpperRight 8..1 |
|
||||||
|
| 0x39-0x3B | IcomHeadPluggedIn / IcomSensor / IcomMikePluggedIn |
|
||||||
|
| 0x3C | Door |
|
||||||
|
| 0x3D | Panic |
|
||||||
|
| 0x3F | Throttle1 |
|
||||||
|
| 0x40 | **JoystickTrigger** (= FIRE) |
|
||||||
|
| 0x41/0x42 | JoystickHatDown / HatUp |
|
||||||
|
| 0x43/0x44 | **JoystickHatRight / HatLeft** (= TORSO TWIST R/L) |
|
||||||
|
| 0x45 | JoystickPinky (= torso/look DOWN per TORSO.CTL) |
|
||||||
|
| 0x46 | JoystickThumbLow (= TORSO CENTER) |
|
||||||
|
| 0x47 | JoystickThumbHigh (= torso/look UP) |
|
||||||
|
|
||||||
|
CORRECTION (user, operated the real pods): in the PRODUCTION cockpit torso
|
||||||
|
twist is AXIS-driven; TORSO.CTL's button mappings (TorsoLeft=HatLeft etc.,
|
||||||
|
limits per TORSO.SUB: +-80deg horiz @20deg/s, +10/-30 vert @40deg/s) were the
|
||||||
|
DEV-rig fallback for setups without the full cockpit. On the real pod the HAT
|
||||||
|
gives momentary GLANCE views (left/right/rear). Keypad keys are a SEPARATE
|
||||||
|
event type (8A KeyPressed: unit,key). Verified live 2026-07-06: vRIO hat
|
||||||
|
glances DO work in-game (units 0x41-0x44 arriving), so the 88/89 button path
|
||||||
|
is validated end-to-end -- weapons-not-firing is NOT a unit-code problem;
|
||||||
|
suspect weapon-group arming/mission state (see MECHWEAP.CTL) instead.
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Conf = "$PSScriptRoot\gauge_arena_sound.conf",
|
[string]$Conf = "$PSScriptRoot\gauge_arena_sound.conf",
|
||||||
[string]$Work = "$env:LOCALAPPDATA\Temp\vwe-pod",
|
[string]$Work = "$env:LOCALAPPDATA\Temp\vwe-pod",
|
||||||
|
[string]$BridgePos = '2020,20', # Division head slot (explode layout)
|
||||||
[switch]$NoBridge,
|
[switch]$NoBridge,
|
||||||
[switch]$NoSound
|
[switch]$NoSound,
|
||||||
|
[switch]$ShowNative # also show the native Division window
|
||||||
|
# (wire-decode diagnostic; black clear)
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
New-Item -ItemType Directory -Force $Work | Out-Null
|
New-Item -ItemType Directory -Force $Work | Out-Null
|
||||||
@@ -40,8 +43,15 @@ $env:VPX_RESPOND = '1'
|
|||||||
$env:VPX_RENDER = '1'
|
$env:VPX_RENDER = '1'
|
||||||
$env:VPX_EXPLODE = '1' # pentapus: 7 cockpit displays
|
$env:VPX_EXPLODE = '1' # pentapus: 7 cockpit displays
|
||||||
$env:VPX_DUMPDIR = $Work
|
$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"
|
$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) {
|
if ($NoSound) {
|
||||||
Remove-Item Env:VWE_AWE32, Env:VWE_AWE_ROM -ErrorAction SilentlyContinue
|
Remove-Item Env:VWE_AWE32, Env:VWE_AWE_ROM -ErrorAction SilentlyContinue
|
||||||
} else {
|
} 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 $NoSound) { Write-Host "sound ON: boot adds ~4 min for the SoundFont upload" }
|
||||||
|
|
||||||
if (-not $NoBridge) {
|
if (-not $NoBridge) {
|
||||||
# live_bridge waits for the fifodump itself; start it right away
|
# park the bridge window on the Division head slot (SDL honors this at
|
||||||
$b = Start-Process py -ArgumentList '-3.13', "$PSScriptRoot\live_bridge.py", "$Work\live.fifodump" `
|
# 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" `
|
-RedirectStandardError "$Work\bridge_err.txt" `
|
||||||
-RedirectStandardOutput "$Work\bridge_out.txt" -PassThru
|
-RedirectStandardOutput "$Work\bridge_out.txt" -PassThru
|
||||||
$b.Id | Set-Content "$Work\bridge_pid.txt"
|
$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>
|
py live_bridge.py <live.fifodump>
|
||||||
"""
|
"""
|
||||||
import os, sys, struct, time
|
import os, sys, struct, time, math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from _backend import pick_renderer
|
from _backend import pick_renderer
|
||||||
from vrboard import VirtualBoard, Msg, A
|
from vrboard import VirtualBoard, Msg, A
|
||||||
Renderer, backend = pick_renderer()
|
Renderer, backend = pick_renderer()
|
||||||
|
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
|
catchup = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
board = VirtualBoard()
|
board = VirtualBoard()
|
||||||
board.munga = False
|
board.munga = False
|
||||||
r = Renderer(w=832, h=512,
|
r = Renderer(w=832, h=512,
|
||||||
title=f"dpl3-revive renderer (Dave) -- LIVE from our pod [{backend}]")
|
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'))
|
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,
|
# eye-height TRIM on top of the camera position (UP/DOWN arrows = +-5,
|
||||||
# LEFT/RIGHT = +-1, live (the render window must be focused). 35 was tuned
|
# LEFT/RIGHT = +-1, live; render window must be focused). The chain camera
|
||||||
# before the ground rendered and is way too high against real terrain.
|
# carries the true cockpit eye so the default trim is 0; the vehicle-root
|
||||||
UPOFF = [float(os.environ.get('FP_UPOFF', '12'))]
|
# 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):
|
def fp_cam(board, cache):
|
||||||
"""First-person cockpit camera from the player vehicle's 0x1f pose: eye at
|
"""First-person cockpit camera, best source first:
|
||||||
the mech (+up), looking along its nose (+Z row), y-up world. Beats Dave's
|
1. HEAD-LOOK (default): the munga cam DCS chain's translation row = the
|
||||||
11-DCS cam chain (which renders sky through our wire's convention)."""
|
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
|
anim = board.anim_abs
|
||||||
chain = cache.cam_chain
|
chain = cache.cam_chain
|
||||||
h = None
|
h = None
|
||||||
@@ -41,16 +88,47 @@ def fp_cam(board, cache):
|
|||||||
f = anim[h]
|
f = anim[h]
|
||||||
R = np.array(f[:9]).reshape(3, 3)
|
R = np.array(f[:9]).reshape(3, 3)
|
||||||
t = np.array(f[9:12])
|
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])
|
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
|
back = -fwd
|
||||||
right = np.cross(worldup, back)
|
right = np.cross(worldup, back)
|
||||||
rn = np.linalg.norm(right)
|
rn = np.linalg.norm(right)
|
||||||
@@ -86,20 +164,86 @@ def render(board):
|
|||||||
r.draw(board)
|
r.draw(board)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
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} ...")
|
# wire source: a fifodump file to tail, or "tcp:<port>" = the device's
|
||||||
while not os.path.exists(path):
|
# VPX_FIFOSOCK live tee (same VPXM records, no file-poll quantum; recv blocks
|
||||||
time.sleep(0.2)
|
# until data arrives so wire-to-render latency is the socket itself).
|
||||||
f = open(path, 'rb')
|
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")
|
print("tailing live wire -> Dave's renderer; drive the pod")
|
||||||
|
|
||||||
pending = b''
|
pending = b''
|
||||||
frames = 0
|
frames = 0
|
||||||
last_report = time.time()
|
last_report = time.time()
|
||||||
while True:
|
while True:
|
||||||
chunk = f.read(1 << 20)
|
chunk = read_chunk()
|
||||||
if chunk:
|
if chunk:
|
||||||
pending += chunk
|
pending += chunk
|
||||||
off = 0
|
off = 0
|
||||||
@@ -120,6 +264,9 @@ while True:
|
|||||||
board.handle(Msg(False, 0xff, action, body[4:]))
|
board.handle(Msg(False, 0xff, action, body[4:]))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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 action == 9: # draw_scene -> present a frame
|
||||||
render(board)
|
render(board)
|
||||||
frames += 1
|
frames += 1
|
||||||
@@ -128,9 +275,10 @@ while True:
|
|||||||
try: r.pump()
|
try: r.pump()
|
||||||
except KeyboardInterrupt: break
|
except KeyboardInterrupt: break
|
||||||
except Exception: pass
|
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:
|
if time.time() - last_report > 4:
|
||||||
last_report = time.time()
|
last_report = time.time()
|
||||||
print(f"frames={frames} nodes={len(board.nodes)} "
|
print(f"frames={frames} nodes={len(board.nodes)} "
|
||||||
f"uploads={len(board.uploads)} tex={len(board.tex)} "
|
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)
|
||||||
|
|||||||
+354
-17
@@ -30,6 +30,16 @@
|
|||||||
* pure host->board writes; the device just absorbs them.
|
* pure host->board writes; the device just absorbs them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <winsock2.h> /* VPX_FIFOSOCK live tee; must precede windows.h */
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "dosbox.h"
|
#include "dosbox.h"
|
||||||
#include "inout.h"
|
#include "inout.h"
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
@@ -51,6 +61,21 @@ static int vpx_max_handshakes = 3;
|
|||||||
static unsigned char in_fifo[64];
|
static unsigned char in_fifo[64];
|
||||||
static int in_len = 0, in_pos = 0;
|
static int in_len = 0, in_pos = 0;
|
||||||
|
|
||||||
|
/* ---- reticle pick (weapons-fire target gate) ---------------------------- *
|
||||||
|
* The game runs a continuous reticle intersection test: it arms it once with
|
||||||
|
* set_sect_pixel (action 38) at screen (0.5,0.5), then every frame reads the
|
||||||
|
* result -- hit instance + DCS + 3D intersection point -- which the board is
|
||||||
|
* expected to return piggybacked on the draw_scene (action 9) reply. The host
|
||||||
|
* (dpl_RapidSectPixel @0x4903c8) copies that into globals; targetReticle.
|
||||||
|
* targetEntity = the hit DCS's app-specific pointer. That entity IS the fire
|
||||||
|
* gate the weapon state machine checks (mech+0x388): no hit => targetEntity
|
||||||
|
* NULL => every weapon misfires ("boo-beep"). A real i860 answered this each
|
||||||
|
* frame; our HLE never did. We now carry a real scene hit in the frame ack. */
|
||||||
|
static bool sect_armed = false;
|
||||||
|
/* Real reticle raycast (Moller-Trumbore against the live scene, center ray). */
|
||||||
|
static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out,
|
||||||
|
unsigned *geom_out, float xyz_out[3]);
|
||||||
|
|
||||||
/* ---- boot state machine ------------------------------------------------- */
|
/* ---- boot state machine ------------------------------------------------- */
|
||||||
enum Phase { P_INIT, P_HANDSHAKE, P_POSTBOOT };
|
enum Phase { P_INIT, P_HANDSHAKE, P_POSTBOOT };
|
||||||
static Phase phase = P_INIT;
|
static Phase phase = P_INIT;
|
||||||
@@ -148,8 +173,107 @@ static bool vpx_render = false; /* Phase 3b live GL backend */
|
|||||||
static void scene_burst(const unsigned char *p, size_t n); /* fwd (3b) */
|
static void scene_burst(const unsigned char *p, size_t n); /* fwd (3b) */
|
||||||
static void scene_reset(void); /* fwd (3b) */
|
static void scene_reset(void); /* fwd (3b) */
|
||||||
|
|
||||||
|
/* ---- live socket tee (VPX_FIFOSOCK=<port>) ------------------------------ *
|
||||||
|
* Streams the same VPXM records to one localhost TCP client (the dpl3-revive
|
||||||
|
* bridge) with TCP_NODELAY -- removes the fifodump file-poll quantum from the
|
||||||
|
* wire-to-photon path. All calls are non-blocking on the emulation thread: a
|
||||||
|
* stalled client gets a bounded pending buffer, then is dropped (the bridge
|
||||||
|
* reconnects; vehicle poses are absolute so a brief gap self-heals). */
|
||||||
|
#ifdef _WIN32
|
||||||
|
static SOCKET fifo_lsock = INVALID_SOCKET; /* listener */
|
||||||
|
static SOCKET fifo_csock = INVALID_SOCKET; /* single client */
|
||||||
|
static unsigned char *fifo_pend = NULL; /* client-stalled unsent tail */
|
||||||
|
static size_t fifo_pend_len = 0, fifo_pend_cap = 0;
|
||||||
|
|
||||||
|
static bool fifo_sock_active(void) { return fifo_lsock != INVALID_SOCKET; }
|
||||||
|
|
||||||
|
static void fifo_sock_init(int port) {
|
||||||
|
WSADATA wd;
|
||||||
|
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0) return;
|
||||||
|
fifo_lsock = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fifo_lsock == INVALID_SOCKET) return;
|
||||||
|
sockaddr_in a; memset(&a, 0, sizeof a);
|
||||||
|
a.sin_family = AF_INET;
|
||||||
|
a.sin_port = htons((u_short)port);
|
||||||
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
u_long nb = 1;
|
||||||
|
if (bind(fifo_lsock, (sockaddr *)&a, sizeof a) != 0 ||
|
||||||
|
listen(fifo_lsock, 1) != 0 ||
|
||||||
|
ioctlsocket(fifo_lsock, FIONBIO, &nb) != 0) {
|
||||||
|
closesocket(fifo_lsock); fifo_lsock = INVALID_SOCKET;
|
||||||
|
LOG_MSG("VPXLOG: fifosock: cannot listen on 127.0.0.1:%d", port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG_MSG("VPXLOG: fifosock listening on 127.0.0.1:%d", port);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fifo_sock_drop(void) {
|
||||||
|
if (fifo_csock != INVALID_SOCKET) closesocket(fifo_csock);
|
||||||
|
fifo_csock = INVALID_SOCKET;
|
||||||
|
fifo_pend_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fifo_sock_accept(void) {
|
||||||
|
if (fifo_lsock == INVALID_SOCKET) return;
|
||||||
|
SOCKET c = accept(fifo_lsock, NULL, NULL);
|
||||||
|
if (c == INVALID_SOCKET) return; /* WOULDBLOCK: nobody waiting */
|
||||||
|
fifo_sock_drop();
|
||||||
|
u_long nb = 1; ioctlsocket(c, FIONBIO, &nb);
|
||||||
|
int v = 1;
|
||||||
|
setsockopt(c, IPPROTO_TCP, TCP_NODELAY, (const char *)&v, sizeof v);
|
||||||
|
v = 1 << 20;
|
||||||
|
setsockopt(c, SOL_SOCKET, SO_SNDBUF, (const char *)&v, sizeof v);
|
||||||
|
fifo_csock = c;
|
||||||
|
LOG_MSG("VPXLOG: fifosock client connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fifo_pend_stash(const unsigned char *p, size_t n) {
|
||||||
|
if (fifo_pend_len + n > (8u << 20)) { fifo_sock_drop(); return; }
|
||||||
|
if (fifo_pend_len + n > fifo_pend_cap) {
|
||||||
|
size_t ncap = fifo_pend_cap ? fifo_pend_cap : 65536;
|
||||||
|
while (ncap < fifo_pend_len + n) ncap *= 2;
|
||||||
|
unsigned char *np = (unsigned char *)realloc(fifo_pend, ncap);
|
||||||
|
if (np == NULL) { fifo_sock_drop(); return; }
|
||||||
|
fifo_pend = np; fifo_pend_cap = ncap;
|
||||||
|
}
|
||||||
|
memcpy(fifo_pend + fifo_pend_len, p, n);
|
||||||
|
fifo_pend_len += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fifo_sock_write(const unsigned char *p, size_t n) {
|
||||||
|
if (fifo_csock == INVALID_SOCKET) return;
|
||||||
|
/* drain any stalled tail first (framing must stay contiguous) */
|
||||||
|
while (fifo_pend_len) {
|
||||||
|
int r = send(fifo_csock, (const char *)fifo_pend,
|
||||||
|
(int)(fifo_pend_len > 65536 ? 65536 : fifo_pend_len), 0);
|
||||||
|
if (r > 0) {
|
||||||
|
memmove(fifo_pend, fifo_pend + r, fifo_pend_len - (size_t)r);
|
||||||
|
fifo_pend_len -= (size_t)r;
|
||||||
|
} else if (r == SOCKET_ERROR &&
|
||||||
|
WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||||
|
fifo_pend_stash(p, n); return;
|
||||||
|
} else { fifo_sock_drop(); return; }
|
||||||
|
}
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < n) {
|
||||||
|
int r = send(fifo_csock, (const char *)(p + off),
|
||||||
|
(int)((n - off) > 65536 ? 65536 : (n - off)), 0);
|
||||||
|
if (r > 0) off += (size_t)r;
|
||||||
|
else if (r == SOCKET_ERROR &&
|
||||||
|
WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||||
|
fifo_pend_stash(p + off, n - off); return;
|
||||||
|
} else { fifo_sock_drop(); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool fifo_sock_active(void) { return false; }
|
||||||
|
static void fifo_sock_init(int) {}
|
||||||
|
static void fifo_sock_accept(void) {}
|
||||||
|
static void fifo_sock_write(const unsigned char *, size_t) {}
|
||||||
|
#endif
|
||||||
|
|
||||||
static void fifo_buf_push(unsigned char v) {
|
static void fifo_buf_push(unsigned char v) {
|
||||||
if (fifo_dump_fp == NULL && !vpx_render) return;
|
if (fifo_dump_fp == NULL && !vpx_render && !fifo_sock_active()) return;
|
||||||
if (fifo_buf_len >= (1u << 20)) return; /* runaway guard */
|
if (fifo_buf_len >= (1u << 20)) return; /* runaway guard */
|
||||||
if (fifo_buf_len == fifo_buf_cap) {
|
if (fifo_buf_len == fifo_buf_cap) {
|
||||||
size_t ncap = fifo_buf_cap ? fifo_buf_cap * 2 : 4096;
|
size_t ncap = fifo_buf_cap ? fifo_buf_cap * 2 : 4096;
|
||||||
@@ -161,14 +285,19 @@ static void fifo_buf_push(unsigned char v) {
|
|||||||
}
|
}
|
||||||
static void fifo_flush_record(void) {
|
static void fifo_flush_record(void) {
|
||||||
if (fifo_buf_len == 0) return;
|
if (fifo_buf_len == 0) return;
|
||||||
|
unsigned char hdr[8] = { 'V','P','X','M',
|
||||||
|
(unsigned char)(fifo_buf_len), (unsigned char)(fifo_buf_len >> 8),
|
||||||
|
(unsigned char)(fifo_buf_len >> 16), (unsigned char)(fifo_buf_len >> 24) };
|
||||||
if (fifo_dump_fp) {
|
if (fifo_dump_fp) {
|
||||||
unsigned char hdr[8] = { 'V','P','X','M',
|
|
||||||
(unsigned char)(fifo_buf_len), (unsigned char)(fifo_buf_len >> 8),
|
|
||||||
(unsigned char)(fifo_buf_len >> 16), (unsigned char)(fifo_buf_len >> 24) };
|
|
||||||
fwrite(hdr, 1, sizeof hdr, fifo_dump_fp);
|
fwrite(hdr, 1, sizeof hdr, fifo_dump_fp);
|
||||||
fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp);
|
fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp);
|
||||||
fflush(fifo_dump_fp);
|
fflush(fifo_dump_fp);
|
||||||
}
|
}
|
||||||
|
if (fifo_sock_active()) {
|
||||||
|
fifo_sock_accept();
|
||||||
|
fifo_sock_write(hdr, sizeof hdr);
|
||||||
|
fifo_sock_write(fifo_buf, fifo_buf_len);
|
||||||
|
}
|
||||||
if (vpx_render) scene_burst(fifo_buf, fifo_buf_len);
|
if (vpx_render) scene_burst(fifo_buf, fifo_buf_len);
|
||||||
fifo_buf_len = 0;
|
fifo_buf_len = 0;
|
||||||
}
|
}
|
||||||
@@ -238,6 +367,42 @@ static void queue_render_ack_node(unsigned char action, unsigned node) {
|
|||||||
}
|
}
|
||||||
static void queue_render_ack(unsigned char action) { queue_render_ack_node(action, 0); }
|
static void queue_render_ack(unsigned char action) { queue_render_ack_node(action, 0); }
|
||||||
|
|
||||||
|
static void wr_u32(unsigned char *p, unsigned v) {
|
||||||
|
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||||||
|
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||||||
|
}
|
||||||
|
/* A draw_scene (action 9) reply that also carries the reticle sect result, so
|
||||||
|
* velocirender_receive stores a live hit (see 0x491d08 / 0x4922e0 in BTL4OPT).
|
||||||
|
* The receive loop copies payload+4 into a host buffer; the store handler then
|
||||||
|
* reads instance @buf+0xc, xi/yi/zi @buf+0x10, DCS @buf+0x1c, gg @buf+0x20,
|
||||||
|
* geom @buf+0x24. In payload terms (buf == payload+4): action@0x00,
|
||||||
|
* instance@0x10, xyz@0x14, DCS@0x20, gg@0x24, geom@0x28. length_word = the
|
||||||
|
* payload byte count (matches queue_render_ack_node's nb=8 convention). */
|
||||||
|
static void queue_sect_frame_reply(unsigned inst, unsigned dcs,
|
||||||
|
unsigned gg, unsigned geom,
|
||||||
|
const float xyz[3]) {
|
||||||
|
fifo_flush_record(); /* a receive means the outstanding burst is done */
|
||||||
|
unsigned char *m = in_fifo;
|
||||||
|
memset(m, 0, sizeof in_fifo);
|
||||||
|
const unsigned nb = 0x2c; /* payload = action(4) + 40 = 44 bytes */
|
||||||
|
wr_u32(m, nb); /* length_word (non-iserver: nb) */
|
||||||
|
unsigned char *pl = m + 4;
|
||||||
|
wr_u32(pl + 0x00, 9); /* action = vr_draw_scene_action */
|
||||||
|
/* pl+0x04/0x08/0x0c: debug echo words (left zero) */
|
||||||
|
wr_u32(pl + 0x10, inst); /* hit instance handle (type 4) */
|
||||||
|
unsigned u;
|
||||||
|
memcpy(&u, &xyz[0], 4); wr_u32(pl + 0x14, u); /* xi */
|
||||||
|
memcpy(&u, &xyz[1], 4); wr_u32(pl + 0x18, u); /* yi */
|
||||||
|
memcpy(&u, &xyz[2], 4); wr_u32(pl + 0x1c, u); /* zi */
|
||||||
|
wr_u32(pl + 0x20, dcs); /* hit DCS handle (type 5) */
|
||||||
|
wr_u32(pl + 0x24, gg); /* hit geogroup handle (type 9) --
|
||||||
|
* the game does GetAppSpecific(gg) on
|
||||||
|
* a hit; sending 0 handed it a null. */
|
||||||
|
wr_u32(pl + 0x28, geom); /* hit geometry handle (type 0xa) */
|
||||||
|
in_len = (int)(4 + nb); /* 48 bytes */
|
||||||
|
in_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- logging (run-length coalesced) ------------------------------------- */
|
/* ---- logging (run-length coalesced) ------------------------------------- */
|
||||||
static unsigned long vpx_seq = 0;
|
static unsigned long vpx_seq = 0;
|
||||||
static io_port_t last_port = 0xFFFF; static unsigned last_val = ~0u;
|
static io_port_t last_port = 0xFFFF; static unsigned last_val = ~0u;
|
||||||
@@ -332,13 +497,32 @@ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
|
|||||||
fprintf(vpx_fp, "# post-boot: sync reply token=0x%X\n",
|
fprintf(vpx_fp, "# post-boot: sync reply token=0x%X\n",
|
||||||
sync_token); fflush(vpx_fp); }
|
sync_token); fflush(vpx_fp); }
|
||||||
} else if (frame_outstanding) {
|
} else if (frame_outstanding) {
|
||||||
/* velocirender_frameack expects a message with
|
/* velocirender_frameack expects a draw_scene (9)
|
||||||
* action == vr_draw_scene_action (9). */
|
* reply; we carry the reticle sect result on it
|
||||||
|
* so weapons acquire a target (else every shot
|
||||||
|
* misfires). The pick runs whenever the game has
|
||||||
|
* armed the reticle (sect_armed); VPX_NO_PICK=1
|
||||||
|
* is the only escape hatch (falls back to the
|
||||||
|
* plain ack -- the pre-pick baseline). */
|
||||||
frame_outstanding = false;
|
frame_outstanding = false;
|
||||||
queue_render_ack(9);
|
static int no_pick = -1;
|
||||||
if (vpx_fp) { flush_run();
|
if (no_pick < 0)
|
||||||
fprintf(vpx_fp, "# post-boot: frame ack (action 9)\n");
|
no_pick = getenv("VPX_NO_PICK") ? 1 : 0;
|
||||||
fflush(vpx_fp); }
|
unsigned pinst = 0, pdcs = 0, pgg = 0, pgeom = 0;
|
||||||
|
float pxyz[3] = { 0, 0, 0 };
|
||||||
|
if (!no_pick && sect_armed &&
|
||||||
|
raycast_pick(&pinst, &pdcs, &pgg, &pgeom, pxyz)) {
|
||||||
|
queue_sect_frame_reply(pinst, pdcs, pgg, pgeom, pxyz);
|
||||||
|
if (vpx_fp) { flush_run();
|
||||||
|
fprintf(vpx_fp, "# post-boot: frame ack "
|
||||||
|
"(9) + sect inst=%08X dcs=%08X\n",
|
||||||
|
pinst, pdcs); fflush(vpx_fp); }
|
||||||
|
} else {
|
||||||
|
queue_render_ack(9);
|
||||||
|
if (vpx_fp) { flush_run();
|
||||||
|
fprintf(vpx_fp, "# post-boot: frame ack (action 9)\n");
|
||||||
|
fflush(vpx_fp); }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Reply action is handler-specific (board side,
|
/* Reply action is handler-specific (board side,
|
||||||
* VR_REMOT.C): most echo data[0] (the sent
|
* VR_REMOT.C): most echo data[0] (the sent
|
||||||
@@ -784,8 +968,19 @@ static void rsh_env(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
|
static void rt_draw(HDC dc, const VFrame &f, int cw, int ch) {
|
||||||
|
/* VPX_CLEAR="r,g,b" (0-1 floats) overrides the wire back_color clear.
|
||||||
|
* Black makes decode gaps honest -- the game's sky-blue back_color reads
|
||||||
|
* as terrain-to-the-horizon when instances are missing. */
|
||||||
|
static int has_clear = -1;
|
||||||
|
static float cc[3];
|
||||||
|
if (has_clear < 0) {
|
||||||
|
const char *cv = getenv("VPX_CLEAR");
|
||||||
|
has_clear = (cv && sscanf(cv, "%f,%f,%f",
|
||||||
|
&cc[0], &cc[1], &cc[2]) == 3) ? 1 : 0;
|
||||||
|
}
|
||||||
glViewport(0, 0, cw, ch);
|
glViewport(0, 0, cw, ch);
|
||||||
glClearColor(f.bg[0], f.bg[1], f.bg[2], 1.0f);
|
if (has_clear) glClearColor(cc[0], cc[1], cc[2], 1.0f);
|
||||||
|
else glClearColor(f.bg[0], f.bg[1], f.bg[2], 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
if (f.has_cam && !f.polys.empty()) {
|
if (f.has_cam && !f.polys.empty()) {
|
||||||
double n = f.nearp > 0 ? f.nearp : 2.0;
|
double n = f.nearp > 0 ? f.nearp : 2.0;
|
||||||
@@ -1187,13 +1382,20 @@ static DWORD WINAPI rt_main(LPVOID) {
|
|||||||
bool cockpit = (ck && ck[0] && ck[0] != '0');
|
bool cockpit = (ck && ck[0] && ck[0] != '0');
|
||||||
const char *ex = getenv("VPX_EXPLODE");
|
const char *ex = getenv("VPX_EXPLODE");
|
||||||
bool explode = !cockpit && ex && ex[0] && ex[0] != '0';
|
bool explode = !cockpit && ex && ex[0] && ex[0] != '0';
|
||||||
|
/* VPX_NOMAIN=1: no native Division window (the dpl3-revive bridge is the
|
||||||
|
* out-the-window view; ours stays available as a wire-decode diagnostic).
|
||||||
|
* Radar/MFD windows are unaffected; the main slot geometry still anchors
|
||||||
|
* the DOSBox parking below. */
|
||||||
|
const char *nm = getenv("VPX_NOMAIN");
|
||||||
|
bool nomain = nm && nm[0] && nm[0] != '0';
|
||||||
|
|
||||||
HWND wnd; HDC dc; HGLRC gl;
|
HWND wnd = NULL; HDC dc = NULL; HGLRC gl = NULL;
|
||||||
int mx = 40, my = 40, mw = 832, mh = 512;
|
int mx = 40, my = 40, mw = 832, mh = 512;
|
||||||
if (cockpit) { mx = 0; my = 0; mw = 800; mh = 600; }
|
if (cockpit) { mx = 0; my = 0; mw = 800; mh = 600; }
|
||||||
else if (explode) { mx = 2020; my = 20; mw = 800; mh = 600; }
|
else if (explode) { mx = 2020; my = 20; mw = 800; mh = 600; }
|
||||||
env_rect("VPX_MAIN", &mx, &my, &mw, &mh);
|
env_rect("VPX_MAIN", &mx, &my, &mw, &mh);
|
||||||
if (!make_gl_window("VPX VelociRender (emulated)", mw, mh, mx, my,
|
if (!nomain &&
|
||||||
|
!make_gl_window("VPX VelociRender (emulated)", mw, mh, mx, my,
|
||||||
cockpit, &wnd, &dc, &gl)) return 1;
|
cockpit, &wnd, &dc, &gl)) return 1;
|
||||||
|
|
||||||
/* Display windows: win0 = bits 0-7 via pal0 (color radar); win3/win4 =
|
/* Display windows: win0 = bits 0-7 via pal0 (color radar); win3/win4 =
|
||||||
@@ -1218,8 +1420,14 @@ static DWORD WINAPI rt_main(LPVOID) {
|
|||||||
* lower MFDs in the outer columns (LR aligned under UR) with the radar
|
* lower MFDs in the outer columns (LR aligned under UR) with the radar
|
||||||
* centered between them; main in the right-hand column (mx/my above). */
|
* centered between them; main in the right-hand column (mx/my above). */
|
||||||
pal_radar_cw = explode;
|
pal_radar_cw = explode;
|
||||||
static const int ex_x[10] = { 760, 0,0,0,0, 20, 1340, 20, 680, 1340 };
|
/* win7 (UL-decoded) and win9 (UR-decoded) trade screen positions: the two
|
||||||
static const int ex_y[10] = { 560, 0,0,0,0, 560, 560, 20, 20, 20 };
|
* upper-outer MFDs read backward on the desktop (user 2026-07-07). This is
|
||||||
|
* a LOCATION swap only -- the decode (channel/palette) is untouched; whether
|
||||||
|
* the underlying cause is a color-translation or pentapus-cable order is
|
||||||
|
* still TBD and must be checked on a real pod. So win7 sits on the right
|
||||||
|
* (x=1340), win9 on the left (x=20). */
|
||||||
|
static const int ex_x[10] = { 760, 0,0,0,0, 20, 1340, 1340, 680, 20 };
|
||||||
|
static const int ex_y[10] = { 560, 0,0,0,0, 560, 560, 20, 20, 20 };
|
||||||
HWND pwnd[10]; HDC pdc[10]; HGLRC pgl[10];
|
HWND pwnd[10]; HDC pdc[10]; HGLRC pgl[10];
|
||||||
bool phave[10];
|
bool phave[10];
|
||||||
int slot = 0; /* debug-grid position counter */
|
int slot = 0; /* debug-grid position counter */
|
||||||
@@ -1260,7 +1468,7 @@ static DWORD WINAPI rt_main(LPVOID) {
|
|||||||
EnterCriticalSection(&rt_lock);
|
EnterCriticalSection(&rt_lock);
|
||||||
if (rt_new) { cur = rt_pending; rt_new = false; redraw = true; }
|
if (rt_new) { cur = rt_pending; rt_new = false; redraw = true; }
|
||||||
LeaveCriticalSection(&rt_lock);
|
LeaveCriticalSection(&rt_lock);
|
||||||
if (redraw && cur.valid) {
|
if (redraw && cur.valid && wnd) {
|
||||||
wglMakeCurrent(dc, gl);
|
wglMakeCurrent(dc, gl);
|
||||||
RECT cr; GetClientRect(wnd, &cr);
|
RECT cr; GetClientRect(wnd, &cr);
|
||||||
rt_draw(dc, cur, cr.right, cr.bottom);
|
rt_draw(dc, cur, cr.right, cr.bottom);
|
||||||
@@ -1288,7 +1496,9 @@ static DWORD WINAPI rt_main(LPVOID) {
|
|||||||
EnumWindows(find_dosbox_wnd, (LPARAM)&dos);
|
EnumWindows(find_dosbox_wnd, (LPARAM)&dos);
|
||||||
if (dos) {
|
if (dos) {
|
||||||
RECT dv, db;
|
RECT dv, db;
|
||||||
GetWindowRect(wnd, &dv);
|
if (wnd) GetWindowRect(wnd, &dv);
|
||||||
|
else { dv.left = mx; dv.top = my; /* VPX_NOMAIN: the */
|
||||||
|
dv.right = mx + mw; dv.bottom = my + mh; } /* main slot */
|
||||||
GetWindowRect(dos, &db);
|
GetWindowRect(dos, &db);
|
||||||
int x = (dv.left + dv.right) / 2 - (int)(db.right - db.left) / 2;
|
int x = (dv.left + dv.right) / 2 - (int)(db.right - db.left) / 2;
|
||||||
int y = dv.bottom + 10;
|
int y = dv.bottom + 10;
|
||||||
@@ -1880,11 +2090,134 @@ static void scene_burst(const unsigned char *p, size_t n) {
|
|||||||
case 9: /* draw_scene: commit */
|
case 9: /* draw_scene: commit */
|
||||||
scene_publish_frame();
|
scene_publish_frame();
|
||||||
break;
|
break;
|
||||||
|
case 38: /* set_sect_pixel: game armed the continuous reticle pick.
|
||||||
|
* From here the board is expected to return the hit in each
|
||||||
|
* frame reply -- that's what unblocks weapons fire. */
|
||||||
|
sect_armed = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Moller-Trumbore ray/triangle: returns hit distance t>0, or -1 (no hit). */
|
||||||
|
static float ray_tri(const float O[3], const float D[3],
|
||||||
|
const float a[3], const float b[3], const float c[3]) {
|
||||||
|
float e1[3], e2[3], p[3], q[3], s[3];
|
||||||
|
for (int i = 0; i < 3; i++) { e1[i] = b[i] - a[i]; e2[i] = c[i] - a[i]; }
|
||||||
|
p[0] = D[1]*e2[2] - D[2]*e2[1];
|
||||||
|
p[1] = D[2]*e2[0] - D[0]*e2[2];
|
||||||
|
p[2] = D[0]*e2[1] - D[1]*e2[0];
|
||||||
|
float det = e1[0]*p[0] + e1[1]*p[1] + e1[2]*p[2];
|
||||||
|
if (det > -1e-6f && det < 1e-6f) return -1.0f; /* ray parallel */
|
||||||
|
float inv = 1.0f / det;
|
||||||
|
for (int i = 0; i < 3; i++) s[i] = O[i] - a[i];
|
||||||
|
float u = (s[0]*p[0] + s[1]*p[1] + s[2]*p[2]) * inv;
|
||||||
|
if (u < 0.0f || u > 1.0f) return -1.0f;
|
||||||
|
q[0] = s[1]*e1[2] - s[2]*e1[1];
|
||||||
|
q[1] = s[2]*e1[0] - s[0]*e1[2];
|
||||||
|
q[2] = s[0]*e1[1] - s[1]*e1[0];
|
||||||
|
float v = (D[0]*q[0] + D[1]*q[1] + D[2]*q[2]) * inv;
|
||||||
|
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
||||||
|
float t = (e2[0]*q[0] + e2[1]*q[1] + e2[2]*q[2]) * inv;
|
||||||
|
return t > 1e-3f ? t : -1.0f; /* in front only */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Real reticle pick: cast the camera centre ray (the screen-0.5,0.5 reticle)
|
||||||
|
* against the live scene and return the nearest triangle's owning instance, the
|
||||||
|
* DCS it hangs under (whose app-specific the game reads as the target Entity),
|
||||||
|
* and the world hit point. Same picking Dave's renderer does, self-contained.
|
||||||
|
* Traversal mirrors scene_publish_frame: dcs -> instance -> object -> lod[0] ->
|
||||||
|
* geogroup -> geometry -> polys, transformed by the DCS world matrix. */
|
||||||
|
static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out,
|
||||||
|
unsigned *geom_out, float xyz_out[3]) {
|
||||||
|
if (!S.view.has_cam) return false;
|
||||||
|
const float O[3] = { S.view.eye[0], S.view.eye[1], S.view.eye[2] };
|
||||||
|
float D[3] = { -S.view.rot[6], -S.view.rot[7], -S.view.rot[8] }; /* cam fwd */
|
||||||
|
float dl = sqrtf(D[0]*D[0] + D[1]*D[1] + D[2]*D[2]);
|
||||||
|
if (dl < 1e-6f) return false;
|
||||||
|
D[0] /= dl; D[1] /= dl; D[2] /= dl;
|
||||||
|
/* Report the TRUE nearest hit -- terrain IS a valid target (you must be
|
||||||
|
* able to fire into the ground and miss). Return ALL of the hit handles:
|
||||||
|
* instance, DCS, geogroup, geometry -- the game does GetAppSpecific on the
|
||||||
|
* DCS (=> targetEntity) AND the geogroup (=> damage zone), so a real board
|
||||||
|
* always has a geogroup on a hit; sending 0 there handed the game a null. */
|
||||||
|
float best_t = 1e30f;
|
||||||
|
unsigned best_inst = 0, best_dcs = 0, best_gg = 0, best_geo = 0;
|
||||||
|
std::map<unsigned, M16> cache;
|
||||||
|
for (std::map<unsigned, std::vector<unsigned> >::const_iterator di =
|
||||||
|
S.children.begin(); di != S.children.end(); ++di) {
|
||||||
|
std::map<unsigned, unsigned>::const_iterator ti = S.type.find(di->first);
|
||||||
|
if (ti == S.type.end() || ti->second != 5) continue; /* DCS parent */
|
||||||
|
M16 world; dcs_world(di->first, cache, world);
|
||||||
|
for (size_t ii = 0; ii < di->second.size(); ii++) {
|
||||||
|
unsigned inst = di->second[ii];
|
||||||
|
std::map<unsigned, unsigned>::const_iterator oi =
|
||||||
|
S.inst_object.find(inst);
|
||||||
|
if (oi == S.inst_object.end()) continue;
|
||||||
|
std::map<unsigned, std::vector<unsigned> >::const_iterator li =
|
||||||
|
S.children.find(oi->second);
|
||||||
|
if (li == S.children.end() || li->second.empty()) continue;
|
||||||
|
std::map<unsigned, std::vector<unsigned> >::const_iterator ggi =
|
||||||
|
S.children.find(li->second[0]); /* lod[0] */
|
||||||
|
if (ggi == S.children.end()) continue;
|
||||||
|
for (size_t g = 0; g < ggi->second.size(); g++) {
|
||||||
|
unsigned gg = ggi->second[g]; /* geogroup handle */
|
||||||
|
std::map<unsigned, std::vector<unsigned> >::const_iterator gci =
|
||||||
|
S.children.find(gg); /* geogroup->geoms */
|
||||||
|
if (gci == S.children.end()) continue;
|
||||||
|
for (size_t k = 0; k < gci->second.size(); k++) {
|
||||||
|
unsigned geo = gci->second[k]; /* geometry handle */
|
||||||
|
std::map<unsigned, std::vector<float> >::const_iterator vi =
|
||||||
|
S.verts.find(geo);
|
||||||
|
std::map<unsigned,
|
||||||
|
std::vector<std::vector<int> > >::const_iterator pi =
|
||||||
|
S.polys.find(geo);
|
||||||
|
if (vi == S.verts.end() || pi == S.polys.end()) continue;
|
||||||
|
const std::vector<float> &vv = vi->second;
|
||||||
|
for (size_t r = 0; r < pi->second.size(); r++) {
|
||||||
|
const std::vector<int> &idx = pi->second[r];
|
||||||
|
if (idx.size() < 3) continue;
|
||||||
|
size_t o0 = (size_t)idx[0] * 3;
|
||||||
|
if (o0 + 2 >= vv.size()) continue;
|
||||||
|
float w0[3]; m16_xform(world, &vv[o0], w0);
|
||||||
|
for (size_t j = 1; j + 1 < idx.size(); j++) {
|
||||||
|
size_t o1 = (size_t)idx[j] * 3;
|
||||||
|
size_t o2 = (size_t)idx[j + 1] * 3;
|
||||||
|
if (o1 + 2 >= vv.size() || o2 + 2 >= vv.size()) continue;
|
||||||
|
float w1[3], w2[3];
|
||||||
|
m16_xform(world, &vv[o1], w1);
|
||||||
|
m16_xform(world, &vv[o2], w2);
|
||||||
|
float t = ray_tri(O, D, w0, w1, w2);
|
||||||
|
if (t > 0.0f && t < best_t) {
|
||||||
|
best_t = t; best_inst = inst;
|
||||||
|
best_dcs = di->first; best_gg = gg; best_geo = geo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best_inst) return false;
|
||||||
|
xyz_out[0] = O[0] + D[0] * best_t;
|
||||||
|
xyz_out[1] = O[1] + D[1] * best_t;
|
||||||
|
xyz_out[2] = O[2] + D[2] * best_t;
|
||||||
|
*inst_out = best_inst; *dcs_out = best_dcs;
|
||||||
|
*gg_out = best_gg; *geom_out = best_geo;
|
||||||
|
static unsigned last_i = 0, last_d = 0;
|
||||||
|
if ((best_inst != last_i || best_dcs != last_d) && vpx_fp) {
|
||||||
|
flush_run();
|
||||||
|
fprintf(vpx_fp, "# raycast pick: inst=%08X dcs=%08X gg=%08X t=%.1f "
|
||||||
|
"pos(%.1f,%.1f,%.1f)\n", best_inst, best_dcs, best_gg, best_t,
|
||||||
|
xyz_out[0], xyz_out[1], xyz_out[2]);
|
||||||
|
fflush(vpx_fp);
|
||||||
|
last_i = best_inst; last_d = best_dcs;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void scene_reset(void) {
|
static void scene_reset(void) {
|
||||||
S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear();
|
S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear();
|
||||||
S.ggmat.clear(); S.children.clear();
|
S.ggmat.clear(); S.children.clear();
|
||||||
@@ -1903,6 +2236,7 @@ static void vpx_render_start(void) {
|
|||||||
}
|
}
|
||||||
#else /* !VPX_RENDER_SUPPORTED */
|
#else /* !VPX_RENDER_SUPPORTED */
|
||||||
static void scene_burst(const unsigned char *, size_t) {}
|
static void scene_burst(const unsigned char *, size_t) {}
|
||||||
|
static bool raycast_pick(unsigned *, unsigned *, unsigned *, unsigned *, float *) { return false; }
|
||||||
static void scene_reset(void) {}
|
static void scene_reset(void) {}
|
||||||
static void vpx_render_start(void) {}
|
static void vpx_render_start(void) {}
|
||||||
#endif
|
#endif
|
||||||
@@ -2077,6 +2411,9 @@ void VPXLOG_Init(void) {
|
|||||||
if (fifo_dump_fp == NULL) LOG_MSG("VPXLOG: cannot open fifodump '%s'", fd);
|
if (fifo_dump_fp == NULL) LOG_MSG("VPXLOG: cannot open fifodump '%s'", fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *fsk = getenv("VPX_FIFOSOCK");
|
||||||
|
if (fsk && atoi(fsk) > 0) fifo_sock_init(atoi(fsk));
|
||||||
|
|
||||||
const char *dd = getenv("VPX_DUMPDIR");
|
const char *dd = getenv("VPX_DUMPDIR");
|
||||||
if (dd && dd[0]) {
|
if (dd && dd[0]) {
|
||||||
strncpy(pal_dump_dir, dd, sizeof pal_dump_dir - 1);
|
strncpy(pal_dump_dir, dd, sizeof pal_dump_dir - 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user