mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-11-01 12:14:56 +02:00
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:
1 parent
173f3bebda
commit
5af949b338
15 files changed
+261
-141
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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.
@@ -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.
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user