Phase 3a: decode captured VPX render stream to pixels (SMPTE bars)
- vpxlog.cpp: VPX_FIFODUMP=<path> records every FIFO burst ('VPXM' records)
- decode_fifodump.py: action census + payload dumps of a capture
- render_capture.py: reconstruct the DPL scene graph from a capture and
software-render each draw_scene frame (camera, view, materials, geometry
all taken from the wire)
- divrgb.conf + divrgb.fifodump: flyk divrgb.scn capture fixture
- divrgb-decoded.png / divrgb-frame0.png: first images ever produced from
the Rel 4.10 VPX protocol without a real board -- the textbook SMPTE
color-bar pattern, validating verts/conns/materials/camera in one shot
- PHASE3-PROGRESS.md: the established Rel 4.10 wire protocol (action map,
node types, message layouts); RENDER-HARNESS.md updated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,12 @@
|
|||||||
dosbox-x/
|
dosbox-x/
|
||||||
image/
|
image/
|
||||||
*.png
|
*.png
|
||||||
|
# Phase 3 evidence renders (keep: decoded from the captured VPX stream)
|
||||||
|
!divrgb-decoded.png
|
||||||
|
!divrgb-frame0.png
|
||||||
dbx_out.txt
|
dbx_out.txt
|
||||||
|
vpx*.txt
|
||||||
|
sweep_*.txt
|
||||||
dbx_err.txt
|
dbx_err.txt
|
||||||
src/
|
src/
|
||||||
vpxlog.txt
|
vpxlog.txt
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Phase 3 — Render Backend: Progress
|
||||||
|
|
||||||
|
**Status (2026-07-03): Phase 3a complete — the render command stream is fully
|
||||||
|
captured and decoded, and a captured DIVRGB frame has been reconstructed to
|
||||||
|
pixels. First image ever produced from the Rel 4.10 VPX protocol without a
|
||||||
|
real board.**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
That is `flyk divrgb.scn` — Division's SMPTE color-bar calibration scene —
|
||||||
|
rendered *entirely from the FIFO command stream captured by the emulated VPX
|
||||||
|
board*: geometry, connectivity, materials, camera, viewport and background all
|
||||||
|
come from the wire, none from the scene files. `divrgb-frame0.png` is the same
|
||||||
|
capture drawn with the actual frame-0 spline camera the app sent.
|
||||||
|
|
||||||
|
## 3a. Full FIFO capture
|
||||||
|
|
||||||
|
`vpxlog.cpp` now records every FIFO burst when `VPX_FIFODUMP=<path>` is set
|
||||||
|
(records: `'VPXM'` magic, u32 length, raw bytes; one record per burst between
|
||||||
|
`outputData` tag writes). The capture used here: `divrgb.fifodump`
|
||||||
|
(1297 messages) from `divrgb.conf`.
|
||||||
|
|
||||||
|
Tools:
|
||||||
|
|
||||||
|
- `decode_fifodump.py <dump> [--hex N] [--action A]` — action census + payload
|
||||||
|
hexdumps.
|
||||||
|
- `render_capture.py <dump> [-o out.png] [--frame N] [--eye x,y,z]` —
|
||||||
|
reconstructs the scene graph and software-renders the frame each
|
||||||
|
`draw_scene` commits.
|
||||||
|
|
||||||
|
## The Rel 4.10 wire protocol (established from this capture)
|
||||||
|
|
||||||
|
One burst per message: `[action:4][payload]`, packetized at 508 bytes.
|
||||||
|
The i860 image download itself rides the FIFO in this build (actions 18–20,
|
||||||
|
`VREND.MNG`). The DPL3 `vr_action` enum holds for 0–23 but Rel 4.10 extends
|
||||||
|
and re-purposes the tail:
|
||||||
|
|
||||||
|
| action | meaning | payload |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | init | args string (`/device~0x150~/video~svga~…`) |
|
||||||
|
| 1 | create | `[type][name]` — **the host assigns node names** (1,2,3…); the board's create reply node value is ignored |
|
||||||
|
| 3 | flush | `[name][type][node struct]` (see node types below) |
|
||||||
|
| 7 | dcs_link | `[parent][child]` |
|
||||||
|
| 9 | draw_scene | `[view?][0][0][1][1.0f]` — commits the frame |
|
||||||
|
| 11 | list_add | `[parent][child]` |
|
||||||
|
| 18–21 | 860code/data/bss/args | i860 download (FIFO in this build) |
|
||||||
|
| 23 | **set_geom_verts** (Rel4.10; DPL3 had 22) | hdr `[name][0][n_verts][3][n_blocks][1][5][n_verts][1.0f]`, then float32 x,y,z per vertex |
|
||||||
|
| 25 | **set_geom_conns** (new) | hdr `[name][n_polys][loop_len][0]`, then indices; each poly a closed loop (last=first) |
|
||||||
|
| 31 | **camera** (new) | `[?][view][3×3 rotation, row-major][eye x,y,z]` per frame |
|
||||||
|
| 45 (0x2D) | sync | token ping (see PHASE2) |
|
||||||
|
|
||||||
|
Node types (from create/flush pairs in this capture):
|
||||||
|
|
||||||
|
| type | node | flush payload highlights |
|
||||||
|
|---|---|---|
|
||||||
|
| 2 | texture/ramp? | one word, `0xFFFFFFFF` / `0x0FFFFFFF` |
|
||||||
|
| 3 | view | window l,b,r,t (±1 × ±0.6154), window-plane distance 1.3, viewport 832×512, near 2, far 12000, background RGB |
|
||||||
|
| 4 | light | |
|
||||||
|
| 5 | dcs | 4×4 matrix |
|
||||||
|
| 6 | material (old-style) | RGB floats |
|
||||||
|
| 7 | object | |
|
||||||
|
| 8 | lod | 2×4 bounds + lists |
|
||||||
|
| 9 | geogroup | bounds; **payload int 14 = material node name** |
|
||||||
|
| 10 | geometry | `[geo_type][n_polys][n_verts]…[radius]` |
|
||||||
|
| 11 | material | **floats 10–12 (payload) = diffuse RGB** |
|
||||||
|
|
||||||
|
Graph: `list_add` links object→lod→geogroup→geometry (and zone-level nodes to
|
||||||
|
node 0); geogroups bind materials by name. The 13 DIVRGB bars decode to the
|
||||||
|
textbook SMPTE pattern — 7 color bars, the reverse strip, and the −I/white/+Q
|
||||||
|
PLUGE row — which validates vertices, connectivity, materials, camera and
|
||||||
|
window mapping in one image. (Division screen x runs opposite to a GL-style
|
||||||
|
eye space; without negating x the pattern comes out mirrored.)
|
||||||
|
|
||||||
|
## Next: 3b — live OpenGL backend
|
||||||
|
|
||||||
|
Feed the same decode path a live stream inside DOSBox-X instead of a dump:
|
||||||
|
maintain the node store in `vpxlog.cpp` (or a sibling `vpxrender.cpp`), upload
|
||||||
|
geometry on set_geom_verts/conns, draw on draw_scene into an OpenGL window
|
||||||
|
(or the DOSBox surface). The `divrgb.fifodump` fixture allows developing the
|
||||||
|
backend offline; `flyk yip.scn` (CYCLE, Red Planet geometry) is the next
|
||||||
|
fixture up once the DOS/4GW sync variant is handled, then the game itself
|
||||||
|
(`buttee.bgf` etc. via the production content path).
|
||||||
@@ -61,12 +61,11 @@ clean shutdown) is correct for an arbitrary DPL renderer, not just the game.
|
|||||||
- `rio.conf` — game with the real RIO on COM1 (Phase 5, validated).
|
- `rio.conf` — game with the real RIO on COM1 (Phase 5, validated).
|
||||||
- `respond.conf` — game with the minimal test image.
|
- `respond.conf` — game with the minimal test image.
|
||||||
|
|
||||||
## Next (Phase 3): pixels
|
## Phase 3a: pixels (DONE — see PHASE3-PROGRESS.md)
|
||||||
|
|
||||||
Use `flyk DIVRGB.SCN` (color bars) as the first geometry target. The geometry /
|
`flyk DIVRGB.SCN` was captured with `VPX_FIFODUMP` (`divrgb.conf` →
|
||||||
material / texture commands already flow over the FIFO as framed messages
|
`divrgb.fifodump`), the full Rel 4.10 FIFO message set was decoded
|
||||||
(`vr_create`, `vr_set_geom_verts` (22), `vr_set_texmap_texels` (23),
|
(`decode_fifodump.py`), and the captured frame was reconstructed to pixels
|
||||||
`vr_list_add`, `vr_draw_scene`). Phase 3 decodes those payloads — the same
|
(`render_capture.py` → `divrgb-decoded.png`): the textbook SMPTE color-bar
|
||||||
DIV-BIZ2 vertex/material formats already implemented in
|
pattern, drawn entirely from the wire protocol. Phase 3b is the live OpenGL
|
||||||
`restoration/divformats.py` — into an OpenGL backend and presents to a window,
|
backend inside DOSBox-X.
|
||||||
turning the emulated board's frame into actual pixels.
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Decode a VPX_FIFODUMP capture (Phase 3a).
|
||||||
|
|
||||||
|
The dump is a sequence of records, each one FIFO burst (the bytes REP OUTSWed
|
||||||
|
to the FIFO port between two outputData tag writes):
|
||||||
|
|
||||||
|
'VPXM' u32-LE length, then <length> raw bytes
|
||||||
|
|
||||||
|
Wire model (VR_COMMS.C velocirender_transmit, FIFO path): each transmit is two
|
||||||
|
bursts -- burst 1 is the 4-byte action word, burst 2 the message data. So
|
||||||
|
records pair up into [action][data] messages. velocirender_packetize splits
|
||||||
|
payloads >508 bytes into multiple transmits with the same action.
|
||||||
|
|
||||||
|
Usage: decode_fifodump.py <dump> [--hex N] [--action A]
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
# DPL3 VR_PROT.H enum -- the Rel4.10 build extends this (0x2D sync etc.);
|
||||||
|
# names beyond the DPL3 enum are provisional and marked with '?'.
|
||||||
|
ACTION_NAMES = {
|
||||||
|
0: "init", 1: "create", 2: "delete", 3: "flush",
|
||||||
|
4: "sect_pixel", 5: "sect_vector",
|
||||||
|
6: "dcs_nest", 7: "dcs_link", 8: "dcs_prune",
|
||||||
|
9: "draw_scene", 10: "draw_scene_complete",
|
||||||
|
11: "list_add", 12: "list_remove", 13: "morph",
|
||||||
|
14: "version", 15: "statistics", 16: "readpixels",
|
||||||
|
17: "hspcode", 18: "860code", 19: "860data", 20: "860bss", 21: "860args",
|
||||||
|
22: "set_geom_verts", 23: "set_texmap_texels",
|
||||||
|
0x2D: "sync(rel410)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_records(path):
|
||||||
|
recs = []
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
hdr = f.read(8)
|
||||||
|
if len(hdr) < 8:
|
||||||
|
break
|
||||||
|
magic, ln = hdr[:4], struct.unpack("<I", hdr[4:])[0]
|
||||||
|
if magic != b"VPXM":
|
||||||
|
raise SystemExit(f"bad magic at record {len(recs)}: {magic!r}")
|
||||||
|
recs.append(f.read(ln))
|
||||||
|
return recs
|
||||||
|
|
||||||
|
|
||||||
|
def pair_messages(recs):
|
||||||
|
"""Split burst records into (action, data) messages.
|
||||||
|
|
||||||
|
DPL3's transmit sends two bursts (action, then data); the Rel4.10 build
|
||||||
|
observed in captures pumps [action:4][data] as ONE burst per tag write.
|
||||||
|
Handle both: a 4-byte record whose next record isn't 4 bytes pairs with
|
||||||
|
it; otherwise the record's first word is the action."""
|
||||||
|
msgs = []
|
||||||
|
i = 0
|
||||||
|
while i < len(recs):
|
||||||
|
r = recs[i]
|
||||||
|
if len(r) == 4 and i + 1 < len(recs) and len(recs[i + 1]) > 4:
|
||||||
|
msgs.append((struct.unpack("<I", r)[0], recs[i + 1]))
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if len(r) >= 4:
|
||||||
|
msgs.append((struct.unpack("<I", r[:4])[0], r[4:]))
|
||||||
|
else:
|
||||||
|
msgs.append((None, r))
|
||||||
|
i += 1
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
|
def aname(a):
|
||||||
|
if a is None:
|
||||||
|
return "<no-action>"
|
||||||
|
n = ACTION_NAMES.get(a)
|
||||||
|
return f"{a}:{n}" if n else f"{a}(0x{a:X})?"
|
||||||
|
|
||||||
|
|
||||||
|
def hexdump(b, limit=64):
|
||||||
|
out = []
|
||||||
|
for i in range(0, min(len(b), limit), 16):
|
||||||
|
row = b[i:i + 16]
|
||||||
|
hx = " ".join(f"{c:02x}" for c in row)
|
||||||
|
tx = "".join(chr(c) if 32 <= c < 127 else "." for c in row)
|
||||||
|
out.append(f" {i:04x} {hx:<48} {tx}")
|
||||||
|
if len(b) > limit:
|
||||||
|
out.append(f" ... {len(b)} bytes total")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def floats(b, n=8):
|
||||||
|
k = min(n, len(b) // 4)
|
||||||
|
return " ".join(f"{v:.4g}" for v in struct.unpack(f"<{k}f", b[:k * 4]))
|
||||||
|
|
||||||
|
|
||||||
|
def ints(b, n=8):
|
||||||
|
k = min(n, len(b) // 4)
|
||||||
|
return " ".join(f"{v:#x}" for v in struct.unpack(f"<{k}I", b[:k * 4]))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = sys.argv[1]
|
||||||
|
hexn = 0
|
||||||
|
only = None
|
||||||
|
if "--hex" in sys.argv:
|
||||||
|
hexn = int(sys.argv[sys.argv.index("--hex") + 1])
|
||||||
|
if "--action" in sys.argv:
|
||||||
|
only = int(sys.argv[sys.argv.index("--action") + 1], 0)
|
||||||
|
|
||||||
|
recs = read_records(path)
|
||||||
|
msgs = pair_messages(recs)
|
||||||
|
print(f"{len(recs)} burst records -> {len(msgs)} messages\n")
|
||||||
|
|
||||||
|
counts = Counter()
|
||||||
|
sizes = Counter()
|
||||||
|
for a, d in msgs:
|
||||||
|
counts[a] += 1
|
||||||
|
sizes[a] += len(d)
|
||||||
|
|
||||||
|
print("action count total-bytes sizes")
|
||||||
|
persize = {}
|
||||||
|
for a, d in msgs:
|
||||||
|
persize.setdefault(a, Counter())[len(d)] += 1
|
||||||
|
for a in sorted(counts, key=lambda x: (x is None, x)):
|
||||||
|
sz = ",".join(f"{s}x{c}" if c > 1 else f"{s}"
|
||||||
|
for s, c in sorted(persize[a].items())[:6])
|
||||||
|
print(f"{aname(a):<26} {counts[a]:>5} {sizes[a]:>11} {sz}")
|
||||||
|
|
||||||
|
if hexn or only is not None:
|
||||||
|
print()
|
||||||
|
shown = 0
|
||||||
|
for i, (a, d) in enumerate(msgs):
|
||||||
|
if only is not None and a != only:
|
||||||
|
continue
|
||||||
|
print(f"[{i}] {aname(a)} len={len(d)}")
|
||||||
|
if d:
|
||||||
|
print(" ints: ", ints(d))
|
||||||
|
print(" floats:", floats(d))
|
||||||
|
print(hexdump(d, hexn if hexn else 64))
|
||||||
|
shown += 1
|
||||||
|
if hexn and shown >= 40 and only is None:
|
||||||
|
print("... (truncated)")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
[sdl]
|
||||||
|
output=opengl
|
||||||
|
[dosbox]
|
||||||
|
memsize=32
|
||||||
|
machine=svga_s3
|
||||||
|
[cpu]
|
||||||
|
core=normal
|
||||||
|
cputype=pentium
|
||||||
|
cycles=20000
|
||||||
|
[serial]
|
||||||
|
serial1=disabled
|
||||||
|
serial2=disabled
|
||||||
|
[autoexec]
|
||||||
|
mount c "C:\VWE\TeslaRel410\ALPHA_1"
|
||||||
|
c:
|
||||||
|
cd \VWETEST\VGLTEST
|
||||||
|
set VIDEOFORMAT=svga
|
||||||
|
set TEMP=c:\
|
||||||
|
set DPLARG=/tranny~division\vrendmon.btl~/i860~division\vrend.mng~/device~0x150~/video~svga~/pipes~1~/qual~0x14
|
||||||
|
32rtm.exe -x
|
||||||
|
flyk.exe divrgb.scn ..\cycle\camera.spl
|
||||||
|
32rtm.exe -u
|
||||||
|
echo FLYK-DONE rc=%errorlevel%
|
||||||
|
pause
|
||||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd /c/VWE/TeslaRel410/emulator/src || exit 1
|
||||||
|
make -j3 2>&1 | tail -15
|
||||||
|
echo "EXIT=${PIPESTATUS[0]}"
|
||||||
|
ls -la src/dosbox-x.exe
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Phase 3: render a captured VPX FIFO stream to pixels.
|
||||||
|
|
||||||
|
Reconstructs the DPL scene graph from a VPX_FIFODUMP capture (see
|
||||||
|
decode_fifodump.py for the record format) and software-renders the frame each
|
||||||
|
vr_draw_scene commits, using the camera matrix delivered by the per-frame
|
||||||
|
camera action (31) and the view parameters from the view-node flush.
|
||||||
|
|
||||||
|
Rel4.10 wire protocol (established from the divrgb.scn capture, 2026-07-03):
|
||||||
|
|
||||||
|
action 0 init args string
|
||||||
|
action 1 create [type][name] (host assigns node names)
|
||||||
|
action 3 flush [name][type][node struct fields]
|
||||||
|
action 9 draw_scene commit frame
|
||||||
|
action 11 list_add [parent][child]
|
||||||
|
action 23 set_geom_verts hdr [name][n_verts][3][n_blocks][1][5][n_verts][1.0f]
|
||||||
|
then float32 x,y,z per vertex (508-byte packetized)
|
||||||
|
action 25 set_geom_conns hdr [name][n_polys][verts_per_poly+1][0]
|
||||||
|
then indices, each poly a closed loop (last=first)
|
||||||
|
action 31 camera [?][view][3x3 rotation row-major][eye x,y,z]
|
||||||
|
action 45 sync token ping
|
||||||
|
|
||||||
|
node types (from create/flush): 2=texture? 3=view 4=light 5=dcs 6=material(old)
|
||||||
|
7=object 8=lod 9=geogroup 10=geometry 11=material
|
||||||
|
type 9 geogroup flush: payload int 14 = material node name
|
||||||
|
type 11 material flush: floats[10..12] = diffuse RGB
|
||||||
|
type 3 view flush: floats include window (l,b,r,t), viewport w,h, near, far
|
||||||
|
|
||||||
|
Usage: render_capture.py <dump> [-o out.png] [--frame N]
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
def read_messages(path):
|
||||||
|
msgs = []
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
hdr = f.read(8)
|
||||||
|
if len(hdr) < 8:
|
||||||
|
break
|
||||||
|
if hdr[:4] != b"VPXM":
|
||||||
|
raise SystemExit("bad magic")
|
||||||
|
d = f.read(struct.unpack("<I", hdr[4:])[0])
|
||||||
|
if len(d) >= 4:
|
||||||
|
msgs.append((struct.unpack("<I", d[:4])[0], d[4:]))
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
|
def u32s(b):
|
||||||
|
return list(struct.unpack(f"<{len(b) // 4}I", b[: len(b) // 4 * 4]))
|
||||||
|
|
||||||
|
|
||||||
|
def f32s(b):
|
||||||
|
return list(struct.unpack(f"<{len(b) // 4}f", b[: len(b) // 4 * 4]))
|
||||||
|
|
||||||
|
|
||||||
|
class Scene:
|
||||||
|
def __init__(self):
|
||||||
|
self.types = {} # name -> created type
|
||||||
|
self.verts = {} # geometry name -> [(x,y,z), ...]
|
||||||
|
self.polys = {} # geometry name -> [[i, ...], ...]
|
||||||
|
self.material = {} # material name -> (r, g, b)
|
||||||
|
self.gg_material = {} # geogroup name -> material name
|
||||||
|
self.children = {} # parent name -> [child names]
|
||||||
|
self.view = None # (win l,b,r,t, win-dist, vw, vh, near, far)
|
||||||
|
self.background = (0, 0, 0)
|
||||||
|
self.frames = [] # camera (3x3 rotation rows, eye) per draw_scene
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct(msgs):
|
||||||
|
sc = Scene()
|
||||||
|
camera = None
|
||||||
|
geom_pend = None # (name, n_verts) awaiting vertex float records
|
||||||
|
conn_pend = None # (name, n_polys, loop_len) awaiting index records
|
||||||
|
for action, d in msgs:
|
||||||
|
if action == 1:
|
||||||
|
t, name = u32s(d)[:2]
|
||||||
|
sc.types[name] = t
|
||||||
|
elif action == 3:
|
||||||
|
w = u32s(d)
|
||||||
|
name, t = w[0], w[1]
|
||||||
|
if t == 11 and len(d) >= 92:
|
||||||
|
f = f32s(d)
|
||||||
|
sc.material[name] = tuple(f[12:15]) # diffuse RGB
|
||||||
|
elif t == 9 and len(d) >= 80:
|
||||||
|
sc.gg_material[name] = w[16]
|
||||||
|
elif t == 3 and len(d) >= 104:
|
||||||
|
f = f32s(d)
|
||||||
|
# window l,b,r,t; window-plane distance; viewport w,h;
|
||||||
|
# near, far; background rgb (last flush wins)
|
||||||
|
sc.view = (f[6], f[7], f[8], f[9], f[10],
|
||||||
|
f[11], f[12], f[13], f[14])
|
||||||
|
sc.background = tuple(f[15:18])
|
||||||
|
elif action == 11:
|
||||||
|
p, c = u32s(d)[:2]
|
||||||
|
sc.children.setdefault(p, []).append(c)
|
||||||
|
elif action == 23:
|
||||||
|
if geom_pend is None:
|
||||||
|
w = u32s(d)
|
||||||
|
geom_pend = (w[0], w[2])
|
||||||
|
sc.verts[w[0]] = []
|
||||||
|
else:
|
||||||
|
name, n = geom_pend
|
||||||
|
f = f32s(d)
|
||||||
|
vl = sc.verts[name]
|
||||||
|
vl.extend((f[i], f[i + 1], f[i + 2])
|
||||||
|
for i in range(0, len(f) - 2, 3))
|
||||||
|
if len(vl) >= n:
|
||||||
|
geom_pend = None
|
||||||
|
elif action == 25:
|
||||||
|
if conn_pend is None:
|
||||||
|
w = u32s(d)
|
||||||
|
conn_pend = (w[0], w[1], w[2])
|
||||||
|
sc.polys[w[0]] = []
|
||||||
|
else:
|
||||||
|
name, n_polys, loop = conn_pend
|
||||||
|
idx = u32s(d)
|
||||||
|
pl = sc.polys[name]
|
||||||
|
for i in range(0, len(idx), loop):
|
||||||
|
pl.append(idx[i:i + loop - 1]) # drop closing duplicate
|
||||||
|
if len(pl) >= n_polys:
|
||||||
|
conn_pend = None
|
||||||
|
elif action == 31:
|
||||||
|
f = f32s(d)
|
||||||
|
rot = [f[2:5], f[5:8], f[8:11]]
|
||||||
|
eye = f[11:14]
|
||||||
|
camera = (rot, eye)
|
||||||
|
elif action == 9:
|
||||||
|
sc.frames.append(camera)
|
||||||
|
return sc
|
||||||
|
|
||||||
|
|
||||||
|
def render(sc, frame, out, ss=2):
|
||||||
|
if sc.view:
|
||||||
|
wl, wb, wr, wt, wd, vw, vh, near, far = sc.view
|
||||||
|
vw, vh = int(vw), int(vh)
|
||||||
|
else:
|
||||||
|
wl, wb, wr, wt, wd = -1, -0.615, 1, 0.615, 1.3
|
||||||
|
vw, vh, near, far = 832, 512, 2, 12000
|
||||||
|
rot, eye = sc.frames[frame]
|
||||||
|
W, H = vw * ss, vh * ss
|
||||||
|
bg = tuple(max(0, min(255, int(c * 255 + 0.5))) for c in sc.background)
|
||||||
|
img = Image.new("RGB", (W, H), bg)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
def project(p):
|
||||||
|
x, y, z = (p[0] - eye[0], p[1] - eye[1], p[2] - eye[2])
|
||||||
|
ex = rot[0][0] * x + rot[0][1] * y + rot[0][2] * z
|
||||||
|
ey = rot[1][0] * x + rot[1][1] * y + rot[1][2] * z
|
||||||
|
ez = rot[2][0] * x + rot[2][1] * y + rot[2][2] * z
|
||||||
|
return ex, ey, ez
|
||||||
|
|
||||||
|
# painter's algorithm: gather polys with depth, far first
|
||||||
|
items = []
|
||||||
|
for gg, mat in sc.gg_material.items():
|
||||||
|
rgb = sc.material.get(mat, (1, 0, 1))
|
||||||
|
col = tuple(max(0, min(255, int(c * 255 + 0.5))) for c in rgb)
|
||||||
|
for geo in sc.children.get(gg, []):
|
||||||
|
vl = sc.verts.get(geo)
|
||||||
|
if not vl:
|
||||||
|
continue
|
||||||
|
evs = [project(v) for v in vl]
|
||||||
|
for poly in sc.polys.get(geo, []):
|
||||||
|
pts = [evs[i] for i in poly if i < len(evs)]
|
||||||
|
if len(pts) < 3:
|
||||||
|
continue
|
||||||
|
# view direction: Division looks down -Z in eye space
|
||||||
|
depth = sum(p[2] for p in pts) / len(pts)
|
||||||
|
if depth > -near:
|
||||||
|
continue # behind the camera
|
||||||
|
scr = []
|
||||||
|
for ex, ey, ez in pts:
|
||||||
|
# Division screen x runs opposite to GL eye x: the SMPTE
|
||||||
|
# pattern (gray leftmost, -I/white/+Q PLUGE) comes out
|
||||||
|
# mirrored without the negation.
|
||||||
|
ndc_x = (-ex * wd / -ez - wl) / (wr - wl)
|
||||||
|
ndc_y = (ey * wd / -ez - wb) / (wt - wb)
|
||||||
|
scr.append((ndc_x * W, (1 - ndc_y) * H))
|
||||||
|
items.append((depth, scr, col))
|
||||||
|
items.sort(key=lambda it: it[0])
|
||||||
|
for _, scr, col in items:
|
||||||
|
draw.polygon(scr, fill=col)
|
||||||
|
|
||||||
|
if ss > 1:
|
||||||
|
img = img.resize((vw, vh), Image.LANCZOS)
|
||||||
|
img.save(out)
|
||||||
|
print(f"rendered frame {frame}: {len(items)} polys -> {out} ({vw}x{vh})")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = sys.argv[1]
|
||||||
|
out = "capture.png"
|
||||||
|
frame = 0
|
||||||
|
if "-o" in sys.argv:
|
||||||
|
out = sys.argv[sys.argv.index("-o") + 1]
|
||||||
|
if "--frame" in sys.argv:
|
||||||
|
frame = int(sys.argv[sys.argv.index("--frame") + 1])
|
||||||
|
msgs = read_messages(path)
|
||||||
|
sc = reconstruct(msgs)
|
||||||
|
if "--eye" in sys.argv: # override camera to survey the whole scene
|
||||||
|
eye = [float(v) for v in sys.argv[sys.argv.index("--eye") + 1].split(",")]
|
||||||
|
sc.frames = [([[1, 0, 0], [0, 1, 0], [0, 0, 1]], eye)]
|
||||||
|
frame = 0
|
||||||
|
print(f"{len(msgs)} messages: {len(sc.verts)} geometries, "
|
||||||
|
f"{len(sc.material)} materials, {len(sc.frames)} frames, view={sc.view}")
|
||||||
|
render(sc, frame, out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -129,6 +129,40 @@ static bool frame_outstanding = false; /* draw_scene sent, frame-ack owed
|
|||||||
|
|
||||||
static void fifo_arm_action(void) { fifo_arm = true; fifo_cap_pos = 0; fifo_cap = 0; }
|
static void fifo_arm_action(void) { fifo_arm = true; fifo_cap_pos = 0; fifo_cap = 0; }
|
||||||
|
|
||||||
|
/* ---- Phase 3: full FIFO message dump (VPX_FIFODUMP=<path>) -------------- *
|
||||||
|
* The FIFO wire format (VR_COMMS.C velocirender_transmit + OUTSW.ASM): each
|
||||||
|
* transmit is TWO tag-delimited bursts. The C caller fires the first 3 bytes
|
||||||
|
* of the protocol word at outputData, outsw() sends 0x40 as its final byte
|
||||||
|
* and REP OUTSWs the payload into the FIFO port. Burst 1 carries the 4-byte
|
||||||
|
* action, burst 2 the data (protocol word (0xff<<16)|(data+4)). So recording
|
||||||
|
* every FIFO byte between outputData writes yields alternating action/data
|
||||||
|
* records that a decoder can pair back into [action][data] messages.
|
||||||
|
* Record format: 'VPXM' u32 magic, u32 length LE, then the raw burst bytes. */
|
||||||
|
static FILE *fifo_dump_fp = NULL;
|
||||||
|
static unsigned char *fifo_buf = NULL;
|
||||||
|
static size_t fifo_buf_len = 0, fifo_buf_cap = 0;
|
||||||
|
|
||||||
|
static void fifo_buf_push(unsigned char v) {
|
||||||
|
if (fifo_dump_fp == NULL) return;
|
||||||
|
if (fifo_buf_len == fifo_buf_cap) {
|
||||||
|
size_t ncap = fifo_buf_cap ? fifo_buf_cap * 2 : 4096;
|
||||||
|
unsigned char *nb = (unsigned char *)realloc(fifo_buf, ncap);
|
||||||
|
if (nb == NULL) return; /* drop byte rather than crash */
|
||||||
|
fifo_buf = nb; fifo_buf_cap = ncap;
|
||||||
|
}
|
||||||
|
fifo_buf[fifo_buf_len++] = v;
|
||||||
|
}
|
||||||
|
static void fifo_flush_record(void) {
|
||||||
|
if (fifo_dump_fp == NULL || fifo_buf_len == 0) return;
|
||||||
|
unsigned char hdr[8] = { 'V','P','X','M',
|
||||||
|
(unsigned char)(fifo_buf_len), (unsigned char)(fifo_buf_len >> 8),
|
||||||
|
(unsigned char)(fifo_buf_len >> 16), (unsigned char)(fifo_buf_len >> 24) };
|
||||||
|
fwrite(hdr, 1, sizeof hdr, fifo_dump_fp);
|
||||||
|
fwrite(fifo_buf, 1, fifo_buf_len, fifo_dump_fp);
|
||||||
|
fflush(fifo_dump_fp);
|
||||||
|
fifo_buf_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void parse_fifo_byte(unsigned char v) {
|
static void parse_fifo_byte(unsigned char v) {
|
||||||
if (!fifo_arm) return;
|
if (!fifo_arm) return;
|
||||||
fifo_cap |= ((unsigned)v) << (fifo_cap_pos * 8);
|
fifo_cap |= ((unsigned)v) << (fifo_cap_pos * 8);
|
||||||
@@ -175,6 +209,7 @@ static int vpx_max_postboot_acks = 200; /* safety cap */
|
|||||||
static int empty_polls = 0;
|
static int empty_polls = 0;
|
||||||
static const int POLL_THRESHOLD = 6; /* consecutive empty polls => blocking receive */
|
static const int POLL_THRESHOLD = 6; /* consecutive empty polls => blocking receive */
|
||||||
static void queue_render_ack_node(unsigned char action, unsigned node) {
|
static void queue_render_ack_node(unsigned char action, unsigned node) {
|
||||||
|
fifo_flush_record(); /* a receive means the outstanding burst is complete */
|
||||||
unsigned char m[12] = {
|
unsigned char m[12] = {
|
||||||
0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */
|
0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */
|
||||||
action, 0x00, 0x00, 0x00, /* payload[0..3] = action (LE) */
|
action, 0x00, 0x00, 0x00, /* payload[0..3] = action (LE) */
|
||||||
@@ -342,13 +377,16 @@ static void vpx_write(Bitu port, Bitu val, Bitu iolen) {
|
|||||||
wf_payload_left = -1; wf_action_pos = 0; wf_action = 0; last_action = 0;
|
wf_payload_left = -1; wf_action_pos = 0; wf_action = 0; last_action = 0;
|
||||||
fifo_arm = false; fifo_cap_pos = 0; fifo_cap = 0;
|
fifo_arm = false; fifo_cap_pos = 0; fifo_cap = 0;
|
||||||
expect_sync_token = false; sync_pending = false; sync_token = 0; frame_outstanding = false;
|
expect_sync_token = false; sync_pending = false; sync_token = 0; frame_outstanding = false;
|
||||||
|
fifo_flush_record();
|
||||||
note("board reset");
|
note("board reset");
|
||||||
} else if (off == 1) {
|
} else if (off == 1) {
|
||||||
saw_write = true; /* outputData: a download/response byte */
|
saw_write = true; /* outputData: a download/response byte */
|
||||||
empty_polls = 0; /* a write means the game isn't blocking-reading */
|
empty_polls = 0; /* a write means the game isn't blocking-reading */
|
||||||
|
fifo_flush_record(); /* outputData write = FIFO burst boundary */
|
||||||
if (parse_frames) { parse_out_byte((unsigned char)val); fifo_arm_action(); }
|
if (parse_frames) { parse_out_byte((unsigned char)val); fifo_arm_action(); }
|
||||||
} else if (off == 4 || off == 5) {
|
} else if (off == 4 || off == 5) {
|
||||||
/* FIFO data port (link B): payload words, low/high bytes. */
|
/* FIFO data port (link B): payload words, low/high bytes. */
|
||||||
|
fifo_buf_push((unsigned char)val);
|
||||||
if (parse_frames) parse_fifo_byte((unsigned char)val);
|
if (parse_frames) parse_fifo_byte((unsigned char)val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,6 +405,12 @@ void VPXLOG_Init(void) {
|
|||||||
const char *h = getenv("VPX_HANDSHAKES");
|
const char *h = getenv("VPX_HANDSHAKES");
|
||||||
if (h && atoi(h) > 0) vpx_max_handshakes = atoi(h);
|
if (h && atoi(h) > 0) vpx_max_handshakes = atoi(h);
|
||||||
|
|
||||||
|
const char *fd = getenv("VPX_FIFODUMP");
|
||||||
|
if (fd && fd[0]) {
|
||||||
|
fifo_dump_fp = fopen(fd, "wb");
|
||||||
|
if (fifo_dump_fp == NULL) LOG_MSG("VPXLOG: cannot open fifodump '%s'", fd);
|
||||||
|
}
|
||||||
|
|
||||||
IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18);
|
IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18);
|
||||||
IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18);
|
IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user