Files
vt1200/bad-apple/REPORT.md
T

6.1 KiB
Raw Blame History

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 L primitive, 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 live MAME tap test reaches the payload but stops before UDP bind: the marker shows status=0x20 and no socket is open. The suspect is malloc_(2401942) — the OS heap does not provide one contiguous block that size. The debugger path is the working demo. A fixed low-RAM staging receive or raw-LANCE loader is the next step. This is the one delivery-path gap.

r5.01 OS/2 compiler check (2026-11-01)

The 1990 TMS340 r5.01 ANSI C toolchain, run under the patched 2ine-tms34010 fork, builds this demo. The pipeline was:

gspac -k MAIN.C MAIN.IF
gspcg -m MAIN.IF MAIN.ASM MAIN.TMP     # no gspopt for MAIN.C
gspa MAIN.ASM
gspac/gspopt -O2/gspcg/gspa for MOVIE.C
gspa FILL.ASM
gspa ENTRY.ASM
gsplnk PLAYER.CMD                       # .text 018C0000h, .bss 01C00000h

Result: .text 0x6300 bits = 3,168 bytes, entry 0x018C5C70. MAME frame 0 and frame 180 are EXACT with BA_PAYLOAD=PLAYER.bin BA_ENTRY=0x018C5C70.

Caveat: gspopt -O2 on MAIN.C hoists the CTRL_READY polling load and the payload waits forever at takeover. MOVIE.C is fine at -O2; build MAIN.C without gspopt, or add volatile to the polling reads. gspac and gspcg exit 139 at teardown but still write valid output files. gspa 5.01 accepted entry.asm/fill.asm (including sp); the b15 round-trip difference does not affect this demo.

Speed A/B in MAME, 600 records with BA_PACE=0 (takeover at ~frame 2505):

build status frame processing frames
1987, no optimizer 3,922 ~1,417
r5.01, no optimizer 3,856 ~1,351
r5.01, MOVIE.C at -O2 3,605 ~1,100

So the r5.01 compiler alone is ~5 % faster here, and the practical r5.01 recipe is ~29 % faster than the 1987 build. MAME's TMS34010 interpreter is not silicon; this is a relative measurement, not a unit-cycle claim.

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

  1. Real-unit 30 s performance: needs a power-cycle run with tools/load.sh.
  2. UDP receive on the unit: socket API proven, this payload not yet.
  3. Incremental painting can show a brief wipe on a cut; accepted for this prototype.
  4. Potrace/RDP temporal jitter was reduced with a 3-frame median but not measured as a motion score.