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.
This commit is contained in:
arti committed 2026-10-31 18:06:14 +02:00
1 parent 173f3bebda
commit 5af949b338
15 files changed
+261 -141

No files matched your search

+8 -4
View File
@@ -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
+5 -1
View File
@@ -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 —
+26
View File
@@ -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.
+4 -3
View File
@@ -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
+39 -4
View File
@@ -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<long> 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())
+19 -2
View File
@@ -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``
+2 -1
View File
@@ -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
+32 -10
View File
@@ -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():
+2 -1
View File
@@ -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.
Binary file not shown.
Binary file not shown.
+104 -104
View File
@@ -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
}
Binary file not shown.
+17 -9
View File
@@ -1,7 +1,8 @@
"""Turn 1-bpp source rows into framed strip packets for the VT1200 stream.
A *strip* frame is *N* unique rows of 1280 pixels, each row 160 bytes packed MSB
first. The terminal blits row *n* to destination row ``n * stretch`` with the
A *strip* frame is *N* unique rows of 1280 pixels, each row 160 bytes packed
**LSB first** (bit 0 of a byte is the row's leftmost pixel -- the terminal's
scanout order, see :meth:`Rows.pack_row`). The terminal blits row *n* to destination row ``n * stretch`` with the
blitter's vertical stretch, so each source row is reused ``stretch`` times and
the whole screen is repainted from *N* = 1024 / stretch rows. That is the whole
scheme: 128 rows is a full-screen frame at 20,480 B of payload, 8 rows is a
@@ -63,19 +64,26 @@ class Rows:
def pack_row(pixels: list[int]) -> 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:
+3 -2
View File
@@ -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)