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:
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user