4.5 KiB
Bad Apple 30 s vector prototype — report
Date: 2026-11-01
Design: 2026-10-31-bad-apple-vector-design.md
What was built
- Host pipeline: video → temporal median → blur/threshold → potrace
straight-line polygons → RDP → even-odd rasteriser → 32×32 tile deltas →
movie.bin. - Payload: TMS34010 C decoder +
FILL Lprimitive, 16-bit marker page, display-interrupt pacing, UDP receive path. - Tools: MAME debugger loader, VRAM checker, UDP sender, rdebug load script.
Measured numbers (first 30 s, 1,800 source frames)
| quantity | value |
|---|---|
| source | 960×720, 60 fps, first 30 s |
| logical grid | 640×512, 2×2 screen blocks |
| records | 1,757 (43 source frames dropped by the cost gate) |
| data | 2,401,942 bytes (cap 3.03 MB, gate 3.0 MB) |
| dirty tiles/frame | mean 44.9, p50 47, p95 72, max 320 |
| rectangles/frame | mean 393, p50 409, p95 670, max 1,175 |
| estimated cost/frame | mean 7.47 ms, p50 7.95 ms, p95 12.01 ms, max 37.99 ms |
| gate | 13.9 ms (one 72 Hz refresh) |
The stream fits with ~0.6 MB spare. The p95 frame is inside one refresh; the max is a scene cut that necessarily takes more than one.
Exactness evidence (MAME)
tools/check_vram.py unpacks the 256-byte-stride VRAM dump, downsamples 2×2,
and compares bit-for-bit against the Python application of the records.
| run | movie frame | result |
|---|---|---|
| test pattern | 0 | EXACT |
| 5 s slice | 0 | EXACT |
| 5 s slice | 180 | EXACT |
| 5 s slice | 297 | EXACT |
| 30 s stream | 0 | EXACT |
| 30 s stream | 900 | EXACT |
The one-frame MAME run also proved the FILL geometry: a logical black rectangle 10..109 × 20..69 became exactly screen x 20..219. The first version was exactly twice as wide; the cause was the OS leaving PSIZE at 2 bpp. The primitive now re-asserts PSIZE=1 on every call.
Potrace emits y-up GeoJSON; the framebuffer is y-down. The first build was
vertically mirrored, and the VRAM checker still said EXACT because it consumed
the same flipped data. trace() now flips y before simplification, and
test_trace_preserves_vertical_position pins the orientation. The preview PNGs
were the visible symptom.
Pacing
wait_vsync() programs DPYINT=1, clears INTPEND, and polls the DI bit at
0xC0000120. The loop reads each record's ticks from the index and advances
a 1/360 s accumulator by 5 per vsync, so a 1/60 s record needs 6 units and a
held record needs 12. Measured with BA_COUNT=60: status at MAME frame 2580,
74 frames after takeover at 2506 — 60 source frames in 72 display frames plus
startup, i.e. the 60-on-72 cadence is real, not free-running.
The performance gap MAME cannot close
MAME needed ~2.1 emulated frames per movie record while replaying frames 0..900
of the full stream. The encoder's model says a mean record is 7.47 ms of a
13.9 ms refresh, so MAME's TMS34010 interpreter is several times slower than the
model. MAME proves the picture and the delta chain; it cannot prove the unit's
60 fps. The unit's measured FILL rate (0.40 µs per 16-pixel word) is the
number the cost model uses. A real-unit run is the remaining performance
measurement.
Network delivery
player-net.bin uses the OS UDP socket API (socket_, bind_,
recvfrom_, malloc_) to receive movie.bin on port 7001 into a heap buffer,
then DINTs and plays. The sender is tools/send_movie.py. Packets are
10-byte headers (offset, length, seq, crc16) plus body; a final
offset==length-of-file / 4-byte-checksum packet is required, and the receiver
refuses to jump unless the whole-file checksum matches. Out-of-range packets
are dropped without reading or writing outside the buffer.
Status: the packet format, header offset, checksum path and sender are
unit-tested; the MAME tap was down in this worktree, so the UDP receive was
not exercised end-to-end. The same OS socket API is already proven on the unit
by harness/udpstream/udpstream.c. This is the one delivery-path gap.
Regression
The no-env MAME baseline at 200 frames is unchanged:
--- distinct PCs sampled: 2
PC=3FDCE400 samples=199
PC=3FDCE350 samples=1
Remaining gaps
- Real-unit 30 s performance: needs a power-cycle run with
tools/load.sh. - UDP receive on the unit: socket API proven, this payload not yet.
- Incremental painting can show a brief wipe on a cut; accepted for this prototype.
- Potrace/RDP temporal jitter was reduced with a 3-frame median but not measured as a motion score.