diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md index 2e2bd31..230b9c1 100644 --- a/harness/mame/NETWORK_REPORT.md +++ b/harness/mame/NETWORK_REPORT.md @@ -5038,3 +5038,51 @@ already depends on for every string and label it draws. Only the *target* changed; the loop and its store count are the same, so the per-store cost is still what the benchmark reports. + +## 75. The CPU cannot build frames — the cost is per store, not per destination + +`cmd 9`/`cmd 11` measured the same loop against different targets: + +| store type | frames | per frame | per store | +|---|---|---|---| +| byte → VRAM (unit) | 2 | 2282 ms | 13.93 us | +| byte → DRAM (unit) | 20 | 2281 ms | **13.92 us** | +| byte → VRAM (MAME) | 2 | 1588 ms | 9.70 us | +| 16-bit → VRAM (MAME) | 10 | 863 ms | 10.53 us per **word** | + +Byte stores to DRAM and to VRAM cost *the same* — 13.9 us — so the display +arbiter is not involved at all, and halving the store count (16-bit stores) halves +the time exactly. The cost is **per store instruction**, not per byte and not per +destination. + +Compare the graphics engine: `FILL L` writes 2.5 M destination words/s +(0.40 us/word) and `PIXBLT L,XY` 1.6 M words/s (0.62 us/word). The blitter is +therefore **25-35x** faster per word than the CPU, and that is the whole +difference between 0.5 fps and 30 fps. + +Consequences for streaming video into VRAM: + +1. **Never build pixels with CPU stores.** Expanding a 160x128 source to + 1280x1024 in RAM would be 163,840 stores x 13.9 us = 2.3 s per frame, in + either memory. The DRAM path does not help; the earlier plan to "compose in + RAM then blit" is dead for the same reason. +2. **Let the LANCE write the frame into the buffer.** RX descriptors can chain, + so a single 160 KB frame spanning ~108 descriptors lands *contiguously* in a + pre-armed buffer with **no CPU copy** (descriptor i receives + `[i*1526, (i+1)*1526)`). Then one `PIXBLT L,XY` puts it on screen. +3. The budget then becomes wire + blit: + + | frame size | wire @1.27 MB/s | blit | total | fps | + |---|---|---|---|---| + | 1280x1024 (163,840 B) | 129 ms | ~60 ms | 189 ms | **~5** | + | 640x512 (40,960 B) | 32 ms | ~15 ms | 47 ms | **~21** | + | 320x240 (9,600 B) | 8 ms | ~4 ms | 12 ms | **~80** | + + (blit scaled linearly from the measured 51 ms MAME / ~60 ms real per full + frame; double-buffering would hide the blit behind the next frame's wire + time, so the achievable rate is closer to `max(wire, blit)`.) + +So "stream Bad Apple into VRAM" is real, but the resolution/fps trade is set by +the wire and the blitter, not by the CPU: full screen is a ~5-8 fps proposition, +half resolution ~20 fps. `cmd 10` (`PIXBLT L,XY` from a RAM buffer) and `cmd 11` +are the benchmarks behind this table. diff --git a/harness/udpstream/build/lance_cmd.coff b/harness/udpstream/build/lance_cmd.coff index 5599b99..7e2dcd1 100644 Binary files a/harness/udpstream/build/lance_cmd.coff and b/harness/udpstream/build/lance_cmd.coff differ diff --git a/harness/udpstream/lance_cmd.c b/harness/udpstream/lance_cmd.c index b309d23..bf898fc 100644 --- a/harness/udpstream/lance_cmd.c +++ b/harness/udpstream/lance_cmd.c @@ -68,6 +68,14 @@ unsigned int vram_pattern; * cost, and the linker guarantees it is memory we own. */ static unsigned char drambuf[512]; +/* blit parameters, read by vram.asm */ +unsigned int blit_src, blit_sptch, blit_daddr, blit_dydx; +extern void vram_blit_(); + +/* one 1280x1024 1-bpp frame, 256-byte rows. .bss is enlarged to 256 KB in + * build-lance.sh so this fits. */ +static unsigned char framebuf[163840]; + extern void vram_fill_(); /* Compile-time guard: the RX ring must end before the TX ring starts. @@ -509,6 +517,60 @@ int main() } wb(TXBUF + 19 * 8, (wlen >> 8) & 0xFF); wb(TXBUF + 20 * 8, wlen & 0xFF); + } else if (cmd == 11) { + /* 16-bit store benchmark: does a native word store cost + * the same as a byte store? If it does not, CPU-side + * frame building is back on the table. 80 words/row. */ + unsigned int nw, fw, yw, xw, ow, rw, sw; + nw = rlen; + if (nw > 100) { + nw = 100; + } + rw = addr; + if (rw == 0 || rw > 1024) { + rw = 1024; + } + sw = 0; + for (fw = 0; fw < nw; fw++) { + for (yw = 0; yw < rw; yw++) { + ow = VRAM_BASE + yw * 2048; + for (xw = 0; xw < 80; xw++) { + *(unsigned int *)(ow + xw * 16) = + (unsigned int)(yw + xw + fw); + sw = sw + 1; + } + } + } + wb(TXBUF + 19 * 8, (nw >> 8) & 0xFF); + wb(TXBUF + 20 * 8, nw & 0xFF); + wb(TXBUF + 21 * 8, (rw >> 8) & 0xFF); + wb(TXBUF + 22 * 8, rw & 0xFF); + wb(TXBUF + 23 * 8, (sw >> 24) & 0xFF); + wb(TXBUF + 24 * 8, (sw >> 16) & 0xFF); + wb(TXBUF + 25 * 8, (sw >> 8) & 0xFF); + wb(TXBUF + 26 * 8, sw & 0xFF); + } else if (cmd == 10) { + /* PIXBLT L,XY benchmark: RAM frame buffer -> VRAM. + * The buffer is touched only lightly (1 KB) so the fill is + * not part of the measurement. */ + unsigned int nb, b2i, k2; + nb = rlen; + if (nb > 100) { + nb = 100; + } + for (k2 = 0; k2 < 1024; k2++) { + framebuf[k2] = (unsigned char)(k2 * 7); + } + blit_src = (unsigned int)framebuf; + blit_sptch = 0x800; + blit_daddr = 0; + blit_dydx = (1024 << 16) | 1280; + for (b2i = 0; b2i < nb; b2i++) { + vram_blit_(); + } + wb(TXBUF + 19 * 8, (nb >> 8) & 0xFF); + wb(TXBUF + 20 * 8, nb & 0xFF); + wb(TXBUF + 21 * 8, 0x04); wb(TXBUF + 22 * 8, 0x00); } 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. diff --git a/harness/udpstream/vtcmd.py b/harness/udpstream/vtcmd.py index 3dcdfcf..bf811ab 100644 --- a/harness/udpstream/vtcmd.py +++ b/harness/udpstream/vtcmd.py @@ -166,6 +166,10 @@ 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("--word", action="store_true", + help="vram: 16-bit store loop instead of byte stores") + ap.add_argument("--blit", action="store_true", + help="vram: use PIXBLT L,XY from a RAM frame buffer") ap.add_argument("--dram", action="store_true", help="vram: same byte-store loop but into DRAM (no VRAM " "arbiter) -- sizes the frame-expansion stage") @@ -196,7 +200,8 @@ def main(): if a.cmd == "vram": n = a.frames - opcode = 8 if a.fill else (9 if a.dram else 7) + opcode = 10 if a.blit else (8 if a.fill else (11 if a.word 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)) @@ -205,23 +210,27 @@ def main(): print("no reply") return 1 dt = t1 - t0 - if a.fill: + if a.blit: + nf = struct.unpack(">H", data[:2])[0] + rows, stores = 1024, nf * 1024 * 80 + elif a.fill: nf, x = struct.unpack(">HH", data[:4]) rows = 1024 stores = nf * 1024 * 80 # destination words, not bytes - elif a.dram: + elif a.dram or a.word: 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 - ("DRAM stores" if a.dram else "byte stores"), + % (nf, rows, "PIXBLT L,XY" if a.blit else + ("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)) - if a.fill: + if a.fill or a.blit: print(" %.0f words/s (%.2f us per destination word)" % (rate, 1e6 / rate if rate else 0)) else: