Commit Graph
15 Commits
Author SHA1 Message Date
arti 6febcf00f4 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.
2026-10-31 23:27:27 +02:00
arti 5af949b338 fix(tiles): pack LSB first, the terminal's scanout order; render the real layout
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.
2026-10-31 18:06:14 +02:00
opencode.energia.arti.ee a84f5f2811 fix(tiles): early status on dictionary failure, decode-time field, chunk bound 2026-10-31 03:35:14 +00:00
opencode.energia.arti.ee 79e27680c9 feat(tiles): bad apple tile stream end to end 2026-10-31 03:06:04 +00:00
opencode.energia.arti.ee 8f8454461d feat(tiles): delta frames and periodic keyframes 2026-10-31 02:38:40 +00:00
opencode.energia.arti.ee 0b726fda3a feat(tiles): dictionary storage and kind dispatch in the payload
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.
2026-10-31 01:55:06 +00:00
opencode.energia.arti.ee 84349d6da3 feat(tiles): chunked dictionary upload protocol 2026-10-31 01:35:22 +00:00
opencode.energia.arti.ee a6e4ac44ad feat(tiles): deterministic dictionary builder and quantiser 2026-10-31 01:32:02 +00:00
opencode.energia.arti.ee 22c539bf74 fix(stream): ring ownership window, FCS-error accounting, unit loader procedure 2026-10-31 01:25:14 +00:00
opencode.energia.arti.ee 53b1580abd feat(stream): paced sender and terminal status frames 2026-10-31 00:26:26 +00:00
opencode.energia.arti.ee 16148d0d24 fix(stream): header-only frame CRC; draw-mode evidence guards
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.
2026-10-30 23:59:20 +00:00
opencode.energia.arti.ee 487bb1789a feat(stream): frame parser shared by host tests and payload 2026-10-30 23:16:48 +00:00
opencode.energia.arti.ee 1a68630534 feat(stream): 1-bpp row packing and strip frame encoder 2026-10-30 22:21:24 +00:00
opencode.energia.arti.ee 11375a1323 fix(stream): seal_header rejects over-length input; payload_len must match payload 2026-10-30 22:17:48 +00:00
opencode.energia.arti.ee 16a52bb853 feat(stream): frame header and CRC16 shared by both ends 2026-10-30 22:15:18 +00:00