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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-19 14:23:29 -05:00
co-authored by Claude Opus 4.8
parent cff783d2ad
commit ff91bc675f
2 changed files with 159 additions and 0 deletions
@@ -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"},
+116
View File
@@ -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)