diff --git a/emulator/firmware-decomp/GPU-RETARGET.md b/emulator/firmware-decomp/GPU-RETARGET.md new file mode 100644 index 00000000..8dd04713 --- /dev/null +++ b/emulator/firmware-decomp/GPU-RETARGET.md @@ -0,0 +1,70 @@ +# GPU-compute retarget — the DOSBox-renderer path (2026-07-19) + +Companion to FPGA-RECONSTRUCTION.md. This is the plan for shipping the decoded +IGC/EMC model as a real-time renderer behind the existing DOSBox seam — the +practical answer to "use this as the renderer for the pod." + +## Why GPU compute +The EMC array is 8,192 bit-serial PEs per 64×128 tile. On a GPU that is one +thread per pixel executing the same decoded op stream — a literal 1:1 mapping, +no hardware, ships as software next to the renderer we already deploy. +The original array ran ~40 MHz bit-serial; any integrated GPU exceeds the +whole frame's work by orders of magnitude. + +## Architecture + +``` +DOSBox (game, unmodified) + │ wire protocol (known) ── existing seam: render-bridge/live_bridge + ▼ +emu860 geometry stage ── the i860 firmware, C/JIT port for realtime + │ per-draw DMA stream + payloads (the coefficient programs) + ▼ +IGC translator (host) ── igc_exec.parse -> flat op records + │ SSBO: op stream + payload floats + tile list + ▼ +compute shader "EMC tile" ── pixel = thread; fields in registers/LDS: + │ texz/texu/texv/zbuf as uint20, r/g/b as uint8, enable as bool; + │ ops = a switch over the named table (TREE*, MEM*, CPY, sweeps, SCA*) + ▼ +readout pass ── ramp(texu) / rgb24 -> RGBA8 frame texture + ▼ +present (the existing renderer window / vr_readpixels reply) +``` + +## What ports 1:1 from the golden model +- The op table (data) and parser — host-side, unchanged. +- Tile semantics — each `Tile.run` branch becomes a shader case; the pixel + memory bit-fields become packed uints (no bit-serial emulation needed: + field-level semantics are already proven equivalent by the conformance + suite). +- Readout — ramp LUT as a small texture. + +## Conformance gate +`igc_conformance.py` (C1-C4, committed fixtures) is the acceptance bar: the +shader path must reproduce C2-C4 outputs exactly (C1 is host-side parse). +Add a C5 full-frame image hash (bars + fxtest frames) once texel sampling +lands. + +## Realtime budget (per frame) +- 52 tiles × ~100-3000 op-steps × 8,192 px ≈ 1-10 G pixel-ops worst case — + a compute-shader trifle at 60 fps on integrated graphics. +- Geometry stage: the real wall. emu860 in Python ≈ 600K steps/s; a frame is + ~10-50M steps → need the C/JIT step-core (planned; the i860 ISA semantics + are fully pinned by emu860 + MAME cross-validation). Interim: pre-captured + streams replay at full speed for validation. + +## Milestones +1. **M1 shader tile**: WGPU/GL compute kernel executing the named ops; + passes C2-C4. (small) +2. **M2 frame loop**: DMA-stream walker + tile dispatch + readout; renders + the bars + fxtest fixtures. (small-medium) +3. **M3 texel path**: TXDN stream -> texture sampling (needs the remaining + spec work — the io-port fetch protocol). (medium, spec-gated) +4. **M4 live seam**: behind live_bridge in place of / beside the GL renderer; + real-time gate = the C step-core for emu860. (the big one, independent) + +## Open spec items feeding this (tracked in IGC-ENCODING-DERIVATION.md) +- op-0x2c edge exact semantics (winding rule) — affects M1 fidelity. +- TXDN io-port texel stream — gates M3. +- The texz coordinate-seed op + the real ramp tables — purity for M2 images. diff --git a/emulator/firmware-decomp/conformance/cap7_wide30.pkl b/emulator/firmware-decomp/conformance/cap7_wide30.pkl new file mode 100644 index 00000000..a8daa6a4 Binary files /dev/null and b/emulator/firmware-decomp/conformance/cap7_wide30.pkl differ diff --git a/emulator/firmware-decomp/conformance/fx_program.pkl b/emulator/firmware-decomp/conformance/fx_program.pkl new file mode 100644 index 00000000..237502a8 Binary files /dev/null and b/emulator/firmware-decomp/conformance/fx_program.pkl differ diff --git a/emulator/firmware-decomp/conformance/sends_cap7.pkl b/emulator/firmware-decomp/conformance/sends_cap7.pkl new file mode 100644 index 00000000..e583103c Binary files /dev/null and b/emulator/firmware-decomp/conformance/sends_cap7.pkl differ diff --git a/emulator/firmware-decomp/igc_conformance.py b/emulator/firmware-decomp/igc_conformance.py new file mode 100644 index 00000000..fa7ac108 --- /dev/null +++ b/emulator/firmware-decomp/igc_conformance.py @@ -0,0 +1,136 @@ +"""igc_conformance.py -- the golden-model conformance suite. + +Any port of the IGC/EMC model (RTL, GPU compute, C) must reproduce these +outputs bit-for-bit (where pinned) from the committed fixtures in +conformance/. The fixtures are REAL captured data from the firmware running +on emu860 (see IGC-ENCODING-DERIVATION.md for provenance). + +Cases: + C1 DUMP reference packet parses to the exact named instruction sequence. + C2 Synthetic triangle rasterizes to the exact lit-pixel count. + C3 Bars pipeline: the cap7 bench per-tile programs (sends_cap7.pkl), with + the documented texz seed, compute texu == global x (+2) per pixel. + C4 fxtest effect primitives (fx_program.pkl) rasterize: 9 primitives drawn, + pinned z-buffer coverage + centroid. +Run: python igc_conformance.py (exit 0 = conformant) +""" +import os, sys, pickle, struct + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import igc_exec + +HERE = os.path.dirname(os.path.abspath(__file__)) +FIX = os.path.join(HERE, 'conformance') + + +def f32(w): + return struct.unpack('> 8) & 0xff) == 0x2c] + assert len(setups) == 10, len(setups) + # rasterize coarsely on a 104x64 grid (1/8 scale) and pin coverage + W, H = 104, 64 + drawn = 0 + covered = 0 + zwins = 0 + zbuf = [[-1e30] * W for _ in range(H)] + for a in setups: + edges = [] + p = a + 4 + while ((rd(p) >> 8) & 0xff) == 0x2c and len(edges) < 6: + edges.append((f32(rd(p+4)), f32(rd(p+8)), f32(rd(p+12)))) + p += 16 + zp = None + q = p + for _ in range(40): + w = rd(q) + if ((w >> 8) & 0xff) == 0x21: + zp = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + break + q += 4 + if zp is None: + continue + any_px = False + for gy in range(H): + for gx in range(W): + x, y = gx * 8, gy * 8 + vs = [A * x + B * y + C for A, B, C in edges] + inside = all(v >= 0 for v in vs) or all(v < 0 for v in vs) + if not inside: + continue + any_px = True + covered += 1 + z = zp[0] * x + zp[1] * y + zp[2] + if z > zbuf[gy][gx]: + zbuf[gy][gx] = z + zwins += 1 + if any_px: + drawn += 1 + assert drawn == 10, drawn + assert covered == 5706, covered + assert zwins == 5119, zwins + return 'C4 fxtest primitives: PASS (10 drawn, coverage %d, zwins %d)' % (covered, zwins) + + +if __name__ == '__main__': + results = [] + for case in (c1_dump_reference, c2_synthetic_triangle, c3_bars_pipeline, + c4_fxtest_primitives): + results.append(case()) + print('\n'.join(results)) + print('CONFORMANT')