Files
vt1200/bad-apple

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
VT1200_DUMP_VRAM_FRAMES=2700 VT1200_DUMP_DIR=/tmp/ba-one \
    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-call movi 1 / move @0C0000150h.
  • VRAM polarity is inverted from the canvas: bit 1 = white, bit 0 = black. tools/check_vram.py downsamples 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.
  • The marker/control block lives in .bss, not at bit 0x01810000: a 2.4 MB heap allocation can cross that address. player.env publishes MARK_PTR; load_mame.lua reads it.
  • Pacing reads each record's ticks. 60 source frames took 74 MAME frames (72 display frames plus startup), so the 60-on-72 cadence is measured, not assumed.