From fb095eb7781d74729dec280dadf3571bec3abcf2 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 30 Oct 2026 17:34:47 +0200 Subject: [PATCH] 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. --- harness/mame/NETWORK_REPORT.md | 38 ++++++++++++++++++++++++ harness/udpstream/build/lance_cmd.coff | Bin 9033 -> 9457 bytes harness/udpstream/lance_cmd.c | 39 +++++++++++++++++++++++-- harness/udpstream/vtcmd.py | 10 +++++-- 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md index 4115af1..3dda0e9 100644 --- a/harness/mame/NETWORK_REPORT.md +++ b/harness/mame/NETWORK_REPORT.md @@ -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. diff --git a/harness/udpstream/build/lance_cmd.coff b/harness/udpstream/build/lance_cmd.coff index 36d2b0a6c14838f51dcc3570853a842e341ee475..c88d29ffbbdb2b87f2f7d146826f4629d0647559 100644 GIT binary patch delta 677 zcmYjPO=uHQ5T1E2iAh7H35G;k;%*Q`D81@Q_Oh*lJqSe)9+q|U0?{>*WD)g}n?gxS z!-JkvL84%xx=IduSQPweJ?x>B;!&glZ#`%=)o$OWX441nXTJI78|KY?p5D1sTZ9nw zHoo87mI2NJzywf?;zwAGqObs@c>twX7e*adqYa$0*gdQ78DI!*-yR;4cuwVYOBc5v z<8R;V=Qz3)fMzWg9Z;IhG=CeoN21+RWUYIZI^CG(0X#Ye z;Gj0@h8uh*ox*KCd-9xpA+r=B(|_#Ei3KDNe%poYaS~Q~0K5E(oT8#c-jr0L^!S#P z`)|rTE1#ymjw;ImHZNbS2@L%oBcSPq!R}a^ivdz$dR_x?Nr2>;so&SY_3DGD+q!9% z4b9yTkSa5oA#`XEb)#77>+B6VW#bkToa>_Bhw1%pWyN;?4iK$SVa!h-3WN-^TosV} rFW{KPs>Mp03jwEU=@l_HqW)hY6033F%kNhJ(X@=H?CnKs3K@ delta 634 zcmYk4&r2IY6vyApCNxnRvH`Vfk;xhmp{Rd>c(JsHdJ+_bZnv8$#BP(2U5Z|eCl6&S z6XskHLDEa{#~yMLLJuva2tAb2YoUi8+@}6+>mklWGdl2Q-hAH7`_9apgNL8)ml;H$ z{`KelH~|<2fV-ff82Sjsb1@N(yb@jLBLE*2&wVuF(FLy+X)}N?t@(9FggcV8JWGsn zk2D#T8aLYnK&2dywU<;XJ$%=QZ*R>e@sB9)&N2r_YJ=NHIG}(F9eDax(_1ziuXI=+HOVsz{ z&8E1_Er2>QZ?nHXs@8}$lni%>CcTXsXw?V%!DjDX1o!<9mIhW4p86LgNCHP81ZeQI z(WQw)bA2#f*^2PT2Eh>WWy5Gw{=vv-A~g?<^cBmMrIdUI45nq> 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); diff --git a/harness/udpstream/vtcmd.py b/harness/udpstream/vtcmd.py index aca334f..3dcdfcf 100644 --- a/harness/udpstream/vtcmd.py +++ b/harness/udpstream/vtcmd.py @@ -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))