diff --git a/emulator/render-bridge/DEATH-SEQUENCE-NOTES.md b/emulator/render-bridge/DEATH-SEQUENCE-NOTES.md new file mode 100644 index 0000000..4564082 --- /dev/null +++ b/emulator/render-bridge/DEATH-SEQUENCE-NOTES.md @@ -0,0 +1,72 @@ +# Death-sequence camera — findings (2026-07-09) + +Pilot report (networked mission one, arena, Mad Cat): at death the bridge +render shows **the inside of the escape pod** instead of the authentic +sequence (trainer video: the dying pilot never sees the pod interior — the +view rides to the blue-swirly translocation portal, the new mech steps out +of the portal onto the field; the pod eject is a third-party visual only). + +Reference capture: `captures/netdeath-20260708.fifodump` (4.5 MB, complete +networked mission incl. the death and mission-end fade; taken 2026-07-08 +UTC-night). Probes: `probes/deathcam_probe.py`, `probes/viewflush_probe.py` +(run with `py -3.13`; they replay the capture into a headless VirtualBoard). + +## What the wire actually says + +- **The camera DCS chain never leaves the wreck.** Chain eye stays ~7.8 m + from the vehicle root through the whole death tail (never >100 m from it + anywhere in 18,477 frames). So the `fp_cam` <100 m chain-sanity guard in + live_bridge.py is NOT the culprit, and the transit is NOT a camera-chain + move. +- **Type-3 (view) flushes carry projection + fog only**, no camera pose: + floats [0..8] frustum edges, [9] ≈1.73 FOV scale, [10..11] = 832×512, + **[12]/[13] = hither/yon** (0.25/1300 in gameplay), [18]/[19] = fog + near/far, [20..22] = fog RGB. Only 65 flushes in the whole mission — they + fire on change, not per frame. +- **The mission-end fade is on the wire and already renders**: ~22 flushes + sweeping fog near/far from (193, 1205) down to (0.01, 0.05) with RGB + fading to black — the reverse of the mission-start black hold. +- **At death the 0x1f articulation batches switch to driving nodes anchored + at the world origin (0, 1, 0)** — the portal diorama animating — while + the mech's own nodes stop updating. This is the clearest wire signature + that the death sequence is active. + +## Hypothesis (matches the BT411 decomp) + +`BTPOVStartEndRenderable` (game source) is constructed with BOTH +`dplMainZone` and `dplDeathZone`. The authentic transit is a **view/zone +re-parent**: the game moves the pilot's dpl_VIEW into the death zone, whose +own camera root does the ride (death site → portal at origin → dropzone). +Our bridge only follows the main-zone camera chain, so it stays at the +wreck — with the escape-pod geometry spawned around it. + +## Reconciliation with the EARLIER respawn-death capture + +The 2026-07-09 analysis of `netdeath_snap.fifodump` (~9MB, the earlier +networked death WITH respawn; see weapon-visuals memory + death_strip.py) +found the chain camera DOES transit: death site → (0, 10, 0) at the portal +during the black hold → dropzone for the fade-in. Two death variants: + +- **Respawn death** (lives remain): chain camera rides to the portal. The + fp_cam `<100 m from vehicle` sanity guard REJECTS exactly this ride → + falls back to the wreck → pilot sees pod/shroud geometry. **For this + variant the guard IS the bug**: relax it once a mission is established + (e.g. keep the guard only until the first accepted lock, or accept large + excursions when the chain eye moves CONTINUOUSLY frame-to-frame). +- **Mission-end death** (tonight's capture): no transit at all — fog fade + at the wreck, mission over. The wreck-side view may be authentic here; + compare period footage of an end-of-mission death before changing it. + +## Next steps + +1. Find the death-zone view root on the wire: at the death frame, diff the + node topology (nest/link records) for a second camera-like chain, and + check which display list the portal diorama nodes hang from. +2. Bridge fix: detect death-sequence-active (origin-anchored 0x1f + activity + fog fade + the known w3/w4 flips) → release `fp_cam` and + follow the death-zone chain (or, worst case, synthesize the ride: + ease the eye from the wreck to the portal diorama at origin). +3. Note: this capture's death was at mission end (no respawn) — capture a + MID-mission death (lives remaining) to see the full portal ride + + new-mech reveal before calling the fix authentic. Compare against the + pilot's VHS stills (7-frame sequence, matched 2026-07-08). diff --git a/emulator/render-bridge/captures/netdeath-20260708.fifodump b/emulator/render-bridge/captures/netdeath-20260708.fifodump new file mode 100644 index 0000000..3cf0c51 Binary files /dev/null and b/emulator/render-bridge/captures/netdeath-20260708.fifodump differ diff --git a/emulator/render-bridge/probes/deathcam_probe.py b/emulator/render-bridge/probes/deathcam_probe.py new file mode 100644 index 0000000..e046b0f --- /dev/null +++ b/emulator/render-bridge/probes/deathcam_probe.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Death-camera forensics on the saved networked death: +per draw frame, where is the CHAIN camera eye vs the player vehicle root? +Does the chain legitimately transit >100m away (portal ride) -- i.e. is the +fp_cam <100m sanity guard what forces the escape-pod-interior view? +Also log view-flush params (fog near/far/rgb + full float head) around the +transit to find a robust 'death sequence active' gate.""" +import struct, sys, os +sys.path.insert(0, r'c:\VWE\TeslaRel410\dpl3-revive\patha') +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Msg +from vrview import Renderer + +path = os.path.join(os.path.dirname(__file__), '..', 'captures', 'netdeath-20260708.fifodump') +data = open(path, 'rb').read() +board = VirtualBoard() +r = Renderer(w=64, h=40) + +frames = 0 +created = {} +fogs = [] # (frame, floats[:24]) +track = [] # (frame, chain_eye, veh_t, dist) +o = 0 +while o + 8 <= len(data): + if data[o:o+4] != b'VPXM': + o += 1; continue + ln = struct.unpack_from(' len(data): break + body = data[o+8:o+8+ln]; o += 8 + ln + if len(body) < 4: continue + a = struct.unpack_from('= 0x100: continue + p = body[4:] + if a == 1 and len(p) >= 8: + created[struct.unpack_from('= 96: + h = struct.unpack_from('100m (the guard threshold) +first_far = next((i for i, (f, ec, t, d) in enumerate(track) if d > 100.0), None) +print(f"first frame with chain-vehicle distance >100m: " + + (f"f{track[first_far][0]}" if first_far is not None else "NONE")) + +# print the trajectory every ~30 frames over the last 1200 frames +print("\n-- chain eye vs vehicle root (tail):") +tail = track[-1200:] +for i in range(0, len(tail), 30): + f, ec, t, d = tail[i] + ts = "None" if t is None else f"({t[0]:8.1f},{t[1]:7.1f},{t[2]:8.1f})" + print(f" f{f}: eye=({ec[0]:8.1f},{ec[1]:7.1f},{ec[2]:8.1f}) veh={ts} dist={d:8.1f}") + +# fog/view flushes in the tail window, with the full float head for gate hunting +if track: + cut = track[-1200][0] if len(track) >= 1200 else track[0][0] + print(f"\n-- view flushes after f{cut} (floats 14..23):") + last = None + for f, fl in fogs: + if f < cut: continue + k = tuple(round(x, 2) for x in fl[14:24]) + if k != last: + print(f" f{f}: " + " ".join(f"{x:.2f}" for x in k)) + last = k diff --git a/emulator/render-bridge/probes/viewflush_probe.py b/emulator/render-bridge/probes/viewflush_probe.py new file mode 100644 index 0000000..789baeb --- /dev/null +++ b/emulator/render-bridge/probes/viewflush_probe.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Type-3 (view) flush anatomy: are floats 0..13 a live camera pose? +Sample across the whole mission + densely in the death tail; compare the +candidate position triplet(s) against the vehicle root per frame.""" +import struct, os, sys +sys.path.insert(0, r'c:\VWE\TeslaRel410\dpl3-revive\patha') +import numpy as np + +path = os.path.join(os.path.dirname(__file__), '..', 'captures', 'netdeath-20260708.fifodump') +data = open(path, 'rb').read() + +frames = 0 +created = {} +veh = {} # latest max-translation 0x1f pose per frame +flushes = [] # (frame, view_handle, floats24, veh_t) +last_t = None +o = 0 +while o + 8 <= len(data): + if data[o:o+4] != b'VPXM': + o += 1; continue + ln = struct.unpack_from(' len(data): break + body = data[o+8:o+8+ln]; o += 8 + ln + if len(body) < 4: continue + a = struct.unpack_from('= 8: + created[struct.unpack_from('= 8: + # crude: largest |t| pose in the batch = player vehicle root + n = struct.unpack_from(' len(p): break + h = struct.unpack_from(' best[0]: + best = (m, t) + off += 4 + 48 + if best: + last_t = best[1] + elif a == 3 and len(p) >= 100: + h = struct.unpack_from('