udpstream: the CPU store path costs ~14 us/store regardless of destination

Measured on the unit: byte stores to DRAM 13.92 us, to VRAM 13.93 us -- the
same, so the display arbiter is not involved.  In MAME 16-bit stores halve the
time exactly (863 ms vs 1588 ms per frame), i.e. the cost is per store
instruction, not per byte.

Against the graphics engine (FILL L 0.40 us/word, PIXBLT L,XY 0.62 us/word)
the CPU is 25-35x slower per word.  So frames must never be built with CPU
stores: the LANCE can chain descriptors so a 160 KB frame lands contiguously
in a pre-armed buffer with no CPU copy, and one PIXBLT L,XY puts it on screen.

Budget: full frame ~5 fps (129 ms wire + 60 ms blit), 640x512 ~21 fps,
320x240 ~80 fps.  Report section 75; cmd 10 (PIXBLT from RAM) and cmd 11
(16-bit store control) are the benchmarks.
This commit is contained in:
arti committed 2026-10-30 17:42:22 +02:00
1 parent 9b3f02cf02
commit 636b3d6985
4 files changed
+125 -6

No files matched your search

+48
View File
@@ -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.
Binary file not shown.
+62
View File
@@ -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.
+15 -6
View File
@@ -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: