"""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 grows DOWN -- y0 is the top of screen, y1 the bottom; see composite_hud's to_px). 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('= 4: # flush h = struct.unpack_from('= 8: op = _EDGE_OP[action] x, y = struct.unpack_from('= 4: # NewDisplayList h = struct.unpack_from('= 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 grows DOWN -- confirmed live, see to_px) -> pixel space (0..W, 0..H, y down, same direction): px = (x-x0)/(x1-x0)*W, py = (y-y0)/(y1-y0)*H, no inversion. 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 # dpl2d Y grows DOWN (screen convention), confirmed both by the user # (live: horizontal/twist tape appeared at top instead of bottom, the # range ladder filled the wrong direction) and by vrview_gl.py's own # comment/fix for the identical symptom ("mapping straight onto GL's # Y-up NDC rendered the whole HUD flipped") -- y0 (the smaller raw # value) is the TOP of screen, y1 the bottom. No inversion needed. return ((x - x0) / dx * W, (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