Files
TeslaRel410/dpl3-revive/patha/decode_anim.py
T
CydandClaude Fable 5 afc3fd839e Vendor dpl3-revive: the Division/DPL3 renderer, now ours
Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.

What's here:
- parser/  B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/    reverse-engineered format + the definitive VelociRender wire
           protocol (from the original DIVISION source, matches our live
           VPX node/action tables exactly).
- source-ref/  read-only copies of the original DIVISION C (BIZREAD.C,
           DPLTYPES.H, DPL.H) that define the formats.
- patha/   the "virtual VelociRender board": vrboard.py (24-action protocol
           server), vrview.py (numpy software rasterizer, the reference),
           vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
           run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
           Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/  WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin  real BTL4OPT arena wire captures (kept for offline renderer
           testing/regression against OUR game).

.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).

Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:06:25 -05:00

126 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""
decode_anim.py -- full decode of the live animation stream (action 0x1d) plus the
scene-graph edges, from a framed capture of a FLYK.EXE run.
Answers, from capture.raw.bin:
1. exact 0x1d record layout + which handles it animates
2. the scene-graph edges (dcs_link/list_add/dcs_nest) -> who is the VIEW's DCS
3. creates in wire order (to map handles -> SHARKS.SCN entities by order)
4. per-frame trajectories of the animated handles (draw_scene = frame boundary)
python decode_anim.py [capture.raw.bin]
"""
import sys, struct
from collections import Counter, defaultdict
from vrboard import Assembler, A
TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'light',8:'object',
9:'lod',10:'geogroup',11:'geometry',12:'material',13:'texmap',14:'texture',15:'ramp'}
def find_framed_start(data):
for i in range(80000, len(data)-8):
w = struct.unpack_from('<I', data, i)[0]
if (w & 0xffff0000) == 0x40ff0000 and 8 <= (w & 0xffff) <= 1024:
act = struct.unpack_from('<I', data, i+4)[0]
if act in (A.init, A.code860, A.args860, A.data860, A.bss860):
return i
raise SystemExit("no framed start found")
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "capture.raw.bin"
data = open(path, "rb").read()
start = find_framed_start(data)
asm = Assembler(); asm.feed(data[start:])
creates = [] # (handle, type) in wire order
flushes = defaultdict(list) # handle -> [body bytes]
edges = [] # (op, a, b) in wire order
anim = [] # (frame, handle, w1, 12 floats) for 0x1d
op1c = []
frame = 0
for m in asm:
if m.iserver: continue
a, p = m.action, m.payload
if a == A.create and len(p) >= 8:
typ, h = struct.unpack_from('<II', p, 0)
creates.append((h, typ))
elif a == A.flush:
h = struct.unpack_from('<I', p, 0)[0]
flushes[h].append(p)
elif a in (A.dcs_nest, A.dcs_link, A.dcs_prune, A.list_add, A.list_remove):
x, y = struct.unpack_from('<II', p, 0)
edges.append((A(a).name, x, y))
elif a == A.draw_scene:
frame += 1
elif a == 0x1d and len(p) >= 56:
h, w1 = struct.unpack_from('<II', p, 0)
fl = struct.unpack_from('<12f', p, 8)
anim.append((frame, h, w1, fl))
elif a == 0x1c:
op1c.append((frame, p))
types = {h: t for h, t in creates}
tname = lambda h: TYPE.get(types.get(h), f'?{types.get(h)}')
print(f"frames (draw_scene count): {frame}")
print(f"creates: {len(creates)} edges: {len(edges)} 0x1d: {len(anim)} 0x1c: {len(op1c)}")
print("\n=== creates in wire order ===")
for i, (h, t) in enumerate(creates):
print(f" {i:3} handle {h:#06x} {TYPE.get(t, t)}")
print("\n=== edges in wire order ===")
for op, x, y in edges:
print(f" {op:12} {x:#06x}({tname(x)}) {y:#06x}({tname(y)})")
print("\n=== instance bodies (as int words) ===")
for h, t in creates:
if TYPE.get(t) != 'instance': continue
body = flushes[h][-1] if flushes[h] else b''
words = struct.unpack_from('<%dI' % (len(body)//4), body, 0)
annot = []
for w in words[2:]:
if w in types: annot.append(f"{w:#x}={tname(w)}")
elif w == 0xffffffff: annot.append("-1")
elif w == 0: annot.append("0")
else: annot.append(f"{w:#x}")
print(f" inst {h:#06x}: [{', '.join(annot)}]")
print("\n=== 0x1d records ===")
hist = Counter(h for _, h, _, _ in anim)
w1s = Counter(w1 for _, _, w1, _ in anim)
print(" word1 values:", dict(w1s))
print(" targets:")
for h, n in hist.most_common():
print(f" handle {h:#06x} ({tname(h)}): {n} updates")
# per-target first/mid/last translation (assuming [3x3 rows][tx ty tz])
print("\n per-target trajectory (frame: t=(tx,ty,tz), mat row0):")
per = defaultdict(list)
for f, h, w1, fl in anim:
per[h].append((f, fl))
for h, lst in per.items():
print(f" handle {h:#06x} ({tname(h)}):")
for f, fl in (lst[0], lst[len(lst)//2], lst[-1]):
m = fl[:9]; t = fl[9:12]
print(f" frame {f:6}: t=({t[0]:9.3f},{t[1]:9.3f},{t[2]:9.3f}) "
f"m0=({m[0]:6.3f},{m[1]:6.3f},{m[2]:6.3f}) m1=({m[3]:6.3f},{m[4]:6.3f},{m[5]:6.3f}) "
f"m2=({m[6]:6.3f},{m[7]:6.3f},{m[8]:6.3f})")
# how many 0x1d per frame, and which handles per frame (sample a few frames)
byframe = defaultdict(list)
for f, h, w1, fl in anim:
byframe[f].append(h)
counts = Counter(len(v) for v in byframe.values())
print("\n 0x1d msgs per frame histogram:", dict(counts))
fr = sorted(byframe)[len(byframe)//2]
print(f" handles updated in frame {fr}: {[hex(h) for h in byframe[fr]]}")
print("\n=== flush counts per handle (re-flushed nodes = animated via flush?) ===")
for h, lst in sorted(flushes.items()):
if len(lst) > 1:
print(f" handle {h:#06x} ({tname(h)}): {len(lst)} flushes")
if __name__ == '__main__':
main()