From d99a9cdb6c6ef0328a60734ec471fb9132e728a4 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 30 Oct 2026 17:32:59 +0200 Subject: [PATCH] udpstream: hardware FILL L makes full-screen writes 70x faster (30.6 fps on the unit) Byte stores to VRAM measure 2284 ms per 1280x1024 frame on the real unit (0.44 fps) -- about a scanline period per byte, i.e. almost no access through the arbiter. vram.asm now drives the GSP's array engine instead: FILL L with DADDR/DPTCH/DYDX/COLOR1 (B2/B3/B7/B9), PSIZE=1, PMASK=0 and PPOP=0, per the TMS34010 User's Guide's implied-operand table, with CONTROL read-modify-written so CD/RM/RR survive. Measured: 32.7 ms/frame, 30.6 fps on the unit; 27.1 ms in MAME (74x over byte stores there, 70x on hardware). 2,508,224 destination words/s = 0.40 us/word. Consequence for video: a full uncompressed frame is wire-limited at 8 fps (163840 B at 1.27 MB/s); a small source expanded on the device is limited by the ~33 ms transfer, i.e. ~30 fps full screen if the expansion uses PIXBLT. build-lance.sh assembles vram.asm and links VRAM.obj; MARK moved to 0x01A00000 because .text had come within 146 bytes of it. --- harness/mame/NETWORK_REPORT.md | 65 +++++++++++++++++++++++ harness/udpstream/build-lance.sh | 11 ++-- harness/udpstream/build/lance_cmd.coff | Bin 8105 -> 9033 bytes harness/udpstream/lance_cmd.c | 68 ++++++++++++++++++++++++- harness/udpstream/vram.asm | 59 +++++++++++++++++++++ harness/udpstream/vtcmd.py | 44 +++++++++++++++- 6 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 harness/udpstream/vram.asm 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 5b199934afd62c361c8ebba3a51a3d3801e91284..36d2b0a6c14838f51dcc3570853a842e341ee475 100644 GIT binary patch delta 1991 zcmaJ?Z%h+s7=PZo9_>*<=ny7YI11=Y5F1T4w=8m5;vkti{IY$~-PLj!v_MlD<321` zvM{14UGZJQqA{s7tlJXQ(GQK$WjB&VCK^m!_F*5k&Wz0uA`Z8qW6SNi!O!Z6jA~L>Rf+L}Fk3 z)XJy=Xe9BarUN8WX?^I(Hv0=E63ao{CKAd)_LzvAgNP;~<{<4RBIF<)CIW~&OoiYw zA95c}USQ8#h>(2Q_7N%i>h^(Bdz#+mchrMTBc*4hl8!xLG8^CUe1t~mE&er#(B|?8 zy~n$0eQ|xgj8c)TRI?*OeU&(HZLWyWU->BY7B>)$4i(ptN6AF7r-=EPqs8R{bDKj$ zpE9R7`hEFHrj(-x9n}nQ^f)eymgEPH9|?_C>?&%r|EAxcrkxd2Wed!BD#j>|iAj-w zcArVUxM_?ql??r%@{>2G$Ugg$F3(I{i!s0^Qoy#^&+C?{TYM)BCxH#WJ)9KSQtbh+ zWBOq$U@4@M=#wx>qy%HtR8e#6$jBrJErG?6Ib8cI+(6{A7p?t4PS~W+XxwtIR4Gh#?fm{V_3)85|Sm z!ikh-=A@Z2O=T04GP;zpCX#+{dK~2qOhimX2@I!wX4=qgDC^0q!}aOeIJYyC=mY-a zFg;x5QjNmI*i~Y+0xL{LAs^RRZpWHT%bHBqeb!nLcI0?UOP;8b7%q-4tbxvMKEYJ~ zO<%1JGGL>V)w_tB&Q-reWa_NhO``M=&Ke!8*{2?ST51>mxy=9TmU+?okWcd(xqUz? zxziRVGXJ49eHvTZ3TKS=fOCgiFPq3E6FFMIhdy7Q?|AP>p}v~uzJY4q{v;;78G zband_G+IFqId^a{53oQNoSPW`GCk*Pwx$h-0Mz5GAoKK|)3rjlNSbc1t)V`rZKLr= zF#hK(NizUsRolf0*b^3d!C5sDv#1u+5(2m~AoX~s9nl}yL(MT53@*r zpZA13F9)BP6j+ZlHU^yDuLc-CwzTA39v5SlU;$n H9tr*fF*&YE delta 1289 zcmaKsO-vI(6vy9e3+t-%;-gp5-}#EenAEGV!}x;YJ#&%SlgO(l9}20{okASUN-Yy z9Gq&6jDisYZ@%;&Y5;HoKs^|c89f8CL=Wm10q|sE2q1>YQVg|(|s0=8zhVbmb6EBYY0orV%cOcr*f(ET^N8WTT$$)HTDn5kYE?c?^{% zXWH#$9Dd6-JF_N&?3V>w$L<(;MXO5h<&BbrT3TEtRj%!n-&91}Iti*7O7jd-qNka=RjM;&!c%7FXn zHepCzaLgloweo5CV>FRYFtX$B10DfMqm-}eI%2*8c2wU?)zuwlN+;7`XksyMt-(sg zetg_&HNx{#zwumsn8}*(k%k?p97h^xsL{gY&Wdb(KN zw9FEgpqX~THrp0MUF;Bp`r=!n+poCur0lPUYWibJ8t8e zOFS8XEI#I}V#pF6<=lwiX|9&>WN?Oatuhy7jm`}bVLb2;TLrxdHo@Z$OoNx`$A#aCy;ragu-Hwb|6~x9hYmX+B{YR j> 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