docs(badapple): how the clip data is prepared, with the measured costs

The setup section said 'a clip cache' without saying where a clip comes from,
what it costs, or how to tell the result is sane.  Adds a Data preparation
section: the public sources (and why an upscale is not better), the frame
extraction (3,292 frames, 489 MB, ~12 s, LSB-first), the cache build (~2 min
47 s, four files, 6.1 MB), and the make_tiles summary to compare against --
including that a saturated 256-entry dictionary and 24 dB PSNR are expected, not
a failure.
This commit is contained in:
arti committed 2026-10-31 21:27:09 +02:00
1 parent 8f831eaf33
commit d6aa6f46c1
1 file changed
+77 -12
+77 -12
View File
@@ -8,7 +8,7 @@ against MAME's debugger API and is not how anything reaches a machine.
```
harness/badapple/
├── README.md this file
├── README.md this file (setup, data preparation, run)
├── demo.sh one command: wait for the OS, load, send
├── rdebug_load.py payload image → terminal RAM over rdebug (TCP 771)
├── clip_from_video.py video → raw 1-bpp frames for make_tiles.py
@@ -42,28 +42,93 @@ Two facts about it are load-bearing and were measured:
## Setup
Two builds and one piece of data preparation. Steps 1 and 2 are C builds; step 3
is the part that needs a source video.
```sh
cd /path/to/vt1200
# 1. MAME + the driver (skip if you already have vt1200 built)
# 1. MAME + the driver (skip if vt1200 is already built)
harness/mame/build.sh
# 2. the payload (TI SDK under DOSBox)
harness/udpstream/build-stream.sh # -> harness/udpstream/build/vidstream.bin
# 2. the payload (TI SDK under DOSBox) -> harness/udpstream/build/vidstream.bin
harness/udpstream/build-stream.sh
```
# 3. a clip cache (skip to use the committed 30-frame synthetic clip)
Step 3 is described below, because the numbers (time, disk, what "good" looks
like) are what make it repeatable.
## Data preparation: a video to a tile cache
Nothing ships a full clip: `harness/udpstream/tiles/` in the repo is the
**30-frame synthetic clip**, which is enough to smoke-test the transport but shows
a moving ellipse, not the animation. A real clip is three commands and about
five minutes.
### 3a. Get a source video
Any file ffmpeg can read. For Bad Apple the usable public sources are:
| source | why |
|---|---|
| `archive.org/details/hd-60-fps-bad-apple-shadow-art-pv-720-x-960-60fps` | 960x720, 60 fps, 219 s, ~35 MB — the one this demo was measured on (the item name is scrambled; the file inside is 960x720) |
| `yt-dlp <url>` from YouTube | fine, but the 4K/8K uploads are upscales of the same 2009 render and add no detail |
| the original PV's Flash file (`archive.org/details/dagobah_Bad_Apple_HQ`) | the true master, vector — needs a Flash renderer to rasterise, which nothing here does |
Resolution matters less than it looks: the terminal shows **1 bpp**, and the
pipeline scales to 1280 px wide before thresholding, so anything at 960 px or
more lands on the same picture.
### 3b. Frames
```sh
python3 harness/badapple/clip_from_video.py bad-apple.mp4 /tmp/badapple --fps 15
```
* one `*.bin` per frame, 1280x960, 1 bpp, 160-byte rows, **LSB first** — bit 0 of
a byte is the row's leftmost pixel, the order both the terminal's scanout and
the payload's encoder use. Do not substitute `ffmpeg`'s `format=monob`: it
packs MSB first and mirrors every eight pixels on the display.
* `--fps 15` on the 219 s source gives **3,292 frames**; `--height 960` fills 30
of the 32 tile bands (a 4:3 source at 1280x960, the bottom two rows black), and
`--height 1024` fills the screen by stretching 4:3.
* measured cost: **~12 s**, **489 MB** on disk (153,600 B per frame).
* the source is white paper with black silhouettes, which is already the right
polarity. If yours is inverted, add `--invert`; `--threshold` (default 127)
decides what counts as ink.
### 3c. The cache
```sh
python3 harness/udpstream/make_tiles.py --clip /tmp/badapple --out /tmp/badapple-tiles
```
`clip_from_video.py` writes **LSB-first** 1-bpp rows (bit 0 of a byte is the
leftmost pixel), which is the order both the terminal's scanout and the
payload's encoder use. Do not substitute `ffmpeg`'s `format=monob`: it packs
MSB first and mirrors every eight pixels on the display.
Takes **~2 min 47 s** (pure-Python Lloyd's algorithm over 3,292 frames) and
writes four files, **6.1 MB** total:
`make_tiles.py` prints what it built — on the real clip (3,292 frames at 15 fps,
1280x960) that is 256/256 dictionary entries, 55 keyframes, mean delta 496 B,
1.63 MB of stream, PSNR 24.4 dB.
| file | size | what it is |
|---|---|---|
| `dictionary.bin` | 32,768 B | 256 32x32 tiles, the vocabulary every frame is expressed in |
| `indices.bin` | 4,213,760 B | one 1,280-byte index frame per clip frame |
| `stream.bin` | 1,645,081 B | the plan: 55 keyframes + 3,141 deltas + 96 "nothing changed" |
| `manifest.json` | 199 KB | the numbers below, per frame |
It prints the same summary. What it said for this clip, and what to check:
```
3292 frames, 256 dictionary entries (256 used) <- the dictionary is saturated
55 keyframe(s), 3141 delta(s), 96 unchanged
payload 1280 B full, 496 B mean delta (5..3602), 1628615 B total
reconstruction 15823231 bit(s) over 3292 frame(s), BER 0.003667, PSNR 24.36 dB
```
Saturated entries and a 24 dB PSNR are expected, not errors: one byte per tile is
the format's ceiling, and shadow-art edges are expensive. A clip that reports
`inf` PSNR has every tile in the dictionary — that happens only for very flat
footage.
Keep the cache outside the repo: it is 6.1 MB of derived data. Pass it to the
demo with `--tiles /tmp/badapple-tiles`.
## Run it in MAME