diff --git a/emulator/.gitignore b/emulator/.gitignore index bbd95bf..93487a6 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -2,7 +2,12 @@ dosbox-x/ image/ *.png +# Phase 3 evidence renders (keep: decoded from the captured VPX stream) +!divrgb-decoded.png +!divrgb-frame0.png dbx_out.txt +vpx*.txt +sweep_*.txt dbx_err.txt src/ vpxlog.txt diff --git a/emulator/PHASE3-PROGRESS.md b/emulator/PHASE3-PROGRESS.md new file mode 100644 index 0000000..9bb7198 --- /dev/null +++ b/emulator/PHASE3-PROGRESS.md @@ -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.** + +![decoded DIVRGB calibration screen](divrgb-decoded.png) + +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=` 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 [--hex N] [--action A]` — action census + payload + hexdumps. +- `render_capture.py [-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). diff --git a/emulator/RENDER-HARNESS.md b/emulator/RENDER-HARNESS.md index 82384f4..eec114c 100644 --- a/emulator/RENDER-HARNESS.md +++ b/emulator/RENDER-HARNESS.md @@ -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). - `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 / -material / texture commands already flow over the FIFO as framed messages -(`vr_create`, `vr_set_geom_verts` (22), `vr_set_texmap_texels` (23), -`vr_list_add`, `vr_draw_scene`). Phase 3 decodes those payloads — the same -DIV-BIZ2 vertex/material formats already implemented in -`restoration/divformats.py` — into an OpenGL backend and presents to a window, -turning the emulated board's frame into actual pixels. +`flyk DIVRGB.SCN` was captured with `VPX_FIFODUMP` (`divrgb.conf` → +`divrgb.fifodump`), the full Rel 4.10 FIFO message set was decoded +(`decode_fifodump.py`), and the captured frame was reconstructed to pixels +(`render_capture.py` → `divrgb-decoded.png`): the textbook SMPTE color-bar +pattern, drawn entirely from the wire protocol. Phase 3b is the live OpenGL +backend inside DOSBox-X. diff --git a/emulator/decode_fifodump.py b/emulator/decode_fifodump.py new file mode 100644 index 0000000..ef9e734 --- /dev/null +++ b/emulator/decode_fifodump.py @@ -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 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 [--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(" 4: + msgs.append((struct.unpack("= 4: + msgs.append((struct.unpack("" + 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() diff --git a/emulator/divrgb-decoded.png b/emulator/divrgb-decoded.png new file mode 100644 index 0000000..0a61589 Binary files /dev/null and b/emulator/divrgb-decoded.png differ diff --git a/emulator/divrgb-frame0.png b/emulator/divrgb-frame0.png new file mode 100644 index 0000000..31c9cb3 Binary files /dev/null and b/emulator/divrgb-frame0.png differ diff --git a/emulator/divrgb.conf b/emulator/divrgb.conf new file mode 100644 index 0000000..0e7e9cf --- /dev/null +++ b/emulator/divrgb.conf @@ -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 diff --git a/emulator/divrgb.fifodump b/emulator/divrgb.fifodump new file mode 100644 index 0000000..381cb03 Binary files /dev/null and b/emulator/divrgb.fifodump differ diff --git a/emulator/rebuild.sh b/emulator/rebuild.sh new file mode 100644 index 0000000..80d56ce --- /dev/null +++ b/emulator/rebuild.sh @@ -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 diff --git a/emulator/render_capture.py b/emulator/render_capture.py new file mode 100644 index 0000000..331a543 --- /dev/null +++ b/emulator/render_capture.py @@ -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 [-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("= 4: + msgs.append((struct.unpack(" 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() diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 1a3e052..2cd1d1f 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -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; } +/* ---- Phase 3: full FIFO message dump (VPX_FIFODUMP=) -------------- * + * 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) { if (!fifo_arm) return; 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 const int POLL_THRESHOLD = 6; /* consecutive empty polls => blocking receive */ 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] = { 0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */ 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; fifo_arm = false; fifo_cap_pos = 0; fifo_cap = 0; expect_sync_token = false; sync_pending = false; sync_token = 0; frame_outstanding = false; + fifo_flush_record(); note("board reset"); } else if (off == 1) { saw_write = true; /* outputData: a download/response byte */ 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(); } } else if (off == 4 || off == 5) { /* 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); } } @@ -367,6 +405,12 @@ void VPXLOG_Init(void) { const char *h = getenv("VPX_HANDSHAKES"); 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_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18);