From 5af949b33884a87f55460b24bcf0f34f0472733e Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Sat, 31 Oct 2026 18:06:14 +0200 Subject: [PATCH] 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. --- harness/mame/NETWORK_REPORT.md | 12 +- harness/mame/README.md | 6 +- harness/mame/RENDER_REPORT.md | 26 +++ harness/mame/vidstream.lua | 7 +- harness/mame/vt1200.cpp | 43 ++++- harness/udpstream/make_tiles.py | 21 ++- harness/udpstream/tests/test_tilecost.py | 3 +- harness/udpstream/tests/test_vidsrc.py | 42 +++-- harness/udpstream/tilecost.py | 3 +- harness/udpstream/tiles/dictionary.bin | Bin 32768 -> 32768 bytes harness/udpstream/tiles/indices.bin | Bin 38400 -> 38400 bytes harness/udpstream/tiles/manifest.json | 208 +++++++++++------------ harness/udpstream/tiles/stream.bin | Bin 2493 -> 2490 bytes harness/udpstream/vidsrc.py | 26 ++- harness/udpstream/vram_tile.asm | 5 +- 15 files changed, 261 insertions(+), 141 deletions(-) diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md index 33de128..90e5862 100644 --- a/harness/mame/NETWORK_REPORT.md +++ b/harness/mame/NETWORK_REPORT.md @@ -5369,7 +5369,8 @@ add about a quarter: 4 (FCS) + 8 (preamble) + 12 (IFG) = 24 B per datagram, and The clip format is pinned by `make_tiles.load_clip`: a directory of `*.bin` files, each one raw 1-bpp frame — whole 160-byte rows, whole 32-row tile bands, -top-aligned, MSB first, 1 = ink, at most 1,024 rows. A partial row or band is +top-aligned, **LSB first** (bit 0 of a byte is the row's leftmost pixel, the +scanout order), 1 = ink, at most 1,024 rows. A partial row or band is rejected with the file's name, not silently reshaped. With ffmpeg (not installed on this machine, so this route is documented and **not executed**): @@ -5384,9 +5385,12 @@ python3 harness/udpstream/make_tiles.py --clip /tmp/badapple \ --out harness/udpstream/tiles-badapple ``` -`monob` is MSB-first 1-bpp; if the picture comes out negative, invert it in the -filter chain (`...,format=monob,negate`). To keep a run's length independent of -the source, `vidsend.py --tiles` loops a short cache. +`monob` is MSB-first 1-bpp, which is **the wrong bit order for this clip**: the +clip is LSB-first, so each byte of `monob` output must be bit-reversed or every +eight pixels come out mirrored (the real-clip run that produced the tile cache +used its own packer for exactly this reason). Invert in the filter chain +(`...,format=monob,negate`) only if the picture comes out negative. To keep a +run's length independent of the source, `vidsend.py --tiles` loops a short cache. The clip loader is not an untested path in the code sense: the 30-frame synthetic clip written out as `frame-0000.bin … frame-0029.bin` and fed back diff --git a/harness/mame/README.md b/harness/mame/README.md index 7af24ea..29e3592 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -269,7 +269,7 @@ Terminal Manager usable. It is a **value-aware inverse switch**: the default lives in the code, not in a wrapper script setting `VT1200_TABLET`, so `VT1200_MOUSE=1` (or `VT1200_MOUSE=0` for the tablet) always flips it either way. -## Environment knobs (21; down from 33) +## Environment knobs (22; down from 33) Almost all are opt-in and off by default — there is no longer a "working incantation" to remember, because everything the firmware needs to run is now the @@ -283,6 +283,10 @@ the descriptor write-back are read directly), `VT1200_BOARD_LOG`, `VT1200_BOARD_ `VT1200_IRQLOG`, `VT1200_LOG_UNMAPPED`, `VT1200_DPYLOG` (+`_FRAMES`), `VT1200_DUMP_FRAME` (+`_FRAMES`), `VT1200_DUMP_RAM` (+`_FRAMES`), `VT1200_DUMP_VRAM` (+`_FRAMES`), `VT1200_DUMP_DIR`, `VT1200_NO_RENDER`, +`VT1200_VRAM_REAL` (draw the framebuffer at the **real unit's** 2048-bit / +256-byte row pitch, 1280 px wide, instead of the derived 1024-bit / 128-byte +pitch — the view for content written for the hardware, e.g. the tile stream; +off by default, see `RENDER_REPORT.md`), `VT1200_INPUT_LOG` (per-byte keyboard/mouse/tablet TX/RX trace, with emulated time). Behaviour switches: `VT1200_SKIP_LEVEL1`, `VT1200_MOUSE` (put the VSXXX-AA mouse (relative) back on DUART2-B; the VSXXX-AB tablet is the default — diff --git a/harness/mame/RENDER_REPORT.md b/harness/mame/RENDER_REPORT.md index 21698f2..73357a0 100644 --- a/harness/mame/RENDER_REPORT.md +++ b/harness/mame/RENDER_REPORT.md @@ -157,3 +157,29 @@ absent, so the code just blanks the line (as it did before). * The video-RAM dump for a single frame is ~58 MB because the share covers the whole `0x04000000..0x1fffffff` mapping; the framebuffer itself is the first ~128 KiB. + +## 8. The other layout: `VT1200_VRAM_REAL=1` + +Everything above describes the **derived** geometry: 1024-bit (128-byte) rows, +1024 active pixels, drawn at x 192..1215. It is what the firmware's own blitter +uses (`pc=3FDF9A00` advances the destination by `0x40` words per glyph row), and +what makes the Level-1 error text legible. + +The **real unit** stores 1280-pixel rows at a 256-byte pitch — first 160 bytes +the picture, the remaining 96 a `0x55` mask — as `harness/c-hello/README.md` +records from measurement ("a payload writing at `row*2048` bits lands on +scanline `row`"), and as `dwt/screenshot.py --real` (width 1280, stride 256) +decodes from live VRAM. + +A payload can target either. The tile stream targets the real unit +(`vram_tile.asm: DPTCH = 0x800` = 2048 bits), so under the derived renderer it +is sheared by a quarter-row per scanline and wrapped. `VT1200_VRAM_REAL=1` +renders the same VRAM at 2048-bit pitch and 1280 px from x=0 instead. Both modes +read the bits LSB-first, which is the TMS34010 pixel order and the order the +tile pipeline now packs (`vidsrc.Rows.pack_row`); an MSB-first packer is +self-consistent with an MSB-first checker and mirrors every eight pixels on a +display. + +The switch changes only the bitmap, never CPU state, so the regression baseline +(`progress.lua`, `PC=3FDCE400 samples=198`) is unaffected. Default is off: the +real machine runs level 1, and the derived geometry is what that text needs. diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua index 7cb4d79..dc0f1df 100644 --- a/harness/mame/vidstream.lua +++ b/harness/mame/vidstream.lua @@ -268,8 +268,9 @@ local RMD1_STP = 0x0200 local RMD1_CRC = 0x0800 local RMD1_ERR = 0x4000 --- The frame every mode delivers first: 8 rows of 1280 pixels, 1 bpp MSB-first --- (160 bytes a row), each row repeated 8 times. 1280 B of payload -- the same +-- The frame every mode delivers first: 8 rows of 1280 pixels, 1 bpp LSB-first +-- (160 bytes a row, bit 0 of a byte is the leftmost pixel), each row repeated +-- 8 times. 1280 B of payload -- the same -- geometry vidsrc.py's ROW_BYTES produces, so Task 5 can blit it unchanged. -- 1296 bytes is ONE datagram, which is what mode rx wants. local RX_ROWS = 8 @@ -1818,7 +1819,7 @@ local function tile_byte(entry, row, col) end -- The 32 KB dictionary as a string: 256 entries of 32 rows of four bytes, --- row-major, MSB-first within each byte -- exactly tilecost.py's layout. +-- row-major, LSB-first within each byte -- exactly tilecost.py's layout. local function tile_dictionary() local out = {} for e = 0, 255 do diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 617a98c..5c4e99b 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -802,6 +802,16 @@ private: // times -- the instrument for telling "progressing" from "stuck": if two // samples minutes apart are byte-identical, nothing is being drawn. bool m_render = true; + // VT1200_VRAM_REAL=1: render the framebuffer at the REAL unit's geometry + // (2048-bit/256-byte row pitch, 1280 px wide) instead of the derived + // 1024-bit/128-byte pitch MAME's own screen geometry implies. Off by + // default: the Level-1 error text is only legible at 128 bytes (see + // RENDER_REPORT.md), so this is for looking at content written by a payload + // that targets the real machine -- e.g. the tile stream, whose + // vram_tile.asm sets DPTCH=0x800 (2048 bits). The bit order is LSB-first in + // both modes, which is the TMS34010 pixel order and matches + // dwt/screenshot.py's decode of live VRAM. + bool m_render_real = false; long m_dump_frame = -1; std::vector m_dump_frames; // VT1200_DUMP_FRAMES=3000,10000,... std::string m_dump_dir = "."; @@ -912,6 +922,8 @@ void vt1200_state::machine_start() // Video: render by default; optionally dump a frame as PPM. if (getenv("VT1200_NO_RENDER")) m_render = false; + if (getenv("VT1200_VRAM_REAL")) + m_render_real = true; if (const char *f = getenv("VT1200_DUMP_FRAME")) m_dump_frame = strtol(f, nullptr, 0); if (const char *f = getenv("VT1200_DUMP_RAM")) @@ -1477,6 +1489,14 @@ void vt1200_state::log_lance_dma(char kind, u32 addr, u16 value, u16 mask) // // The screen line maps 1:1 to a framebuffer row, offset by the vertical // blanking start (params->veblnk). +// +// VT1200_VRAM_REAL=1 switches to the REAL unit's layout instead: pitch 2048 +// bits (0x800 / 256 bytes), 1280 px wide, drawn from x=0. It exists because a +// payload can write for the real machine and then look sheared here -- the tile +// stream's vram_tile.asm sets DPTCH=0x800 and dwt/screenshot.py --real reads +// stride 256 at 1280 wide. With the switch off nothing about the geometry +// changes: this is a view of the same VRAM at the other documented pitch, not a +// different machine model. void vt1200_state::render_active_line(bitmap_ind16 &bitmap, int scanline, const tms340x0_device::display_params *params) { @@ -1492,11 +1512,26 @@ void vt1200_state::render_active_line(bitmap_ind16 &bitmap, int scanline, return; const u16 *const vram = m_vram.target(); - const int x0 = (int)params->heblnk; - const int x1 = (int)params->hsblnk; - const u32 row_bit = (u32)fby * VMEM_PITCH_BITS; + // Two geometries, one per documented layout (harness/c-hello/README.md, + // "Geometry is not portable between the real unit and MAME"): + // + // derived (default) pitch 1024 bits, 1024 px, x = heblnk + fx + // real (VT1200_VRAM_REAL=1) pitch 2048 bits (0x800, 256 bytes), 1280 px, + // x = fx -- the real raster is 1280 wide, so there is + // no blanking offset to add + // + // The real numbers are the ones a payload targeting the hardware writes: + // vram_tile.asm sets DPTCH=0x800 and dwt/screenshot.py --real reads stride + // 256 / width 1280. Revert note: delete the m_render_real branch and the + // environment read and this file returns to the measured 128-byte renderer + // byte for byte; nothing else depends on it. + const u32 pitch = m_render_real ? 2048u : u32(VMEM_PITCH_BITS); + const int width = m_render_real ? 1280 : VMEM_WIDTH; + const int x0 = m_render_real ? 0 : (int)params->heblnk; + const int x1 = m_render_real ? bitmap.width() : (int)params->hsblnk; + const u32 row_bit = (u32)fby * pitch; - for (int fx = 0; fx < VMEM_WIDTH; fx++) + for (int fx = 0; fx < width; fx++) { const int x = x0 + fx; if (x >= x1 || x >= bitmap.width()) diff --git a/harness/udpstream/make_tiles.py b/harness/udpstream/make_tiles.py index fd9719a..527f0d4 100644 --- a/harness/udpstream/make_tiles.py +++ b/harness/udpstream/make_tiles.py @@ -198,12 +198,24 @@ def _silhouette(rows: list[int], n: int) -> None: rows[y] |= _span(x0 + 205, x0 + 250) & _MASK # the "eye" +#: Bit-reversal table: the synthetic clip is built as pixel ints with pixel 0 +#: at the top bit, but the terminal's scanout reads bit 0 of a byte as the +#: leftmost pixel, so every byte is reversed on the way out (one table lookup, +#: not 1,280 shifts a row). +_BITREV = bytes(int(format(value, "08b")[::-1], 2) for value in range(256)) + + +def _pack_row(row: int) -> bytes: + """Pack one row int as the terminal reads it: LSB first within each byte.""" + return row.to_bytes(ROW_BYTES, "big").translate(_BITREV) + + def _synthetic_frame(n: int) -> bytes: rows = list(_background_rows()) for y in range(SCREEN_ROWS - 160, SCREEN_ROWS): rows[y] = 0 # a black ground band _silhouette(rows, n) - return b"".join(row.to_bytes(ROW_BYTES, "big") for row in rows) + return b"".join(_pack_row(row) for row in rows) def synthetic_clip(frames: int = 30) -> list[bytes]: @@ -244,7 +256,12 @@ def load_clip(directory) -> list[bytes]: """Read a clip directory's ``*.bin`` frames, sorted by name. Each file is one raw packed 1-bpp frame, exactly what - :func:`harness.udpstream.tilecost.quantise` takes. A ``.bin`` that is not + :func:`harness.udpstream.tilecost.quantise` takes. The bit order is the + terminal's scanout order: **LSB first** -- pixel *x* of a row is bit + ``x % 8`` of byte ``x // 8`` (:meth:`harness.udpstream.vidsrc.Rows.pack_row`). + An MSB-first packer is self-consistent with an MSB-first checker and mirrors + every eight pixels on the display, so the order is stated here rather than + left to the producer. A ``.bin`` that is not whole rows and whole tile bands is an error naming the file, not a silent reshape -- the terminal's 1,280-byte frame is only meaningful for a whole grid. The sort is by name, so number the frames zero-padded: ``frame-0007`` diff --git a/harness/udpstream/tests/test_tilecost.py b/harness/udpstream/tests/test_tilecost.py index ae79eca..3511986 100644 --- a/harness/udpstream/tests/test_tilecost.py +++ b/harness/udpstream/tests/test_tilecost.py @@ -3,7 +3,8 @@ The plan's two tests come first, verbatim. The extras pin down what the plan text leaves implicit but the later tasks depend on: -* the tile layout -- 32 rows of four bytes, row-major, MSB first -- so a +* the tile layout -- 32 rows of four bytes, row-major, LSB first (bit 0 of a + byte is the leftmost pixel, the scanout order) -- so a dictionary entry is exactly the bytes the terminal's ``PIXBLT B,XY`` reads from ``dict + index*128``; * the index frame's size. The plan says ``quantise`` returns 1,280 index bytes diff --git a/harness/udpstream/tests/test_vidsrc.py b/harness/udpstream/tests/test_vidsrc.py index 9c7932a..fb38af8 100644 --- a/harness/udpstream/tests/test_vidsrc.py +++ b/harness/udpstream/tests/test_vidsrc.py @@ -20,20 +20,19 @@ from harness.udpstream.vidsrc import ( CHUNK = 1450 -def test_pack_row_is_msb_first_and_160_bytes(): +def test_pack_row_is_lsb_first_and_160_bytes(): px = [0] * 1280 px[0] = 1 px[7] = 1 px[8] = 1 b = Rows.pack_row(px) assert len(b) == 160 - # Pixel 0 -> bit 7, pixel 7 -> bit 0, pixel 8 -> bit 7 of byte 1. - # NOTE: the plan/brief write this as 0b11000001, which is 0xC1 and encodes - # pixels 0, 1 and 7 -- three pixels from a two-pixel segment of the row, so - # no packing rule can produce it from px[0] and px[7]. MSB-first, as the - # same brief's prose states and as the terminal's bit-addressed row needs, - # is 0b10000001. See the task-2 report. - assert b[0] == 0b10000001 and b[1] == 0b10000000 + # Pixel 0 -> bit 0, pixel 7 -> bit 7, pixel 8 -> bit 0 of byte 1: the + # terminal's scanout reads bit 0 of a byte as its leftmost pixel + # (dwt/screenshot.py decodes live VRAM the same way). The plan carried an + # MSB-first pair here (0b11000001 / 0b10000000), which is self-consistent + # with an MSB-first checker but mirrors every eight pixels on a display. + assert b[0] == 0b10000001 and b[1] == 0b00000001 def test_strip_source_rejects_stretch_that_does_not_divide_1024(): @@ -54,13 +53,36 @@ def test_strip_frame_round_trips_through_the_header(): # --- packing ------------------------------------------------------------- +def _decode_like_the_terminal(row: bytes) -> list[int]: + """Decode a packed row the way the VT1200 scanout does. + + The same rule ``dwt/screenshot.py`` uses on live VRAM: bit ``x % 8`` of + byte ``x // 8`` is pixel ``x`` -- LSB first within each byte. + """ + return [(row[x // 8] >> (x % 8)) & 1 for x in range(1280)] + + +def test_pack_row_decodes_back_to_the_pixels_the_terminal_shows(): + """A row must survive the terminal's own LSB-first scanout decode. + + The failure this pins: an MSB-first packer is self-consistent with an + MSB-first checker, so every MAME replay check passes while the screen + mirrors every group of eight pixels. Decoding the packed row with the + hardware's rule is the only reading that can catch it. + """ + px = [1 if i % 3 == 0 else 0 for i in range(1280)] + px[0] = 0 # asymmetric ends, so a mirror cannot pass + px[1279] = 1 + assert _decode_like_the_terminal(Rows.pack_row(px)) == px + + def test_pack_row_puts_every_pixel_where_the_terminal_reads_it(): - """Pixel i is bit (7 - i % 8) of byte i // 8, i.e. MSB first, and no bit leaks.""" + """Pixel i is bit (i % 8) of byte i // 8, i.e. LSB first, and no bit leaks.""" px = [1 if i % 3 == 0 else 0 for i in range(1280)] b = Rows.pack_row(px) assert len(b) == 160 for i, v in enumerate(px): - assert (b[i // 8] >> (7 - i % 8)) & 1 == v, i + assert (b[i // 8] >> (i % 8)) & 1 == v, i def test_pack_row_rejects_a_row_that_is_not_1280_pixels(): diff --git a/harness/udpstream/tilecost.py b/harness/udpstream/tilecost.py index 9549d41..960993e 100644 --- a/harness/udpstream/tilecost.py +++ b/harness/udpstream/tilecost.py @@ -13,7 +13,8 @@ Geometry The screen is 1280x1024, one bit per pixel. A source row is therefore 160 bytes (:data:`harness.udpstream.vidsrc.ROW_BYTES`) and a 32x32 tile is 32 rows of **four** bytes -- 32 px is two 16-bit words -- for 128 bytes: row-major, two -bytes per word, MSB first within each byte, the same bit order +bytes per word, **LSB first** within each byte (bit 0 is the leftmost pixel, +the scanout order), the same bit order :meth:`harness.udpstream.vidsrc.Rows.pack_row` produces. The grid is 40 columns by 32 rows: 1,280 tiles. diff --git a/harness/udpstream/tiles/dictionary.bin b/harness/udpstream/tiles/dictionary.bin index 252b3537c249f90083ae96f0066da5f4ed55ca39..d3842ce80925ca68bb12183b78398f95da1a22da 100644 GIT binary patch literal 32768 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 Q{z+oQSNz&_mAu-20nV=2K>z>% literal 38400 zcmeI5cYG8@6vqz@f+P`a*hN%g7wiptEJ-X;1W}X-me7p7fnCs8v6t98c2N)&yI8;$ zd#_P~y_;M}qIZ{@nK!%h-agHIZfgXw=J9#>sb4%|WIe=`7wWl5P@=fGtpB-p|{I}>XZ`?Kg`D-qjIG)^% zTe)T3z)9QGZ#`qHx`BVw|2`1*olE@trM>9|VPEPLM%LDzdUWme_db>?sr?cw^zF0i zDl4zF;_7|-ko#3VC%Ua8_e%^rQAn<9LBYZcEmF`GE1$V~@|h>RK4sd1qCZ}=&8idj zi7I8FNXwghux&ruzwxPcWxoh_$`-4?;W z>XSrVW8BMbuk!q(uk6a=X6}i4&p$Jhg?*w*+xE*>{$0r9-*}t(-vBoM8U!}#iS%!7 zTYHmqr`hWrThnzWSFKZF}JHqqN^N-SSXX}(=b9?>BQ~q5p ztU%j!2dn(sn9aXWIQ*N}*8YWbD>05zF8>y1@~`3{*)7e#iqk#+A`%MeXp!(ErLwqC zuDOipP9B7G6FMD2SFx&cWk1h9B@@g1`?0P4ed$)!OXz=dT1ZM0S5t-vN+u$(oN_fq?^#`gwzz%4X*5$r~JD@2!eEHq1F7aXevAZdt2&|xc~j5 ztzF$S5#u?wPm-CJxdORe*WkAr<4nf3lQfjeo6nvsM3}F0J>x>n0!rEf=0~h2)9V{Zzhj_ z>ssaCMr{6ls;tp@{*`k1*Ne%&vKxRCs1PVSPh}&Xf6AxevhUx2p#)^H&hrnx4>(g2 zl{E3T`OLp>o7+Gj;p5nY%fF?V{2P2HlYjf1!{y&~{{9#EQi{{(f4HpwWsT<_&vdO=0nfIPq_rI+C?`K#18v{d?vfrSKxcs}(^A8#oa8KOXxQbuOzj3N_Wj_fT zgHMHT)1~?DAbb!=J;Ifs5%T``{wL*WQ9dlT%D*B${~lEX!S8?nWb$t`H~-80?t}6C zkIlbP7jyYH#q-bej|#<3*LnVd{Ex}MwNt-)v-ke_JxYzXy2v zpI!cK!scHChkuR9<}ZP%P%V~o`L`sKe`T+NZprumt_1G${4*=u6bq)9aTUL)u3}c< z%6=wantX0n2M8dm{8QGT+_=5f{BLtM|32jK?}ucwTqGd{By=1@Z1ZpMYfS!CUIpAo z^6w>||CwR}#*R`fFsk7oARXwY&_QG+Xav<&%qm>j5BkvHE_qsc$iE)D+2mh$KL6eV z`a_@pS?6EJx-Zes{|?pMNQA*iHveAn{L_pYqSd4cGEgD{3P^`U99an(A^aPwDp&T4 zaF;x-obvBM*)4Vd)^lH*{9ChQ-7C)gFO)yZzt&_kj2TpnqnY_%59|Cp9QvNh|MpL< zd*y%sK|k41iiPS3Qw*RhTnQQ>D?uZuu3}aJh7Lv-wgOoR8VMf>DnTPK24K7fx95_7 zH&0dPO1QCqdfh*3^B;WvH9&BoEcGQds z9UllVrC4I8RAV6Y0xjr0k(HnkR97*paAm(B=diyQc>Zn1&i`2ao7b9T@vn@_za_Z* zJ6;PUJPazIg1)BmzaxGA7b$;N@r(O6UNx@l2V+1BoWNx)QUKY58JE;d%hdaSw}3_n zFYahF|Ld{^n}6>?(_s1EPfY#|;qq@eCjW-XB~3FP!R=S|H1xgapIPBlOhEg&ieKEn z@v3oUKj=fs*JgD<5_Dwj0A1lq&v2}X<0 z|BCxwQKI9`u6rLj|52@VOWgm~JPQXx@h`pZy?_1(L}^gAtN2Cr60-_d_Jc8yC;Xdz z8;~0A#*zbV@^5|1{QJu?{|dSMQ%gEWYwz8y4zd$E7!yGOR>KluFnN;4Uf|258U{YV=%HV%>Z zi4xnd7n6VGYDrU?f8}72{FJ8%{#E<@4@@{96BDlD7t>4Jw!5;Q7R-om(}AP~GjR|= z*MH^ToI5}xxF5H(%D>+1{O@CJ^a%gHWAg6+!97u8<6VZyzoBYLQ<{H!gGKUb{$;-V z=I?)jC=JSX6~CBX;7K&*r{6On<6!51V%|fPNPCH~dbjxe2V5kdgFdf8gyFMy>^tSD1@@4Sami{}3{L{Ke{kV=*{tXb^6D7*OuXRr${A-!t zs(T6L}}%J|Ay&|yIqf}|1E?{nQ#X8tuY`B%Z^UyUQ)d2s-!ea7KeHhBkSz?Y_&(N*F3hv*aC?8<&7Uy5rQEc1mTv+vVjf2=lYhlr{=p@k$^1`SB%k8naL+$ZjJS$lv##Q| F^&c;ou#*4) diff --git a/harness/udpstream/tiles/manifest.json b/harness/udpstream/tiles/manifest.json index d017cbe..7222ffa 100644 --- a/harness/udpstream/tiles/manifest.json +++ b/harness/udpstream/tiles/manifest.json @@ -1,68 +1,68 @@ { "active_row_bytes": 160, "bit_errors": [ - 1308, - 1351, - 1420, - 1366, - 1309, - 1425, - 1442, - 1507, - 1389, - 1574, - 1444, - 1342, - 1446, - 1260, - 1556, - 1459, - 1592, - 1472, - 1369, - 1524, - 1509, - 1581, - 1506, - 1505, - 1514, - 1719, - 1540, - 1595, - 1610, - 1809 + 953, + 778, + 999, + 880, + 868, + 1020, + 801, + 944, + 801, + 955, + 778, + 964, + 1086, + 985, + 823, + 1056, + 895, + 1019, + 882, + 890, + 1041, + 1038, + 1031, + 1043, + 910, + 1011, + 1169, + 1173, + 1195, + 1132 ], "changed_tiles": [ 0, - 12, - 14, - 13, - 7, - 15, - 11, - 11, - 11, - 10, - 11, - 11, - 10, - 12, - 9, - 14, 10, 8, - 13, - 15, - 9, 14, + 9, + 12, + 11, + 12, + 14, + 10, + 12, + 13, + 10, + 13, + 10, 13, 11, + 10, 12, + 13, 12, - 12, - 12, + 10, + 13, + 9, + 13, + 13, 11, - 10 + 11, + 12, + 11 ], "clip": "synthetic_clip", "delta_frames": [ @@ -96,7 +96,7 @@ 28, 29 ], - "delta_payload_bytes": 1057, + "delta_payload_bytes": 1054, "dictionary_bytes": 32768, "entries_used": 256, "frames": 30, @@ -108,71 +108,71 @@ ], "payload_bytes": [ 1280, - 38, - 44, - 41, - 23, - 47, - 35, - 35, - 35, - 32, - 35, - 35, - 32, - 38, - 29, - 44, 32, 26, - 41, - 47, - 29, 44, + 29, + 38, + 35, + 38, + 44, + 32, + 38, + 41, + 32, + 41, + 32, 41, 35, + 32, 38, + 41, 38, - 38, - 38, + 32, + 41, + 29, + 41, + 41, 35, - 32 + 35, + 38, + 35 ], "psnr_db": [ - 30.00902182299432, - 29.868545772656496, - 29.652215819046237, - 29.820592269421663, - 30.005702797369246, - 29.636950619431516, - 29.585446659042702, - 29.393966739730484, - 29.748076805500645, - 29.205051982646346, - 29.579427330540604, - 29.897574104547072, - 29.573416333291682, - 30.171393811701176, - 29.255003336340103, - 29.534546343942285, - 29.155668628860305, - 29.496021162862004, - 29.811064781536906, - 29.345249592840986, - 29.388206865120907, - 29.185780563554715, - 29.396849544229987, - 29.399734263578182, - 29.373840511236263, - 28.82234049600628, - 29.299892054512174, - 29.1474923889448, - 29.106840502558306, - 28.600713594278666 + 31.384170256493537, + 32.265303292979915, + 31.17944438061698, + 31.73027254137512, + 31.789902011111884, + 31.08909754525763, + 32.13877410203443, + 31.425379319896113, + 32.13877410203443, + 31.37506554703934, + 32.265303292979915, + 31.334328923848496, + 30.81680101034852, + 31.240736957900687, + 32.02110091075411, + 30.93846008089887, + 31.656868909717684, + 31.093357422812538, + 31.720413411558606, + 31.681199196427677, + 31.00059196777144, + 31.01312572775241, + 31.042512610041637, + 30.992256178611495, + 31.584685339665867, + 31.127587706966793, + 30.4969541512584, + 30.482119141721512, + 30.401420210035237, + 30.636634994354278 ], "screen_rows": 1024, "seed": 0, - "total_payload_bytes": 2337, + "total_payload_bytes": 2334, "unchanged_frames": [], "version": 1 } diff --git a/harness/udpstream/tiles/stream.bin b/harness/udpstream/tiles/stream.bin index 64f052292c4f16878f92df9f438896a5deef58b1..8d1f32db7ba532cd9a97d48f0bf2e4cc064031c0 100644 GIT binary patch literal 2490 zcmds$X+Ts}7>3Vxjws9q!y<|>0)rGp$s(w!QAWr(XcihqXl7Q}3W8cXF5ohmX{%XORJN(0Y2Q6Fm$dXUKl)Sm&-pRWJnwtH@7}RFiAfql z2_ZfPqsi*9*j-(nUO_17+T>31GS=WtH^JED1w0pg=GnK_z4&3tyepl{JQu~;NIf^3 z-)Nb=Xw|lTQjoX5(d&(-A%lhu9gOD%>F208-|r-z-_D-*+}34~>X1Hn>84$4YzK?* zl;TKVjW#?&qlv;(k}I6mxmHPz-)46TF>9OSFZC|E` z>YUk|9jqIPAbuX76HyVoVc+LJ=8kWgxP|$USfW5>DlEWawo8N-TB@Xj9j=3sQ;$q) zX5ERF$Vn=sA?gGz{VUf@`)!MBS4VAhJ^B1ytOr-BxUv>Hkkn(t65h~Er^hF@W(Ml( z^iNkm#C$y$64Gm-5t{#j8xxX_fB$K2Sl*7j<<{-Yk55r_H-cz3(-|X%9{XW}ee%i| zn2Z-tf(iy;iLy?!9!r?%p_%GYu{FQiA6zo!btWfqL`g(csL%n?RLk2|A$<7pt5;di z*a9_kR^PXiDR_DrY=;J1Pdkz$@ovRXfbN5XuTmJ?aiN$ z=Jy^qrfe-!aitPcuxlxHZ2&=8LO0L`#jP;#mxnD2$1;D9GKC{zdK8VuY(d!=6Ew_p z+VX(EX4>K^7QpwU;KCZ*R69gr&uLux(xPiyzgZKrJJa|mQ}bvA2;rqrkK`F7bx6MK zO1ZA>vy7g@vsoaYr2^(7QiG5i5p>1a@fAq@_@{Bba`ujywTA`qmI_ElpNNZ2;SaAW zZwwk_{N3?LM8W<-7R<$R5P3lziRo_j`q%p17TU2(x!G~Rz(ROYx7La)I*ni!Euxh) z{`LVUzPd@J(-p9uJYDoz;V=iY&{%}om9*TRHT#p3$A(YblIz^cLivXwgL&B0%5`q; zZTXP*!zVpAzKQkXJ{ia%38}yPD(E{E%2>lW>!$mwr+#!d>wOWOjC?ipr6aJUju!I# zcPryt!cGKcF1KdZ9${g8LvHgSIK^2vS3xb;^go#$l9y5EdW!YooY1lwUko>w#f|0k zJ<}Ja{63Rc=IfB28Sf9@amdw##)x#zr@maObVLJ1+^ zUcqU%iEdA@Gb9K_f-A{RA=-LF$tEcVZNS%{qtg1uBW=&#ce?X#U!(Xo0lugy`OK3fkMo8I4VjVO|5(I48)O9 zDM_ixi3#!PDMc(5RfBtZdX<=JS>dSL1E#iJ70cyeNB1CmYx5X{R{1H^9suQ z1Ur3e9d!Nw?e_~j-~Y}n17aW=5)I>_4(zZM(rE^rPG`_ET2437dos?LctDr7)LXo= zh*2_z=n$QNXgi_@;Ru}a70CF#tFpSqKfHS~(p%QpZexs$CR(BgS-l;);4t)K!8CMO zfe!aOFUk4v*r&$9=I!Px7Di%81o3CSKTcXrKs5RTV ze)HTIvt;+Y>bX1FAd*JZB$sz4dG|vaP4_t)>O8g+t?GEs;#wBLIUUz-gOf;^G2wEa z@VPPLJ~Bp|T&BD1ERu6lhE6!-$GNC*;n(gN!wNUgT-(T^NHS3oNv{Uec7mejqR(Fz zG3m!eZp*q&OvS~tpo1L5+7OeBB~uq|e?hBx=J$mO3pQ+FYR=_wF_}umcoNq{&)PWF z-q)mUSna%pX}GBtwOZi-PNol1X+9^n=63gdmOs?=&Vx+LXDwSQm;_l-u!(vvKJUHn zj?OB4x8R}InT{loC^8PyRimP;WkpCeEwVH5W^w-8OwYGL3odh-dx_2(W}ui49n^Ty?SH|)m*oybf@sswwn z(gylu=DyE+`rnVvugY)n7PA;0T9&K=sigNG1r7AAr!I>5;G64WXIEdfy@ti|iA(a8 z7+RtS#NC%i*bn{^H?P*RgN@+BR6#Z;-F_hiy4-N(Xs bytes: """Return the :data:`ROW_BYTES` packed bytes for ``pixels``. - Pixel *i* becomes bit ``7 - i % 8`` of byte ``i // 8`` -- MSB first, - which is the bit order of the terminal's line buffer. A truthy pixel is - a set bit, so ``True``, ``1`` and any other nonzero value all mean ink. + Pixel *i* becomes bit ``i % 8`` of byte ``i // 8`` -- **LSB first**, + which is how the terminal's scanout reads the framebuffer: ``dwt/ + screenshot.py`` decodes live VRAM with exactly this rule, and MAME's + renderer does the same (``(vram[bit >> 4] >> (bit & 15)) & 1``). + + The earlier MSB-first convention here was self-consistent -- the encoder + and ``tile_check.py`` shared it, so every replay check passed -- but it + mirrors every eight pixels on a real display. A truthy pixel is a set + bit, so ``True``, ``1`` and any other nonzero value all mean ink. """ if len(pixels) != ROW_PIXELS: raise ValueError("a row is %d pixels, got %d" % (ROW_PIXELS, len(pixels))) # "1000000..." -> the integer those bits denote, big-endian bytes. - # Equivalent to setting bit 7 - i % 8 of byte i // 8 for each pixel i, - # but the join+parse is C-speed per character and measured ~1.5x a - # per-pixel shift loop on this box (65-120 us vs 100-150 us per row). + # Pixel i must land on bit i % 8 of byte i // 8, so reverse each + # eight-pixel group before the parse: the first character of a group + # becomes that byte's MSB, and the last becomes its LSB. bits = "".join("1" if value else "0" for value in pixels) - return int(bits, 2).to_bytes(ROW_BYTES, "big") + lsb = "".join(bits[i:i + 8][::-1] for i in range(0, ROW_PIXELS, 8)) + return int(lsb, 2).to_bytes(ROW_BYTES, "big") class StripSource: diff --git a/harness/udpstream/vram_tile.asm b/harness/udpstream/vram_tile.asm index a08daaf..1164931 100644 --- a/harness/udpstream/vram_tile.asm +++ b/harness/udpstream/vram_tile.asm @@ -1,8 +1,9 @@ ; vram_tile.asm -- PIXBLT B,XY: one 32x32 1-bpp dictionary tile to the screen. ; ; Task 4's tile primitive. A dictionary entry is 32 rows of FOUR bytes (32 px -; is two 16-bit words) = 128 bytes = 1024 bits, row-major, MSB first within each -; byte -- tilecost.py's layout. PIXBLT's B source form reads packed 1-bit +; is two 16-bit words) = 128 bytes = 1024 bits, row-major, LSB first within each +; byte (bit 0 is the leftmost pixel -- the scanout order) -- tilecost.py's +; layout. PIXBLT's B source form reads packed 1-bit ; pixels from a linear bit address and writes an XY destination, so one tile is ; ; SADDR = dict_base + index * 1024 (128 bytes a tile: index << 10)