HUD reticle: the missing wire mechanism found + composited (verified live)
The user asked to investigate why HUD never renders, reasoning it would also inform the weapon-fire/targeting question. Root cause: the cockpit HUD (reticle + weapon pips) is NOT part of the 3D scene-graph wire traffic this renderer has been parsing all session -- it's a completely separate 2D display-list protocol (dpl2d_*, wire actions 0x29 NewDisplayList / 0x2b FlushDisplayList), which this pipeline has never looked at. BT411's context/gauges-hud.md + phases/phase-02-dpl2d-reticle.md already fully documented this from the game's decompiled source (BTReticleRenderable, the dpl2d opcode table). More directly useful: dpl3-revive/patha/vrview.py (the OTHER renderer) already has a COMPLETE, TESTED interpreter for the wire-level opcodes (hud2d_prims/hud2d_root) -- reused directly here rather than re-derived. hud2d_overlay.py: HudTracker feeds incrementally from the same wire records already flowing through this pipeline (minimal node type+body tracking, the dcs->view list_add edge, and 0x29/0x2b display-list accumulation -- NOT a full scene-graph, just what hud2d_root/hud2d_prims need). composite_hud() maps the resolved primitives from view-rect space (x0..x1, y0..y1, y up -- confirmed live: x[-1,1] y[-0.615,0.615], hither=0.25/yon=1300/zeye=1.732=cot(30 deg) -- independently cross-validating the earlier fog-formula derivation from live wire data, not just the static BTDPL.INI) into pixel space and draws them. Verified offline (netdeath capture): the HUD binding resolves at wire record ~22661 (later in the mission than earlier test frames, which is why it never appeared in this session's spot-checks); composited frame shows a green crosshair + tick-mark cross, a ring, and a right-side range ladder with weapon-pip circles -- an exact match for BT411's documented reticle layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""hud2d_overlay.py -- the cockpit HUD (reticle + weapon pips), composited on
|
||||
top of the i860+GPU 3D render.
|
||||
|
||||
This is a HOST-SIDE overlay, entirely independent of the i860 firmware: the
|
||||
game builds it as a dpl2d_* 2D display list (wire actions 0x29 NewDisplayList /
|
||||
0x2b FlushDisplayList) and binds it to the view node (view body offset 96).
|
||||
vrview.py (the OTHER renderer, dpl3-revive/patha) already has a complete,
|
||||
tested interpreter for this -- hud2d_prims()/hud2d_root() -- reused directly
|
||||
here rather than re-derived. See BT411 context/gauges-hud.md ("Cockpit HUD
|
||||
reticle... LIVE") for the full documented semantics (range ladder, weapon
|
||||
pips colour-coded by range, lock ring, torso-twist tape, compass) -- this
|
||||
module only needs to reproduce the WIRE-side geometry extraction + composite
|
||||
it, not re-implement that logic.
|
||||
|
||||
Verified live (2026-07-20) against netdeath-20260708.fifodump: the resolved
|
||||
primitives' colours match the documented reticle exactly (green crosshair/
|
||||
ladder, yellow width-2 range bar, red/orange filled circles = weapon pips by
|
||||
range class) -- see IG-SHADING-MODEL.md sec 6c.
|
||||
"""
|
||||
import struct, sys, os
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha')
|
||||
from vrview import hud2d_prims, hud2d_root, TYPE # noqa: E402
|
||||
|
||||
_EDGE_OP = {6: 'nest', 7: 'link', 8: 'prune', 11: 'list_add', 12: 'list_remove'}
|
||||
|
||||
|
||||
class HudTracker:
|
||||
"""Incremental wire-record feed -> the view's live dpl2d overlay
|
||||
primitives, in view-rect space (x0..x1, y0..y1, y up).
|
||||
|
||||
Only tracks what hud2d_root/hud2d_prims need: node type+body (create/
|
||||
flush), the dcs->view list_add edge, and the dl2d display-list words
|
||||
(0x29/0x2b). Deliberately NOT a full VirtualBoard -- the 3D scene is the
|
||||
i860 firmware's job; this is purely the 2D overlay layer."""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes = {}
|
||||
self.dl2d = {}
|
||||
self.view = None
|
||||
self._acc = None # in-progress 0x2b chunk accumulation
|
||||
self._hud2d_bind = {} # hud2d_root's own cache, keyed by view
|
||||
|
||||
def feed(self, action, payload):
|
||||
p = payload
|
||||
if action == 1 and len(p) >= 8: # create
|
||||
typ = struct.unpack_from('<I', p, 0)[0]
|
||||
h = struct.unpack_from('<I', p, 4)[0]
|
||||
self.nodes[h] = {'type': typ, 'body': b''}
|
||||
elif action == 3 and len(p) >= 4: # flush
|
||||
h = struct.unpack_from('<I', p, 0)[0]
|
||||
n = self.nodes.setdefault(h, {'type': None, 'body': b''})
|
||||
n['body'] = p[4:]
|
||||
elif action in _EDGE_OP and len(p) >= 8:
|
||||
op = _EDGE_OP[action]
|
||||
x, y = struct.unpack_from('<II', p, 0)
|
||||
if op == 'list_add':
|
||||
tn = lambda h: TYPE.get(self.nodes.get(h, {}).get('type'))
|
||||
if tn(y) == 'view' and tn(x) == 'dcs':
|
||||
self.view = y
|
||||
elif action == 0x29 and len(p) >= 4: # NewDisplayList
|
||||
h = struct.unpack_from('<I', p, 0)[0]
|
||||
self.dl2d[h] = []
|
||||
elif action == 0x2b and len(p) >= 20: # FlushDisplayList
|
||||
hdr, _nxt, tail, size = struct.unpack_from('<4I', p, 0)
|
||||
size = min(size, (len(p) - 20) // 4)
|
||||
dat = list(struct.unpack_from('<%dI' % size, p, 20))
|
||||
if hdr:
|
||||
self._acc = [hdr, max(tail, 1) - 1, dat]
|
||||
elif self._acc is not None:
|
||||
self._acc[1] -= 1
|
||||
self._acc[2] += dat
|
||||
if self._acc is not None and self._acc[1] <= 0:
|
||||
self.dl2d[self._acc[0]] = self._acc[2]
|
||||
self._acc = None
|
||||
|
||||
def view_rect(self):
|
||||
"""(x0, y0, x1, y1, hither, yon) from the current view's body, or
|
||||
None if no full-length view flush has been seen yet."""
|
||||
if self.view is None:
|
||||
return None
|
||||
vb = self.nodes.get(self.view, {}).get('body') or b''
|
||||
if len(vb) < 68: # need stored-body floats up to index 16
|
||||
return None
|
||||
f = struct.unpack_from('<24f', vb, 0)
|
||||
# docstring's wire-payload word k == stored-body word k-1 (vrview.py's
|
||||
# own note): x0,y0,x1,y1,zeye,xs,ys,hither,yon at payload words 6..14.
|
||||
x0, y0, x1, y1 = f[5], f[6], f[7], f[8]
|
||||
hither, yon = f[12], f[13]
|
||||
return (x0, y0, x1, y1, hither, yon)
|
||||
|
||||
def primitives(self):
|
||||
"""Current HUD primitives (view-rect space), or [] if not resolvable
|
||||
yet (no view, or its display-list binding hasn't landed)."""
|
||||
root = hud2d_root(self, self.view)
|
||||
if not root:
|
||||
return []
|
||||
return hud2d_prims(self.dl2d, root)
|
||||
|
||||
|
||||
def composite_hud(img, tracker, W, H):
|
||||
"""Draw the tracker's current HUD primitives onto img (HxWx3 uint8,
|
||||
modified in place). view-rect (x0..x1, y0..y1, y UP) -> pixel space
|
||||
(0..W, 0..H, y DOWN): px = (x-x0)/(x1-x0)*W, py = (1-(y-y0)/(y1-y0))*H.
|
||||
No-op (returns False) if the view rect or display list isn't resolved
|
||||
yet -- callers should keep showing the last good frame, same as the 3D
|
||||
render's own "skip if not ready" convention."""
|
||||
rect = tracker.view_rect()
|
||||
if rect is None:
|
||||
return False
|
||||
x0, y0, x1, y1, _hither, _yon = rect
|
||||
prims = tracker.primitives()
|
||||
if not prims:
|
||||
return False
|
||||
from PIL import Image, ImageDraw
|
||||
pim = Image.fromarray(img, 'RGB')
|
||||
draw = ImageDraw.Draw(pim)
|
||||
dx = x1 - x0 or 1.0
|
||||
dy = y1 - y0 or 1.0
|
||||
|
||||
def to_px(pt):
|
||||
x, y = pt
|
||||
return ((x - x0) / dx * W, (1.0 - (y - y0) / dy) * H)
|
||||
|
||||
for kind, col, width, pts in prims:
|
||||
if len(pts) < 1:
|
||||
continue
|
||||
rgb = tuple(max(0, min(255, int(round(c * 255)))) for c in col)
|
||||
w = max(1, int(round(width)))
|
||||
xy = [to_px(p) for p in pts]
|
||||
if kind == 'points':
|
||||
r = 1.5
|
||||
for (px, py) in xy:
|
||||
draw.ellipse((px - r, py - r, px + r, py + r), fill=rgb)
|
||||
elif kind == 'segs':
|
||||
for i in range(0, len(xy) - 1, 2):
|
||||
draw.line([xy[i], xy[i + 1]], fill=rgb, width=w)
|
||||
elif kind == 'strip':
|
||||
if len(xy) >= 2:
|
||||
draw.line(xy, fill=rgb, width=w)
|
||||
elif kind == 'poly':
|
||||
if len(xy) >= 3:
|
||||
draw.polygon(xy, fill=rgb)
|
||||
img[:] = np.asarray(pim)
|
||||
return True
|
||||
Reference in New Issue
Block a user