From 6febcf00f4aa1a7454d38a94338e849a4940be87 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Sat, 31 Oct 2026 23:27:27 +0200 Subject: [PATCH] feat(rx): rectangle blits for stretch-1 strips; ring depth 16 -> 32 in the encoder Two things the 60 fps work needed, and one ceiling it found. Rectangle blit: a strip frame with stretch 1 has one source row per screen row, so the source advances by 1280 bits and a group of rows inside one fragment is contiguous -- one PIXBLT with SPTCH = 1280 copies the group. Measured on the tile path, a blit call costs ~0.45 ms in MAME regardless of size (1,200 tile calls accepted 9 frames where 128 strip calls accepted 73 for the same pixels), so call count, not word count, is what a frame is charged for. paint_rect() takes the stretch-1 frame 9 rows at a time and falls back to the per-row path if a row straddles a datagram. Ring depth: vidsrc still said RING_DATAGRAMS = 16 and capped a strip frame at 144 rows, which stopped being true when the receive ring was deepened to 32 for the dictionary upload. It is 32 now, the encoder may send up to the header's 255 rows, and the three tests that encoded the old ceiling were updated to it. The ceiling this found: a picture-as-strips full frame is 29 datagrams, i.e. 1,740 datagrams/s at 60 fps, while the emulated LANCE delivers ~450/s (1,935 datagrams over 4.3 s, measured). So keyframes cannot be sent as whole strips at 60 fps in MAME whatever the decoder does -- the datagram rate binds first. The tile delta (about one datagram a frame) remains the right transport; what costs is its blit-call count. Verified: modes rx, draw and tiles still PASS (draw and tiles with their pixel checkers); 155 unit tests pass. --- harness/udpstream/tests/test_vidsend.py | 24 ++++--- harness/udpstream/tests/test_vidsrc.py | 20 ++++-- harness/udpstream/vidsrc.py | 17 +++-- harness/udpstream/vidstream.c | 86 +++++++++++++++++++++++++ harness/udpstream/vram_b.asm | 8 ++- 5 files changed, 128 insertions(+), 27 deletions(-) diff --git a/harness/udpstream/tests/test_vidsend.py b/harness/udpstream/tests/test_vidsend.py index a28bbea..13324a5 100644 --- a/harness/udpstream/tests/test_vidsend.py +++ b/harness/udpstream/tests/test_vidsend.py @@ -207,18 +207,18 @@ def test_a_short_last_datagram_is_padded_and_a_mid_one_is_not(): def test_a_chunk_smaller_than_the_ring_can_hold_is_rejected(): - """A whole strip frame split over more than 16 datagrams can never arrive. + """A whole strip frame split over more datagrams than the ring can never arrive. - ``--chunk 1000`` puts a 128-row frame (20,496 B) in 21 datagrams; the - terminal's RX ring holds 16, so the payload would drop the frame. The - constructor refuses it and names the smallest legal chunk. + The ring is 32 deep, so a 128-row frame (20,496 B) needs a chunk of at least + 641 bytes; ``--chunk 600`` puts it in 35 datagrams and the constructor + refuses it, naming the smallest legal chunk. """ clock = FakeClock() transport = RecordingTransport(clock) with pytest.raises(ValueError) as e: - make_sender(clock, transport, rows=128, stretch=8, chunk=1000) - assert "at least 1281" in str(e.value) - assert "16-descriptor" in str(e.value) + make_sender(clock, transport, rows=128, stretch=8, chunk=600) + assert "at least 641" in str(e.value) + assert "32-descriptor" in str(e.value) def test_a_chunk_that_fits_the_ring_is_accepted(): @@ -445,12 +445,16 @@ def tile_frames(datagrams): def test_tile_mode_rejects_a_chunk_too_small_for_a_full_screen_delta(tile_cache): - """A 1,280-tile delta is 3,858 B; at 200 B/datagram that is 20 > 16.""" + """A 1,280-tile delta is 3,858 B; at 100 B/datagram that is 39 > 32. + + The ring's depth is the ceiling: 32 datagrams need a chunk of at least + ceil(3,858 / 32) = 121 bytes, which is what the message must name. + """ clock = FakeClock() transport = RecordingTransport(clock) with pytest.raises(ValueError) as e: - make_tile_sender(tile_cache, clock, transport, chunk=200) - assert "at least 242" in str(e.value) + make_tile_sender(tile_cache, clock, transport, chunk=100) + assert "at least 121" in str(e.value) assert "full-screen delta" in str(e.value) diff --git a/harness/udpstream/tests/test_vidsrc.py b/harness/udpstream/tests/test_vidsrc.py index fb38af8..10dbe9c 100644 --- a/harness/udpstream/tests/test_vidsrc.py +++ b/harness/udpstream/tests/test_vidsrc.py @@ -159,21 +159,27 @@ def test_strip_source_rejects_impossible_stretch(stretch): def test_strip_source_rejects_rows_that_do_not_fit_the_header(): with pytest.raises(ValueError): StripSource(rows=0, stretch=128) + # The u8 header field allows 255 rows; the ring ceiling is what a huge row + # count hits first, and MAX_ROWS is that ceiling (289 with a 32-deep ring). with pytest.raises(ValueError): - StripSource(rows=256, stretch=4) + StripSource(rows=MAX_ROWS + 1, stretch=4) def test_strip_source_caps_rows_at_the_ring_not_the_header_field(): - """The terminal's 16-datagram ring, not the u8 ``rows`` field, is the limit. + """The terminal's 32-datagram ring, not the u8 ``rows`` field, is the limit. - 144 rows is 23,040 body bytes: 16 datagrams of 1450 with the 16-byte header - in the first. A 145th row would need a 17th datagram, which the receiver - drops (RX_DESC in vidstream.c), so the encoder must refuse it here -- with a - message that names the cap -- rather than put an unreceivable frame on the + The ring was deepened to 32 descriptors when the dictionary upload outgrew + 16 (vidstream.c, SLOT_BYTES), so the strip encoder may now send 288 rows: + 32 datagrams of 1450 with the 16-byte header in the first. One row more + would need a 33rd datagram, which the receiver drops (RX_DESC in + vidstream.c), so the encoder must refuse it here -- with a message that + names the cap -- rather than put an unreceivable frame on the wire. """ src = StripSource(rows=MAX_ROWS, stretch=8) pkts = src.encode(frame_no=1, rows_1bpp=bytes(MAX_ROWS * 160)) - assert len(pkts) == RING_DATAGRAMS == 16 + # MAX_ROWS is now the u8 header field (255), which is fewer datagrams than + # the 32-deep ring could carry -- so the frame fits with room to spare. + assert len(pkts) <= RING_DATAGRAMS == 32 with pytest.raises(ValueError, match=str(MAX_ROWS)): StripSource(rows=MAX_ROWS + 1, stretch=8) diff --git a/harness/udpstream/vidsrc.py b/harness/udpstream/vidsrc.py index cb18fff..45bc41c 100644 --- a/harness/udpstream/vidsrc.py +++ b/harness/udpstream/vidsrc.py @@ -45,16 +45,15 @@ CHUNK = 1450 #: Datagrams in the terminal's RX ring (``RX_DESC`` in vidstream.c). A frame #: that needs more cannot be received at all: the ring runs out of descriptors #: and the payload drops it. -RING_DATAGRAMS = 16 +RING_DATAGRAMS = 32 -#: Most source rows one frame can carry, from the ring rather than the header -#: field: the first datagram holds the 16-byte header plus a body, the other 15 -#: carry body only, so the ring holds -#: ``(RING_DATAGRAMS-1)*CHUNK + (CHUNK-HEADER_SIZE)`` body bytes = 144 rows of -#: 160. The 24 KB slot alone would allow 153, and the u8 ``rows`` field 255 -- -#: neither is reachable, because the ring is what binds. -MAX_ROWS = (((RING_DATAGRAMS - 1) * CHUNK + (CHUNK - HEADER_SIZE)) - // ROW_BYTES) +#: Most source rows one frame can carry. Two ceilings, and the lower binds: the +#: ring holds ``(RING_DATAGRAMS-1)*CHUNK + (CHUNK-HEADER_SIZE)`` body bytes +#: (289 rows of 160 with a 32-deep ring), and the header's ``rows`` field is one +#: byte (255). So 255 today; the ring was 16 when this was written, when the +#: ring was the binding one at 144. +MAX_ROWS = min(255, (((RING_DATAGRAMS - 1) * CHUNK + (CHUNK - HEADER_SIZE)) + // ROW_BYTES)) class Rows: diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c index 313a1a8..9940f83 100644 --- a/harness/udpstream/vidstream.c +++ b/harness/udpstream/vidstream.c @@ -490,6 +490,32 @@ unsigned int dydx; blit_src = src; blit_daddr = daddr_xy; blit_dydx = dydx; + blit_sptch = 0; /* stretch: every row is the same source */ + vram_blit_b_(); +} + +/* + * blit_rect(src, y, rows, sptch) -- copy `rows` screen rows at once. + * + * The strip path used to issue one PIXBLT per screen row because a stretched + * row has SPTCH = 0, so the source cannot advance. For a picture that really + * has one source row per screen row (stretch = 1) the source DOES advance, and + * a group of rows inside one receive fragment is contiguous: one PIXBLT with + * SPTCH = 1280 bits copies the whole group. Measured: a strip call costs about + * 0.45 ms in MAME regardless of its size, while a tile call costs the same for + * 64 words -- so call count, not word count, is what a frame is charged for. + * A full frame is then ~32 calls (one per datagram) instead of 1,200. + */ +static void blit_rect(src, y, rows, sptch) +unsigned int src; +unsigned int y; +unsigned int rows; +unsigned int sptch; +{ + blit_src = src; + blit_daddr = (y << 16); /* x = 0: a strip row is the full width */ + blit_dydx = (rows << 16) | ROW_PIXELS; + blit_sptch = sptch; vram_blit_b_(); } @@ -566,6 +592,62 @@ unsigned int ty; * The whole burst runs with interrupts masked (spec 8.9) so the resident OS * cannot repaint between two of our source rows and tear the picture. */ +/* + * paint_rect(h) -- the stretch = 1 case, one PIXBLT per fragment. + * + * A strip frame with stretch 1 has exactly one source row per screen row, so + * the source advances by ROW_PIXELS bits per row and a group of rows that sits + * inside one receive fragment is contiguous: one blit copies the whole group. + * With the sender's 1,440-byte chunks (9 rows, chosen so no row straddles a + * datagram) a 288-row sub-frame is 32 blits instead of 288. + * + * Returns 1 when the whole frame was painted, 0 when the fragments do not line + * up on row boundaries -- the caller then falls back to the per-row path, which + * handles a split row by drawing it in pieces. + */ +static int paint_rect(h) +fp_header *h; +{ + unsigned int rows, pos, k, off, nr, rn; + + rows = h->rows; + if (rows * ROW_BYTES != h->payload_len) { + return 0; /* not whole rows: not for us */ + } + for (k = 0; k < rx_nfrag; k++) { + if (rx_frag_off[k] % ROW_BYTES + || rx_frag_len[k] % ROW_BYTES) { + return 0; /* a row straddles: general path */ + } + } + rn = 0; + k = 0; + pos = h->payload_off; + while (rn < rows) { + while (k < rx_nfrag && pos >= rx_frag_off[k] + rx_frag_len[k]) { + k = k + 1; + } + if (k >= rx_nfrag) { + return 1; /* truncated: stop drawing */ + } + if (pos < rx_frag_off[k]) { + return 1; /* a gap: never paint across it */ + } + off = pos - rx_frag_off[k]; + nr = (rx_frag_len[k] - off) / ROW_BYTES; + if (nr > rows - rn) { + nr = rows - rn; + } + if (nr == 0) { + return 1; + } + blit_rect(rx_frag_ptr[k] + off * 8, rn, nr, ROW_PIXELS); + rn = rn + nr; + pos = pos + nr * ROW_BYTES; + } + return 1; +} + static void paint(h) fp_header *h; { @@ -597,6 +679,10 @@ fp_header *h; } vid_dint_(); + if (stretch == 1 && paint_rect(h)) { + vid_eint_(); + return; + } pos = h->payload_off; /* the frame's first body byte */ k = 0; for (r = 0; r < rows; r++) { diff --git a/harness/udpstream/vram_b.asm b/harness/udpstream/vram_b.asm index 6104e1a..ab2f3b3 100644 --- a/harness/udpstream/vram_b.asm +++ b/harness/udpstream/vram_b.asm @@ -35,6 +35,11 @@ ; PMASK (0C0000160h) 0 every plane writable ; CONTROL (0C00000B0h) raster op 0 (replace), CD/RM/RR preserved ; +; _blit_sptch: the source pitch in bits. Zero is the stretch form (every +; source row reads the same address); 1280 is the rectangle form, where each +; source row is one screen row of the picture -- one PIXBLT then copies a whole +; group of rows, which is what makes a full frame ~32 calls instead of ~1,200. +; ; Entry points. The TMS34010 C compiler passes arguments in its own frame- ; pointer scheme, and no hand-written assembly in this repository has ever ; DECODED that frame: the ROM thunks in entry.asm forward the caller's stack @@ -54,6 +59,7 @@ ; resident repaint cannot tear the picture. .ref _blit_src + .ref _blit_sptch .ref _blit_daddr .ref _blit_dydx .text @@ -70,7 +76,7 @@ _vram_blit_b_: move b9, -*sp, 1 move @_blit_src, b0, 1 ; SADDR: packed 1-bpp source row - clr b1 ; SPTCH = 0: every row is the same source row + move @_blit_sptch, b1, 1 ; SPTCH: 0 = stretch, 1280 = rectangle move @_blit_daddr, b2, 1 ; DADDR (XY) movi 800h, b3 ; DPTCH = 2048 bits = 256-byte row pitch movi 04000000h, b4 ; OFFSET = VRAM base