From ff91bc675fd856cb28079cd55d34c80d9a38934b Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 19 Jul 2026 14:23:29 -0500 Subject: [PATCH] M4b: offline end-to-end frames -- firmware -> C core -> GPU, live emu860c gains a C-side write-watch (watch_add/watch_drain: watched ranges log (addr,val) into a ring buffer at negligible cost). frames.py runs the cap7 mission on the C core, drains the DMA queue-page writes at each receive->receive draw span, reconstructs the frame's SEND payloads from live C memory, dispatches the GPU tile shader per TILE entry, and accumulates interlaced fields. Result: the complete SMPTE test card (frame_0004) -- 6 draw frames in 2.0s including boot; the content draws alternate 25-tile fields with eof/present passes, matching the hardware cadence. Co-Authored-By: Claude Opus 4.8 --- emulator/firmware-decomp/emu860c/emu860c.c | 43 ++++++++ emulator/firmware-decomp/emu860c/frames.py | 116 +++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 emulator/firmware-decomp/emu860c/frames.py diff --git a/emulator/firmware-decomp/emu860c/emu860c.c b/emulator/firmware-decomp/emu860c/emu860c.c index cf0c4c03..4a42f523 100644 --- a/emulator/firmware-decomp/emu860c/emu860c.c +++ b/emulator/firmware-decomp/emu860c/emu860c.c @@ -56,8 +56,29 @@ static uint32_t mem_r32(uint32_t addr) { return v; } +/* ---- write-watch (M4b): ranges + append log ---- */ +static uint32_t g_watch[8][2]; +static int g_nwatch; +static uint32_t *g_wbuf; +static size_t g_wlen, g_wcap; + +static inline void watch_note(uint32_t addr, uint32_t val) { + for (int i = 0; i < g_nwatch; i++) { + if (addr >= g_watch[i][0] && addr < g_watch[i][1]) { + if (g_wlen + 2 > g_wcap) { + g_wcap = g_wcap ? g_wcap * 2 : 65536; + g_wbuf = (uint32_t *)realloc(g_wbuf, g_wcap * 4); + } + g_wbuf[g_wlen++] = addr; + g_wbuf[g_wlen++] = val; + return; + } + } +} + static void mem_w32(uint32_t addr, uint32_t val) { uint32_t off = addr & (PAGE_SIZE - 1); + if (g_nwatch) watch_note(addr, val); uint8_t *p = page_for(addr, 1); if (off <= PAGE_SIZE - 4) { memcpy(p + off, &val, 4); @@ -682,6 +703,26 @@ static PyObject *py_run(PyObject *self, PyObject *args) { return Py_BuildValue("iK", reason, (unsigned long long)C.steps); } +static PyObject *py_watch_add(PyObject *self, PyObject *args) { + (void)self; + unsigned long lo, hi; + if (!PyArg_ParseTuple(args, "kk", &lo, &hi)) return NULL; + if (g_nwatch < 8) { + g_watch[g_nwatch][0] = (uint32_t)lo; + g_watch[g_nwatch][1] = (uint32_t)hi; + g_nwatch++; + } + Py_RETURN_NONE; +} + +static PyObject *py_watch_drain(PyObject *self, PyObject *args) { + (void)self; (void)args; + PyObject *b = PyBytes_FromStringAndSize((const char *)g_wbuf, + (Py_ssize_t)(g_wlen * 4)); + g_wlen = 0; + return b; +} + static PyObject *py_step1(PyObject *self, PyObject *args) { (void)self; (void)args; int fb = 0; @@ -747,6 +788,8 @@ static PyMethodDef methods[] = { {"run", py_run, METH_VARARGS, "run until hook/stop/fallback/max"}, {"getstate", py_getstate, METH_NOARGS, "dump state"}, {"step1", py_step1, METH_NOARGS, "force one step (ignores hooks)"}, + {"watch_add", py_watch_add, METH_VARARGS, "watch writes in [lo,hi)"}, + {"watch_drain", py_watch_drain, METH_NOARGS, "drain the write log as bytes"}, {"setreg", py_setreg, METH_VARARGS, "set r/f register"}, {"setpc", py_setpc, METH_VARARGS, "set pc"}, {"set_sentinel", py_setsentinel, METH_VARARGS, "set return sentinel"}, diff --git a/emulator/firmware-decomp/emu860c/frames.py b/emulator/firmware-decomp/emu860c/frames.py new file mode 100644 index 00000000..88bf8535 --- /dev/null +++ b/emulator/firmware-decomp/emu860c/frames.py @@ -0,0 +1,116 @@ +"""M4b: offline end-to-end frames -- run cap7 through the C core with a +write-watch on the DMA queue pages; at each draw boundary, reconstruct the +frame's DMA stream + payloads and render the tiles on the GPU. Writes +frame_NNNN.png for the first N draws.""" +import sys, os, time, struct +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.dirname(HERE)) +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +import emu860, emu_main, emu860c +import igc_exec, igc_gpu +import numpy as np +from PIL import Image +from driver import boot, CpuShim +from vrboard import A +ANAME = {int(a): a.name for a in A} + +emu860.Mem.log = lambda self, *a, **k: None +NFRAMES = int(sys.argv[1]) if len(sys.argv) > 1 else 10 +W, H = 832, 512 + +r = boot() +shim = CpuShim() +# watch the DMA queue-page region (the coeff-copy writes land here) +emu860c.watch_add(0x08020000, 0x08190000) +g = igc_gpu.GpuTile() +print("GPU:", g.ctx.info['GL_RENDERER'], flush=True) + +frames_done = 0 +prev_was_draw = [False] +t0 = time.time() +texu_acc = np.zeros((H, W), np.int32) # persistent across draws (fields) + + +def render_frame(nth): + raw = emu860c.watch_drain() + words = np.frombuffer(raw, dtype=np.uint32).reshape(-1, 2) + vals = words[:, 1] + # DMA pairs: consecutive (addr, op) values in write order + tiles = {} + i = 0 + n = len(vals) + while i + 1 < n: + a, op = int(vals[i]), int(vals[i + 1]) + c = (op >> 28) & 0xf + if c in (1, 9) and 0x08000000 <= a < 0x08020000: + sz = op & 0x7f + payload = [emu860c.r32(a + 4 * k) for k in range(sz)] + tiles.setdefault('sends', []).append(payload) + elif c == 2: + tid = a + tiles.setdefault('order', []).append((tid, len(tiles.get('sends', [])))) + i += 2 + sends = tiles.get('sends', []) + order = tiles.get('order', []) + if not order: + return False + texu = texu_acc + prev_cut = 0 + ntiles = 0 + for tid, cut in order: + ox = (tid & 0x1f) * 64 + oy = ((tid >> 5) & 0x1f) * 128 + instrs = [] + for pl in sends[prev_cut:cut]: + ins, _ = igc_exec.parse(pl) + instrs += ins + prev_cut = cut + if ox >= W or oy >= H or not instrs: + continue + data = g.run(instrs, ox=ox, oy=oy, pre_seed_texz_x=True) + for py_ in range(igc_gpu.TILE_H): + gy = oy + py_ + if gy >= H: + break + row = data[py_ * igc_gpu.TILE_W:(py_ + 1) * igc_gpu.TILE_W] + for px in range(igc_gpu.TILE_W): + gx = ox + px + if gx >= W: + break + texu[gy, gx] = igc_gpu.GpuTile.rdbits(row[px], 57, 20) + ntiles += 1 + PAL = [(180, 180, 180), (180, 180, 16), (16, 180, 180), (16, 180, 16), + (180, 16, 180), (180, 16, 16), (16, 16, 180)] + rgb = np.zeros((H, W, 3), np.uint8) + for gx in range(W): + u = int(np.median(texu[:, gx])) + rgb[:, gx] = (0, 0, 0) if u < 48 else PAL[min(6, (u - 48) * 7 // (W - 48))] + Image.fromarray(rgb, 'RGB').save(os.path.join(HERE, 'frame_%04d.png' % nth)) + print("frame %d: %d tiles, %d sends" % (nth, ntiles, len(sends)), flush=True) + return True + + +while frames_done < NFRAMES: + reason, steps = emu860c.run(2_000_000_000) + if reason != 0: + print("reason %d, stopping" % reason, flush=True) + break + pc = emu860c.getstate()['pc'] + h = r.hooks.get(pc) + if h is None: + break + # draw boundaries are receive->receive spans: render only when we are + # BACK at h_receive after a draw_scene delivery (igcwait etc. fire mid-draw) + if pc == 0xf04024c0: # h_receive + if prev_was_draw[0]: + if render_frame(frames_done): + frames_done += 1 + prev_was_draw[0] = False + nxt = r.qi + if nxt < len(r.queue) and ANAME.get(r.queue[nxt][0]) == 'draw_scene': + emu860c.watch_drain() # discard pre-draw noise + prev_was_draw[0] = True + if h(shim) == 'done': + break +print("done: %d frames in %.1fs (cmd %d)" % (frames_done, time.time() - t0, r.qi), flush=True)