diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md index d11c6d0..4115af1 100644 --- a/harness/mame/NETWORK_REPORT.md +++ b/harness/mame/NETWORK_REPORT.md @@ -4918,3 +4918,68 @@ moment its RX ring is armed: together with **no sleep**: listen, load, wait for the announce, `stats`, `osboot`, wait for `:771`. Measured on the unit: announce at 7.2 s, `:771` back 40 s after the hand-back (section 73.3.1). + +## 74. VRAM writes: byte stores are 70x slower than the hardware FILL + +The question "can we stream video into VRAM" reduces to how fast a frame can be +written. Measured with the payload (`cmd 7`), byte-at-a-time stores at the +display's own geometry (1280x1024, 256-byte stride, 160 bytes per row): + +``` +filled 20 frame(s) x 1024 rows: 3276800 stores in 40.040 s + 2002.0 ms per frame -> 0.50 fps ceiling + 81840 bytes/s (12.22 us per byte store) +``` + +12.2 us per byte is about a scanline period, i.e. the CPU gets almost nothing +through the VRAM arbiter this way. + +### 74.1 The manual's answer: FILL and PIXBLT + +The TMS34010 has array instructions that run in the GSP's pixel engine, and the +non-display path only pays for them once per array. From the User's Guide: + +* **FILL L** — Table 13-6, long rows `(1+N*G)L + 2` states; Table 13-7 gives + `G = 2` for "replace, no plane masking". With `N = 80` words/row and + `L = 1024` rows that is **164,864 states**, ~16 ms at a 10 MHz state. +* **PIXBLT B,L** — Table 13-9, `[(2+G)N]L + 5` = **327,680 states**, ~33 ms. + +Implied operands (instruction reference): `DADDR`=B2, `DPTCH`=B3, `DYDX`=B7 +(XY form, `(rows<<16)|columns`), `COLOR1`=B9; plus `CONTROL` (`0C00000B0h`, +PPOP bits 10-14 = 0 for replace), `PSIZE` (`0C0000150h` = 1), `PMASK` +(`0C0000160h` = 0, i.e. nothing protected — a 1 would *block* a plane). Note the +manual's FILL page lists CONTROL at `C0000090h`; the register map (Figure 6-1) +says `C00000B0h` is CONTROL and `C0000090h` is DPYSTRT, and the reset trace +writes `C00000B0h`, so `B0h` is right. + +`harness/udpstream/vram.asm` implements it: save B2/B3/B7/B9, load the four +implied operands, set `PSIZE`/`PMASK`/`CONTROL` (CONTROL is read-modify-write so +`CD`, `RM` and `RR` survive), read CONTROL back because I/O writes are +pipelined, then `FILL L`. The pattern comes from the C global `_vram_pattern` +so no calling-convention guess is needed. + +### 74.2 Measured, both machines + +| | byte stores | `FILL L` | speedup | +|---|---|---|---| +| MAME | 1992 ms/frame | 27.1 ms (36.9 fps) | 74x | +| **real VT1200** | **2284 ms/frame** | **32.7 ms (30.6 fps)** | **70x** | + +``` +=== REAL FILL L, 30 frames === +filled 30 frame(s) x 1024 rows (FILL L): 2457600 stores in 0.980 s + 32.7 ms per frame -> 30.62 fps ceiling + 2508224 words/s (0.40 us per destination word) +``` + +So a full-screen write costs ~33 ms, and the frame budget for video becomes: + +* **full frame over the wire**: 163,840 B at the measured 1.27 MB/s = **8 fps** — + the network, not the CPU, is the limit for uncompressed full frames; +* **small source expanded on the device** (160x128 = 2,560 B/frame, 2 ms of + wire): the ~33 ms fill/transfer dominates, so **~30 fps full screen** is the + target, provided the expansion itself is done with `PIXBLT` rather than CPU + stores. + +`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. diff --git a/harness/udpstream/build-lance.sh b/harness/udpstream/build-lance.sh index c68e366..63f7f92 100755 --- a/harness/udpstream/build-lance.sh +++ b/harness/udpstream/build-lance.sh @@ -29,6 +29,7 @@ SRC=${LANCE_SRC:-$DIR/lance.c} cp "$SRC" "$WORK/LANCE.C" cp "$DIR/romapi.h" "$WORK/ROMAPI.H" cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" +cp "$DIR/vram.asm" "$WORK/VRAM.ASM" { printf '%s\n' '-a' @@ -37,6 +38,7 @@ cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" printf '%s\n' '-e _start' printf '%s\n' 'LANCE.obj' printf '%s\n' 'ENTRY.obj' + printf '%s\n' 'VRAM.obj' printf '%s\n' '-l RTS.LIB' printf '%s\n' 'MEMORY' printf '%s\n' '{' @@ -55,7 +57,8 @@ sed -i 's/$/\r/' "$WORK/LANCE.CMD" rm -f "$WORK"/LANCE.OBJ "$WORK"/LANCE.OUT "$WORK"/LANCE.LST \ "$WORK"/LANCE.MAP "$WORK"/LANCE.ASM "$WORK"/LANCE.CPP \ - "$WORK"/LANCE.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST + "$WORK"/LANCE.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST \ + "$WORK"/VRAM.OBJ "$WORK"/VRAM.LST SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ @@ -65,18 +68,19 @@ timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ -c "gspcg LANCE" \ -c "gspa LANCE LANCE.obj LANCE.lst -l" \ -c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \ + -c "gspa VRAM VRAM.obj VRAM.lst -l" \ -c "gsplnk LANCE.cmd" \ -c "exit" -exit > "$WORK/dosbox.log" 2>&1 if [ ! -f "$WORK/LANCE.OUT" ]; then echo "build.sh: GSPLNK produced no LANCE.OUT" >&2 sed 's/\r$//' "$WORK/LANCE.MAP" 2>/dev/null | tail -20 >&2 || true - for f in LANCE.LST ENTRY.LST; do + for f in LANCE.LST ENTRY.LST VRAM.LST; do sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true done exit 1 fi -for f in LANCE.LST ENTRY.LST; do +for f in LANCE.LST ENTRY.LST VRAM.LST; do if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then echo "build.sh: $f diagnostics:" >&2 sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2 @@ -107,5 +111,6 @@ if m: PYCHK sed 's/\r$//' "$WORK/LANCE.LST" > "$OUT/lance.lst" sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst" +sed 's/\r$//' "$WORK/VRAM.LST" > "$OUT/vram.lst" echo "build.sh: $OUT/$OUTNAME ($(wc -c < "$OUT/lance.coff") bytes)" grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/lance.map" | head -8 diff --git a/harness/udpstream/build/lance_cmd.coff b/harness/udpstream/build/lance_cmd.coff index 5b19993..36d2b0a 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 f927b86..5111e74 100644 --- a/harness/udpstream/lance_cmd.c +++ b/harness/udpstream/lance_cmd.c @@ -20,7 +20,7 @@ */ #define RAP 0x05900000 #define RDP 0x05800000 -#define MARK 0x018D0000 /* after .text (which now reaches 0x018CACD0) */ +#define MARK 0x01A00000 /* after .text (which now reaches 0x018CACD0) */ #define VRAM_BASE 0x04000000 #define MODE_LOOP 0x0004 @@ -61,6 +61,11 @@ #define RXBUFLEN 1526 /* one full 1514-byte MTU frame + margin */ #define ETHCMD 0x88B6 +/* read by vram.asm (symbol _vram_pattern) */ +unsigned int vram_pattern; + +extern void vram_fill_(); + /* Compile-time guard: the RX ring must end before the TX ring starts. * (Bit addresses: NRX descriptors of 64 bits each.) Violating this is what * broke the first version of this payload, and it fails silently at runtime. */ @@ -442,6 +447,12 @@ int main() wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB6); wb(TXBUF + 14 * 8, 'K'); wb(TXBUF + 15 * 8, 'S'); wb(TXBUF + 16 * 8, cmd); + /* Zero the data area before filling it. Without this a reply + * that writes fewer bytes than the previous one leaves stale + * data behind, and the host cannot tell it from a real field. */ + for (k = 0; k < 24; k++) { + wb(TXBUF + (19 + k) * 8, 0); + } len = 60; if (cmd == 1) { @@ -494,6 +505,61 @@ int main() } wb(TXBUF + 19 * 8, (wlen >> 8) & 0xFF); wb(TXBUF + 20 * 8, wlen & 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; + } + for (f2 = 0; f2 < nf2; f2++) { + vram_pattern = 0xFFFF; + vram_fill_(); + } + wb(TXBUF + 19 * 8, (nf2 >> 8) & 0xFF); + wb(TXBUF + 20 * 8, nf2 & 0xFF); + wb(TXBUF + 21 * 8, (nf2 * 80) >> 8 & 0xFF); + wb(TXBUF + 22 * 8, (nf2 * 80) & 0xFF); + } else if (cmd == 7) { + /* VRAM fill benchmark: write `rlen` full frames of + * 1024 rows x 160 bytes (1280x1024, 256-byte stride) and + * reply when done, so the host can time it. This is the + * same store pattern the renderer uses, i.e. the number is + * the achievable frame rate, not a synthetic one. */ + unsigned int nf, f, y, x, o, rows, stores; + nf = rlen; + if (nf > 100) { + nf = 100; + } + /* `addr` selects how many of the 1024 rows to write, so the + * per-store cost can be separated from any fixed overhead */ + rows = addr; + if (rows == 0 || rows > 1024) { + rows = 1024; + } + stores = 0; + for (f = 0; f < nf; f++) { + for (y = 0; y < rows; y++) { + o = VRAM_BASE + y * 2048; + for (x = 0; x < 160; x++) { + *(unsigned char *)(o + x * 8) = + (unsigned char)(y + x + f); + stores = stores + 1; + } + } + } + /* reply: frames(2) rows(2) stores(4) so the host can + * compute the rate from what the payload counted, not + * from what it was asked to do */ + wb(TXBUF + 19 * 8, (nf >> 8) & 0xFF); + wb(TXBUF + 20 * 8, nf & 0xFF); + wb(TXBUF + 21 * 8, (rows >> 8) & 0xFF); + wb(TXBUF + 22 * 8, rows & 0xFF); + wb(TXBUF + 23 * 8, (stores >> 24) & 0xFF); + wb(TXBUF + 24 * 8, (stores >> 16) & 0xFF); + wb(TXBUF + 25 * 8, (stores >> 8) & 0xFF); + wb(TXBUF + 26 * 8, stores & 0xFF); } else if (cmd == 6) { /* OS cold start: the entry POST branches to at 0x3F2042D0 * when it skips the level-1 batch. Unlike the reset diff --git a/harness/udpstream/vram.asm b/harness/udpstream/vram.asm new file mode 100644 index 0000000..5abdb38 --- /dev/null +++ b/harness/udpstream/vram.asm @@ -0,0 +1,59 @@ +; vram.asm -- hardware framebuffer fill for the VT1200 payload. +; +; The C loop in lance_cmd.c writes the screen one byte at a time and measures +; ~2.0 s per 1280x1024 frame (0.5 fps). FILL L does the same job in the GSP's +; array engine: the manual's Table 13-6 gives (1 + N*G)L states for a long row, +; with G=2 for "replace, no plane masking", N=80 words/row and L=1024 rows -- +; about 165k states, i.e. ~16 ms at a 10 MHz state, roughly 120x faster. +; +; Implied operands of FILL L (TMS34010 User's Guide, instruction reference): +; DADDR = B2 linear address of the first pixel of the array +; DPTCH = B3 pitch between rows, in bits +; DYDX = B7 dimensions in XY form: (rows << 16) | columns +; COLOR1 = B9 fill colour / 16-bit pattern +; CONTROL (0C00000B0h) bits 10-14 PPOP = 0 -> replace (S -> D) +; PSIZE (0C0000150h) 1 -> one bit per pixel +; PMASK (0C0000160h) 0 -> no plane protected, all bits writable +; +; DADDR and DYDX are modified by the instruction, so they are reloaded on every +; call. The pattern is read from the C global _vram_pattern (a .bss word) so +; no calling-convention assumption is needed. + + .ref _vram_pattern + .text + .globl _vram_fill_ +_vram_fill_: + setf 32, 0, 1 + move b2, -*sp, 1 + move b3, -*sp, 1 + move b7, -*sp, 1 + move b9, -*sp, 1 + + ; --- destination array ------------------------------------------------- + movi 04000000h, b2 ; VRAM base, bit address + movi 800h, b3 ; 2048 bits = 256-byte row pitch + movi 04000500h, b7 ; DY=1024 (0400h), DX=1280 (0500h) + move @_vram_pattern, b9, 1 ; fill pattern + + ; --- pixel format ------------------------------------------------------ + movi 1, a1 + move a1, @0C0000150h, 0 ; PSIZE = 1 + clr a1 + move a1, @0C0000160h, 0 ; PMASK = 0 (all planes writable) + move a1, @0C0000170h, 0 ; upper half, for forward compatibility + + ; --- graphics operation: replace, no transparency, no window ---------- + move @0C00000B0h, a1, 0 ; CONTROL + andi 0801Fh, a1 ; keep CD (15) and RM/RR (2-4) only + move a1, @0C00000B0h, 0 + move @0C00000B0h, a1, 0 ; read back: I/O writes are pipelined + + fill l + + move *sp+, b9, 1 + move *sp+, b7, 1 + move *sp+, b3, 1 + move *sp+, b2, 1 + rets + + .end diff --git a/harness/udpstream/vtcmd.py b/harness/udpstream/vtcmd.py index e9c4f33..aca334f 100644 --- a/harness/udpstream/vtcmd.py +++ b/harness/udpstream/vtcmd.py @@ -15,6 +15,8 @@ Protocol, ethertype 0x88B6, both directions: real machine; kept only because MAME tolerates it) cmd 0xFF announce: sent BY the payload, three broadcasts, magic "LANC" right after its ring is armed -- `vtcmd.py wait` listens for it + cmd 7 vram : [21..22] frame count -- fill that many full 1280x1024 + frames, reply when done (the host times it) cmd 6 osboot: no arguments -- jumps to the OS cold-start entry 0x3F5788C0, the path POST takes when it skips level 1. That path STOPs the LANCE and re-programs the board window first. @@ -134,7 +136,8 @@ def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("cmd", choices=["stats", "read", "poke", "jump", - "reboot", "osboot", "wait", "bench"]) + "reboot", "osboot", "wait", "vram", + "bench"]) ap.add_argument("--iface", default="vt1200") ap.add_argument("--addr", type=lambda v: int(v, 0), default=0) ap.add_argument("--len", type=int, default=32) @@ -159,6 +162,13 @@ def main(): "into microbursts that the switch drops") ap.add_argument("--force", action="store_true", help="reboot: required, it succeeded 1 of 2 times and can hang") + ap.add_argument("--frames", type=int, default=5, + 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("--fill", action="store_true", + help="vram: use the hardware FILL L instruction instead of " + "byte stores") ap.add_argument("--wait-timeout", type=float, default=180.0, help="wait: how long to listen for the payload's announce") ap.add_argument("--terse", action="store_true", @@ -181,6 +191,38 @@ def main(): print(" %04x %s" % (i, data[i:i + 16].hex(" "))) return 0 + if a.cmd == "vram": + n = a.frames + opcode = 8 if a.fill else 7 + t0 = time.time() + status, data = request(sock, a.iface, opcode, a.rows, n, + timeout=max(120.0, a.timeout)) + t1 = time.time() + if status is None: + print("no reply") + return 1 + dt = t1 - t0 + if a.fill: + nf, x = struct.unpack(">HH", data[:4]) + rows = 1024 + stores = nf * 1024 * 80 # destination words, not bytes + 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", + stores, dt)) + print(" %.1f ms per frame -> %.2f fps ceiling" + % (per * 1000.0, 1.0 / per if per else 0)) + if a.fill: + print(" %.0f words/s (%.2f us per destination word)" + % (rate, 1e6 / rate if rate else 0)) + else: + print(" %.0f bytes/s (%.2f us per byte store)" + % (rate, 1e6 / rate if rate else 0)) + return 0 + if a.cmd == "wait": # the payload broadcasts this the moment its RX ring is armed, so the # host does not have to sleep and hope