Files
vt1200/bad-apple/REPORT.md
T

149 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bad Apple 30 s vector prototype — report
Date: 2026-11-01
Design: [`2026-10-31-bad-apple-vector-design.md`](../docs/superpowers/specs/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.