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.
The tile pipeline packed every row MSB first: pixel i -> bit 7 - i % 8. The
encoder and tile_check.py shared that convention, so every MAME replay check
passed while a display showed each group of eight pixels mirrored.
Evidence, from the real clip's own replay dump (vram-replay-200.bin, frame 200
of Bad Apple, 1,228,800 pixels):
decoded MSB first 1,215 mismatched <- what the pipeline produced
decoded LSB first 3,113 mismatched <- the mirrored picture, a sawtooth
and both the real unit and MAME decode LSB first: dwt/screenshot.py --real
("LSB-first bits", measured on live VRAM) and vt1200.cpp's
(vram[bit >> 4] >> (bit & 15)) & 1. A zoomed edge of that dump showed the
comb of triangles; after this change the same edge is a smooth curve.
vidsrc.Rows.pack_row now sets bit i % 8 of byte i // 8. The test that pins it
was written first and watched fail: it decodes the packed row with the
hardware's rule, which no MSB-first packer can pass. The two older tests that
asserted MSB first were rewritten -- they were asserting the bug -- and the
synthetic clip now packs through a bit-reversal table, so the committed cache
is regenerated (PSNR 29.47 -> 31.30 dB). The layout claims in tilecost.py,
vram_tile.asm and make_tiles.load_clip are corrected so the clip format states
the order instead of leaving it to whoever writes the frames.
Separately, MAME's renderer reads 128-byte rows while a payload targeting the
real unit writes 256-byte rows (vram_tile.asm DPTCH=0x800; c-hello/README.md:
"a payload writing at row*2048 bits lands on scanline row"). VT1200_VRAM_REAL=1
now draws the framebuffer at 2048 bits / 1280 px from x=0. Off by default: the
Level-1 error text is only legible at the derived 128-byte pitch, and that is
what the faithful boot needs. The branch only touches the bitmap, so the
regression baseline is unmoved (PROG_FRAMES=200 -> PC=3FDCE400, samples=198,
3 distinct PCs), and the default frame-3000 render of the Terminal Manager is
unchanged.
Verified end to end on the 3,292-frame Bad Apple clip: RX_OK=3196, TILES_DROP=0,
3292/3292 frames dumped and matched by tile_check.py, and the dumps decode to
1,379 / 4,125 mismatched pixels of 1,228,800 (quantisation) under the hardware
rule versus 3,101 / 9,793 under MSB. MAME's own frame dumps with
VT1200_VRAM_REAL=1 show the clip unsheared.
Revert note for the driver: delete the m_render_real branch and its getenv and
vt1200.cpp returns to the measured 128-byte renderer byte for byte. No second
reader reviewed that citation; it is quoted in the comment for the next one.
The payload assembles KIND_DICT_CHUNK frames into a 32 KB dictionary at
POOL_PTR + 2 * SLOT_BYTES and learns the tile kinds:
* frame_parse.h gains FP_KIND_TILES_FULL (2), FP_KIND_TILES_DELTA (3) and
FP_KIND_DICT_CHUNK (4), exactly frame.py's KINDS table, plus the marker
cells DICT_READY/DICT_BYTES/DICT_BAD and TILES_DROP. A host test parses
the header and compares it with KINDS so the two cannot drift apart.
* dict_chunk() enforces an ascending, contiguous offset (no gap, overlap,
duplicate or reorder; a mid-entry offset only as a continuation), a chunk
that does not overrun the last entry, and no chunk after DICT_READY. A
refused chunk leaves the prefix alone. Readiness is exactly 32768 bytes
(256 whole entries), so a short upload never becomes ready.
* The dispatcher routes STRIP to the blit, DICT_CHUNK to the assembler, and
refuses TILES_FULL/TILES_DELTA without painting, counting them in
TILES_DROP (Task 4 adds the tile blit).
* A compile-time assertion refuses a POOL_BYTES that cannot hold the
dictionary; the 1987 preprocessor does not fold macro arithmetic in #if,
so it is a defined array whose negative length fails the build.
* VT1200_MODE=dict injects both tile kinds before the dictionary, 23
contiguous chunks with a duplicate offset, a gap and an overrunning tail
mixed in, and reads the stored 32 KB back byte for byte.
Ruling R14: the frame CRC now covers header bytes 0..11 only. A
payload-wide CRC cost the terminal ~139 instructions per payload byte
(the parser, not the wire or the blitter, was the frame-rate ceiling),
while every datagram already carries an Ethernet FCS the LANCE checks
and the length bound catches truncation. frame.py, fp_parse and the
harness's own frame signer follow the new rule; a corrupted payload
byte is now ACCEPTED and a corrupted header byte rejected, stated
explicitly in both test suites, and the C golden vectors were
regenerated from frame.py.
Measured after: RX_READY_A write -> first RX_OK read is 1 emulated
frame for a 20,480-byte frame (84 before), which is the frame hook's
resolution.
Ruling R15: draw mode deletes its dump files before the run (wrapper
and Lua) so a failed run cannot be judged on the previous screen; a
wait that hits its cap reports "timed out waiting for the frame" and
the checker reports it as a timeout, not as pixel mismatches; the
checker asserts the VRAM canary word is 0xA5C3, so a payload that
stopped arming it fails instead of passing quietly.
rx mode now corrupts a header byte in slot B, because a payload flip is
accepted by design under R14.