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:
Cyd
2026-07-21 14:40:31 -05:00
co-authored by Claude Opus 4.8
parent 3364b65ae8
commit 859529d6f2
@@ -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