udpstream: alternating-frame flash (30.7 fps sustained) + DRAM store benchmark + ROM cross-check

cmd 8 alternates 0xFFFF/0x0000 per frame so the rate is visible: 300 frames
in 9.775 s on the unit, 32.6 ms each, 30.69 fps sustained.

cmd 9 is the same byte-store loop aimed at a DRAM buffer, to measure the CPU's
store rate without the VRAM arbiter and size a frame-expansion stage.
vtcmd.py --dram selects it.

Report 74.3 records the independent Ghidra pass over the ROM: the firmware
sets CONVSP/CONVDP=0x14 (pitch 0x800 bits = 256 B), PSIZE=1, PMASK=0 and
OFFSET=0x04000000 for XY addressing -- the same values vram.asm uses -- and
fills with FILL_L chunked at 0x7FC0 pixels because DYDX is 15 bits per axis.
There is no VBLT and no wait-state/arbitration register in the ROM, so the
12 us/byte CPU store cost is inherent to this board, not a setting.
This commit is contained in:
arti committed 2026-10-30 17:34:47 +02:00
1 parent d99a9cdb6c
commit fb095eb778
4 files changed
+82 -5

No files matched your search

+38
View File
@@ -4983,3 +4983,41 @@ So a full-screen write costs ~33 ms, and the frame budget for video becomes:
`cmd 8` (`vtcmd.py vram --fill`) is the benchmark; `cmd 7` is the byte-store
control. Both report what the payload counted, not what it was asked to do.
### 74.3 The ROM does the same thing (Ghidra cross-check)
A static pass over the ROM found the firmware using exactly these instructions,
with values that match what `vram.asm` sets:
| ROM site | what it does | matches |
|---|---|---|
| `rom:07e40a72` | `MOVI 0xc0000000,A1` then copies 11 words into the CRTC block, clears CONTROL bits 10-14 (raster op = replace), then copies 5 words into `0xC0000130..0x180` = CONVSP, CONVDP, PSIZE, PMASK | our CONTROL/PSIZE/PMASK setup |
| `rom:07e40b7c` (profile table) | CONVSP=`0x14`, CONVDP=`0x14`, PSIZE=`0x1`, PMASK=`0x0`; the 34010 decodes `0x14` as `1 << (~0x14 & 0x1f)` = `0x800` bits | our `DPTCH = 0x800` (256-byte stride) |
| `rom:07ee9db6`, `rom:07f6628a` | `MOVI 0x4000000,B4` — B4 is **OFFSET**, the XY-mode framebuffer base | VRAM base `0x04000000` |
| `rom:07f68996`, `rom:07f689c0` | `FILL_L`, chunked: one call for `<= 0x7FC0` pixels, then `MOVI 0x17fc0,B7` for the rest | our single `FILL L` (1280x1024 fits one call) |
| `rom:07f688d8`, `rom:07f6890a` | rectangle copy = `PIXBLT_L_L`, same `0x7FC0` chunking | the planned frame blit |
| `rom:07f66350`, `rom:07f66372` | glyph/text blit = `PIXBLT_B_XY` (packed 1-bpp source, raster op `0x0B` then `0x00`) | the planned expand path |
Two negative results worth keeping: there is **no `VBLT`** anywhere in the ROM
(so "video" is not a DMA engine), and there is **no wait-state or CPU/VRAM
arbitration register** — the 34010's I/O file is only 32 words and the firmware
touches none of them for this. The 12 µs/byte is therefore inherent to CPU pixel
stores on this board, not a misconfiguration we could tune.
The chunking matters for our own code: `DYDX` is 15 bits per axis, so a fill or
blit larger than `0x7FC0` pixels in a row must be split, exactly as the firmware
does.
### 74.4 A visible check: 300 alternating frames
Filling white alone proves nothing on a display. `cmd 8` now alternates
`0xFFFF`/`0x0000` per frame, and 300 frames were run on the unit:
```
filled 300 frame(s) x 1024 rows (FILL L): 24576000 stores in 9.775 s
32.6 ms per frame -> 30.69 fps ceiling
```
9.8 s of full-screen black/white flash at ~31 fps — the operator's own
description was "damn that's fast". It also shows the number is sustained, not a
one-frame burst.
Binary file not shown.
+36 -3
View File
@@ -505,16 +505,49 @@ int main()
}
wb(TXBUF + 19 * 8, (wlen >> 8) & 0xFF);
wb(TXBUF + 20 * 8, wlen & 0xFF);
} else if (cmd == 9) {
/* The same byte-store loop as cmd 7 but into DRAM, so the
* CPU's store rate is measured without the VRAM arbiter.
* This is the number that sizes a frame-expansion stage. */
unsigned int nd, fd, yd, xd, od, rd, st;
nd = rlen;
if (nd > 100) {
nd = 100;
}
rd = addr;
if (rd == 0 || rd > 1024) {
rd = 1024;
}
st = 0;
for (fd = 0; fd < nd; fd++) {
for (yd = 0; yd < rd; yd++) {
od = 0x01A10000 + yd * 2048;
for (xd = 0; xd < 160; xd++) {
*(unsigned char *)(od + xd * 8) =
(unsigned char)(yd + xd + fd);
st = st + 1;
}
}
}
wb(TXBUF + 19 * 8, (nd >> 8) & 0xFF);
wb(TXBUF + 20 * 8, nd & 0xFF);
wb(TXBUF + 21 * 8, (rd >> 8) & 0xFF);
wb(TXBUF + 22 * 8, rd & 0xFF);
wb(TXBUF + 23 * 8, (st >> 24) & 0xFF);
wb(TXBUF + 24 * 8, (st >> 16) & 0xFF);
wb(TXBUF + 25 * 8, (st >> 8) & 0xFF);
wb(TXBUF + 26 * 8, st & 0xFF);
} else if (cmd == 8) {
/* FILL L benchmark: the same full frame, done by the GSP's
* array engine instead of byte stores. */
unsigned int nf2, f2;
nf2 = rlen;
if (nf2 > 100) {
nf2 = 100;
if (nf2 > 400) { /* 400 x 32.7 ms = 13 s max */
nf2 = 400;
}
for (f2 = 0; f2 < nf2; f2++) {
vram_pattern = 0xFFFF;
/* alternate black/white so the rate is visible */
vram_pattern = (f2 & 1) ? 0x0000 : 0xFFFF;
vram_fill_();
}
wb(TXBUF + 19 * 8, (nf2 >> 8) & 0xFF);
+8 -2
View File
@@ -166,6 +166,9 @@ def main():
help="vram: how many frames to fill")
ap.add_argument("--rows", type=int, default=1024,
help="vram: how many of the 1024 rows to write")
ap.add_argument("--dram", action="store_true",
help="vram: same byte-store loop but into DRAM (no VRAM "
"arbiter) -- sizes the frame-expansion stage")
ap.add_argument("--fill", action="store_true",
help="vram: use the hardware FILL L instruction instead of "
"byte stores")
@@ -193,7 +196,7 @@ def main():
if a.cmd == "vram":
n = a.frames
opcode = 8 if a.fill else 7
opcode = 8 if a.fill else (9 if a.dram else 7)
t0 = time.time()
status, data = request(sock, a.iface, opcode, a.rows, n,
timeout=max(120.0, a.timeout))
@@ -206,12 +209,15 @@ def main():
nf, x = struct.unpack(">HH", data[:4])
rows = 1024
stores = nf * 1024 * 80 # destination words, not bytes
elif a.dram:
nf, rows, stores = struct.unpack(">HHI", data[:8])
else:
nf, rows, stores = struct.unpack(">HHI", data[:8])
per = dt / nf if nf else 0
rate = stores / dt if dt else 0
print("filled %d frame(s) x %d rows (%s): %d stores in %.3f s"
% (nf, rows, "FILL L" if a.fill else "byte stores",
% (nf, rows, "FILL L" if a.fill else
("DRAM stores" if a.dram else "byte stores"),
stores, dt))
print(" %.1f ms per frame -> %.2f fps ceiling"
% (per * 1000.0, 1.0 / per if per else 0))