4.1 KiB
Bad Apple on a VT1200 — 30 s vector prototype
A standalone, polygon-styled 30-second Bad Apple demo for the VT1200
(TMS34010). The host pipeline turns the first 30 s of hd60.mp4 into a
640×512 binary picture (2×2 screen blocks), traces it into simplified
straight-edged polygons, and packs only changed 32×32 tiles as rectangles.
The payload paints those rectangles with FILL L.
The payload takes over the terminal: it disables interrupts and never returns. Recovery is a power cycle. The ROM is never modified.
Design: ../docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md
Plan: ../docs/superpowers/plans/2026-11-01-bad-apple-vector.md
Measurements and gaps: REPORT.md
Build the 30 s stream
cd bad-apple
python3 -m pipeline.build \
--video /tmp/opencode/badapple-real/hd60.mp4 \
--out data --duration 30
Outputs:
| file | size | what |
|---|---|---|
data/movie.bin |
2,401,942 B | 1,757 records, 43 source frames dropped |
data/manifest.json |
small | data size, dirty-tile/rect/cost percentiles |
data/preview/frame_*.png |
— | still previews |
Measured: dirty tiles mean 44.9 / p95 72; rectangles mean 393 / p95 670; estimated paint cost mean 7.47 ms / p95 12.01 ms against a 13.9 ms refresh gate. The data area is 3.03 MB, so this fits with ~0.6 MB spare.
Build the payload
bad-apple/player/build.sh # debugger build: build/player.bin
NET=1 bad-apple/player/build.sh # network build: build/player-net.bin
The 1987 TI TMS34010 SDK runs under DOSBox. The C sources are K&R C: the
1987 compiler rejects ANSI prototypes and const. .text is linked at bit
0x018C0000, .bss at 0x01C00000.
Run in MAME (debugger path)
# one frame, then dump VRAM at MAME frame 2700
bad-apple/tools/run_mame.sh one
# 5 s slice, paced
BA_MOVIE=bad-apple/data5/movie.bin BA_START=0 BA_COUNT=300 \
bad-apple/tools/run_mame.sh play
# full 30 s, paced
BA_MOVIE=bad-apple/data/movie.bin BA_MOVIE_BASE=0x00080000 \
BA_WAIT=1 BA_PACE=1 bad-apple/tools/run_mame.sh play
run_mame.sh exits when the payload sets its status marker. The debugger
loader writes the movie into high RAM (0x01D00000) for small slices; for the
full 30 s it uses BA_WAIT=1, lets the payload take over, then writes low RAM
at 0x00080000 — the same ordering as the real delivery path.
Deliver to a real unit (network path)
The terminal must be booted and answering ping. The payload is loaded over
rdebug, then receives movie.bin over UDP into an OS malloc buffer, then
disables interrupts and plays.
bad-apple/tools/load.sh --host <unit-ip> --movie bad-apple/data/movie.bin
This path is implemented but not yet verified on the unit or over MAME's
tap in this worktree: the tap was down. The OS socket API it uses is the one
already proven by harness/udpstream/udpstream.c. See REPORT.md.
Data format
Header 36 bytes (BAPL, version, geometry, frame count, index/data offsets).
Index entry: u32 offset, u8 ticks, u8 flags. Record: u16 dirty tile count,
then per tile u16 tile index, u16 (bg<<15)|rect count, then 3-byte rects
(x0:5 y0:5 w-1:5 h-1:5). A tile is repainted by filling its 64×64 screen
block with the background colour, then filling the minority-colour rectangles.
Gotchas this prototype records
- PSIZE is re-asserted on every FILL. The resident OS leaves PSIZE at 2 bpp;
a one-time write in
_fill_init_was overwritten, and every span came out twice as wide. The fix is the per-callmovi 1 / move @0C0000150h. - VRAM polarity is inverted from the canvas: bit 1 = white, bit 0 = black.
tools/check_vram.pydownsamples with that polarity. - The OS dies before the movie is in low RAM. The network path receives
into an OS heap buffer while the stack is alive, then DINTs. The debugger
path does the same ordering with
BA_WAIT=1. - MAME is not the unit's performance verdict. MAME needed ~2.1 emulated
frames per movie record on the full stream; the unit's 50 MHz FILL rate is
the number that matters. See
REPORT.md.