Files
vt1200/docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md
T

28 KiB
Raw Blame History

Bad Apple, 30 s, vector polygons — design

Date: 2026-10-31 Status: rev 1 — first draft, awaiting approval. Author: this session.

This is a separate prototype from 2026-10-31-badapple-60p-design.md (the raw-tile, full-length, streamed design in harness/badapple/). It does not supersede it and does not change the existing harness. It answers a different brief: a standalone, first-30-seconds, polygon-styled demo in a new top-level bad-apple/ folder.

Decisions marked human: were given in this session. Decisions marked call: are the builder's proposals, not yet agreed.


1. Intent

Play the first 30 seconds of /tmp/opencode/badapple-real/hd60.mp4 on a VT1200, full screen at 1280×1024, as flat black-and-white polygon art in the spirit of Another World — not a dithered bitmap and not a 32×32 tile dictionary.

The picture is quantised to straight-edged polygons, then to a coarse logical grid, then to rectangles that the TMS34010's FILL engine can paint fast. After a small bootstrap is delivered over rdebug, the payload takes over the machine and receives the movie data over Ethernet; playback is then entirely local from the VT1200's 4 MB of main RAM. Recovery is a hardware restart.

2. What the human partner decided

  • Source: /tmp/opencode/badapple-real/hd60.mp4, first 30 s.
  • Frame: fill the whole 1280×1024 panel; no letterboxed 4:3.
  • Look: black-and-white polygons, similar to Another World; drop dithered sections and threshold/simplify them into flat shapes.
  • Motion: 60 fps is the target; complex scenes may drop frames.
  • Ownership: after the initial load, the program may take the whole 4 MB of RAM; the OS may die; recovery is a power cycle.
  • Delivery: rdebug is the bootstrap path; the network loader in harness/badapple/ is fair game to make the big load fast.
  • Location: a new top-level bad-apple/ folder.

3. Success criteria

  1. bad-apple/ reproduces the whole pipeline from the source video with one command per stage, and documents the data budget it measured.
  2. In MAME, the first 30 s plays full-screen, recognisably Bad Apple, with clean straight-edged shapes and no corrupt or stale frames. Typical frames run at 60 fps; heavy frames may drop to 30 fps or hold for one refresh. The baseline repaints dirty tiles incrementally; a brief wipe on a large change is accepted for this prototype (§8.5).
  3. The movie data plus code fits in the VT1200's 4 MB of main RAM, excluding the 256 KB framebuffer in video RAM.
  4. On the real unit, the same payload is delivered by rdebug plus the Ethernet loader and plays without a host attached.
  5. Existing harness/ behaviour and the MAME no-env regression baseline are untouched. The ROM stays byte-identical.

4. Non-goals

  • The full 3:39 clip (the 30 s limit is what makes local RAM playback possible).
  • Audio (the VT1200 has no audio path in this harness).
  • Grey, colour, dither, or anti-aliased pixels; this is 1 bpp.
  • Pixel-exact reproduction of the source. The target is a recognisable, clean vector reinterpretation.
  • Coexistence with the resident OS after takeover.
  • A general-purpose video player; this is a demo with a fixed 30 s stream.

5. Constraints and measured facts

5.1 Hardware

item value source
CPU TMS34010FNL-50, 50 MHz, 1 state = 160 ns harness/mame/vt1200.cpp
panel 1280×1024 visible, 2048-bit (256-byte) row pitch, 72 Hz RENDER_REPORT.md, driver
main RAM 4 MiB at bit address 0 vt1200.cpp address map
video RAM separate, at bit 0x04000000 vt1200.cpp
FILL L on the unit 0.40 µs per destination word (16 pixels), plus ~2.6–3.3 µs per call 2026-10-31-badapple-60p-design.md §5
full 1280×1024 repaint ~82k words ≈ 33 ms derived from the unit rate
60 fps budget 16.7 ms ≈ 41k words ≈ 667k pixels derived
72 Hz refresh 13.9 ms per refresh driver

The 72 Hz display and 60 fps source have no common period; the player must pace a 60 Hz timeline against a 72 Hz refresh (§8.6).

5.2 Source

item value
video 960×720, 60 fps, h264, 219.5 s, 13,167 frames
audio AAC stereo, ignored
first 30 s 1,800 frames
black coverage mean 55 %, p75 80 %, p95 98 % (1280×1024 threshold)

The source is already high-contrast black-on-white; it is the ideal input for a threshold-and-trace pipeline.

5.3 The measured data budget

A host-side pass over the first 30 s at a 640×512 logical grid (each logical pixel is a 2×2 screen block), hard threshold, no vectorisation yet, measured the tile-delta format in §8:

quantity mean p50 p95 max
dirty 32×32 logical tiles per frame 47.4 49 79 295
rectangles per frame 437 451 820 1,381
bytes per frame (5 B tile header + 3 B/rect) 1,548 1,598 2,855 5,618

Total for 1,800 frames: 2.79 MB. The data area is 0x010000..0x317FFF = 3.03 MB, so this leaves ~0.24 MB of headroom. A scene cut touches up to 295 tiles, still only 5.6 KB and one slow repaint.

The vectorisation step will change these numbers; the first milestone re-measures them. The gate is ≤ 3.0 MB and a p95 paint cost within the refresh budget. If vectorisation pushes past 3.0 MB, the fallback is stronger simplification or a 4×4 logical grid, not a shorter clip without asking.

Why 3.03 MB and not 3.5 MB: the movie data starts at byte 0x010000 and must stop below the payload's rdebug origin at bit 0x018C0000 = byte 0x318000. 0x318000 - 0x010000 = 0x308000 = 3.03 MB. Moving the payload origin would cost the one free block the real loader is known to accept, so the design keeps the origin and caps the data instead.

6. Approach

6.1 Why not store rectangles for every frame?

A full-frame rectangle list is simple but forces a full repaint: at the unit's 0.40 µs/word, 1280×1024 costs ~33 ms, i.e. two 60 fps frames. The design must paint only what changed. The measured dirty-tile format does that: typical frames touch ~47 tiles (≈192k screen pixels ≈ 4.8 ms of fill), and the data still fits in 4 MB.

6.2 Why a 640×512 logical grid?

At 1×1 (1280×1024) the same delta format measures ~4× the data (≈10.8 MB) and does not fit. At 2×2 it fits with room to spare, and 640×512 is still four times the resolution of Another World's 320×200. The 2×2 quantisation is also part of the requested look: it turns pixel-level fuzz into flat blocks with straight polygon edges.

call: 640×512 logical, 2×2 screen blocks. If the first milestone shows the data is unexpectedly small, 1×1 can be re-measured as a quality mode; it is not the baseline.

6.3 Why polygons then rectangles?

The polygon step is the look: threshold, despeckle, trace contours, simplify to straight segments, and fill the result. That produces the flat, angular Another World silhouette. The runtime does not rasterise polygons; the host rasterises the simplified polygons back to the 640×512 grid and decomposes each dirty tile into rectangles. Rectangles are what FILL L paints fastest and most simply. The player is a rectangle decoder, not a polygon engine.

6.4 Why not LINE?

LINE costs 5 states/pixel (≈800 ns/pixel), while FILL costs ~25 ns/pixel on the unit. Filling a polygon with LINE outlines would be 30× slower and would not fill the interior. LINE is reserved for an optional debug view that draws polygon outlines; the animation uses FILL L only. The request said "FILL and/or LINE"; this is the FILL branch, for a measured reason.

7. Pipeline

hd60.mp4
  │  ffmpeg: first 30 s, 60 fps, 640×512 gray
  ▼
gray frames ──► temporal median (3) ──► 3×3 blur ──► threshold
  │
  ▼
binary frames ──► potrace -a 0 (straight polygons) ──► RDP simplify
  │
  ▼
simplified polygons ──► rasterise to 640×512 binary
  │
  ▼
tile diff (32×32 logical) ──► per-tile background + rectangles
  │
  ▼
movie.bin  +  index  +  manifest.json
  │
  ▼
player payload (TMS34010 C + FILL asm)

7.1 Extract and stylise

  • ffmpeg -t 30 -vf fps=60,scale=640:512:flags=lanczos,format=gray.
  • Temporal median over 3 frames before threshold. This is the main defence against dither flicker and per-frame contour boiling; it is cheap on the host and does not cost the player anything.
  • 3×3 box/Gaussian blur then threshold at 127. This removes halftone patterns and turns them into flat regions, as requested.
  • Despeckle: drop connected components below ~8 logical pixels.
  • Trace: potrace -b geojson -a 0 -t 4 -u 1, which emits straight-line polygons only (-a 0 disables curves). Parse the GeoJSON rings.
  • Simplify: Ramer–Douglas–Peucker on each ring, epsilon 1.5 logical pixels (tunable), minimum segment 2 px. This is the "straight lines" step.
  • Rasterise: even-odd scanline fill of the simplified rings back to 640×512. The result is the frame the player will actually show.

All parameters live in one pipeline/params.json so the look can be tuned without touching code. A preview command writes side-by-side PNGs of source, thresholded, traced and final frames.

7.2 Tile-delta format

The logical frame is divided into 32×32 logical tiles (64×64 screen pixels): 20 × 16 = 320 tiles, row-major.

For each frame, compare with the previous frame. For every tile with any changed pixel:

  • choose the tile's background colour (black or white) — the majority colour in that tile;
  • decompose the minority colour into rectangles (merge identical horizontal runs vertically);
  • emit the tile index, the background bit, the rectangle count, and the rectangles.

The player paints a dirty tile by:

  1. FILL the whole 64×64 screen tile with the background colour (one call);
  2. FILL each rectangle with the opposite colour.

This reconstructs the tile exactly, including erasing old ink. Unchanged tiles are not touched. The background choice keeps mostly-black tiles cheap (one fill, few white holes).

Record layout (little-endian):

header:
  magic "BAPL"          4
  version               u16
  logical_width         u16   (640)
  logical_height        u16   (512)
  scale                 u16   (2)
  tile_logical          u16   (32)
  tiles_x, tiles_y      u16   (20, 16)
  frame_count           u32
  index_offset          u32
  data_offset           u32
  source_fps            u16   (60)
  display_hz            u16   (72)

index[frame_count]:
  offset                u32   (byte offset of the record)
  ticks                 u8    (1 = 1/60 s, 2 = held for 2/60 s, ...)
  flags                 u8    (bit 0: scene cut / full repaint)

record:
  dirty_tile_count      u16
  for each dirty tile:
    tile_index          u16
    bg_and_count        u16   (bit 15 = background colour, bits 0-14 = rect count)
    rects[count]:
      x0:5 y0:5 w:5 h:5 packed into 3 bytes
      (logical tile coordinates; w,h are stored as w-1,h-1 so 1..32 fits in 5 bits)

Rectangles use the minority colour; the tile background is the majority. A rect count of 0 is legal (a solid tile). The count is 15 bits, so the theoretical maximum of 512 horizontal runs in a 32×32 tile cannot overflow; the decoder rejects a count larger than the remaining record bytes.

Sizing: 5 bytes of tile header + 3 bytes per rect. The measured mean is 1.55 KB/frame, total 2.79 MB for 1,800 frames. A scene-cut record with 295 tiles and ~1,300 rects is ~5.6 KB, still one record.

7.3 Frame-rate adaptation

The encoder estimates each record's cost from the tile and rectangle areas (at 0.40 µs per 16-pixel word), the per-FILL call overhead (~3 µs), and the C decoder's per-rect work. The calibration constants come from milestone 3; the initial values are 0.40 µs/word, 3 µs/call and 1 µs/rect. The gate is 13.9 ms — one 72 Hz refresh — not the 16.7 ms nominal 60 fps period, because a record that misses 13.9 ms loses a whole refresh on the 72 Hz panel.

If a record exceeds the gate:

  • first try stronger simplification for that frame (larger RDP epsilon, larger minimum feature);
  • if it still exceeds the gate, hold the previous frame for an extra 1/60 s and compute the next delta from the held frame. The index ticks field records the hold.

This is build-time frame dropping, so the delta chain stays exact. The player never skips a record at runtime: every record is a delta from the one before, so skipping one would leave its tiles stale. If the real unit is slower than the encoder's estimate, the movie stretches — every frame is still shown, in order, with the correct picture — rather than dropping or corrupting frames. That is the safe failure mode; the encoder is re-tuned from the unit's measured cost if the stretch is noticeable.

7.4 Scene cuts

A large frame-to-frame difference is a cut, not motion. The encoder marks the record flags & 1, and emits every tile as dirty. The player repaints the screen tile by tile. This costs one slow frame (~30–40 ms) and may drop the next refresh; that is the documented price of a cut. Future work could split a cut over two frames if it looks bad.

8. Player

8.1 Memory layout

region byte address use
0x000000–0x00FFFF 64 KB low staging / scratch; free after load
0x010000–0x317FFF 3.03 MB movie data (movie.bin)
0x318000–0x3FFFFF 0.9 MB payload code, LANCE block, read-only tables, stack
VRAM 0x04000000 (bit) 256 KB single framebuffer (separate)

0x018C0000 is a bit address = byte 0x318000, the low end of the high code block. The existing loader's origin is reused so rdebug needs no new address. The movie data starts at byte 0x010000 and is loaded over the network after takeover; it overwrites the OS, which is dead by then.

The LANCE block (init block, rings, RX buffers) lives in the high code block, following harness/udpstream/vidstream.c, which places its RX buffers near byte 0x3BC000 and is documented as having run on the unit. The 64 KB low reservation is a fallback staging area if the real chip turns out to truncate a high RX buffer address; the loader then receives low and copies up. Milestone 5 settles this before the full 30 s run. It is a loader change, not a player change.

8.2 Takeover

The payload is entered from rdebug. It:

  1. publishes a marker word;
  2. executes DINT once, for good, and stops the OS's LANCE use;
  3. takes the LANCE over with the verified init/ring sequence and receives movie.bin into 0x010000 (the OS is already dead; the display controller keeps scanning out because it was configured before takeover);
  4. verifies the movie header and a whole-file checksum;
  5. reads the display registers the OS programmed (DPYCTL, DPYSTRT, DPYINT, pitch) and publishes them for diagnosis; it inherits the mode rather than hardcoding one;
  6. clears the screen to white with one full-screen FILL;
  7. enters the playback loop.

Interrupts stay disabled. The OS's timer and DUART ISRs are never entered again. The LANCE is stopped after the load; the player no longer needs it. The OS dies before the first byte lands in low RAM — that is what makes overwriting its heap and ISRs safe. The earlier wording, "receive while the OS is alive, then DINT", was wrong; it would crash mid-load.

Recovery: a hang after step 2 is a power cycle. The README says so in the first screenful.

8.3 Loader

Two paths, same payload:

  • MAME / development: a Lua script writes movie.bin into MAME's RAM at byte 0x010000 and the payload at 0x318000, then jumps. Slow but fine for 5 s slices.

  • Real unit / full 30 s: rdebug writes a small bootstrap into the high block. The bootstrap disables interrupts, takes the LANCE over with the proven register/ring sequence from harness/udpstream (copied, not edited), receives movie.bin in 1,450-byte datagrams into 0x010000, ACKs every window, and jumps to the player entry. The host sender is a small Python tool using the same raw-Ethernet framing as harness/badapple / harness/udpstream.

    The baseline places the RX buffers in the high code block, as vidstream.c does. If the real chip truncates a high buffer address, the fallback receives into a 32 KB ring below 64 KB and copies each datagram up with MOVE/word copy. That copy rate is measured in milestone 5; it is a loader-only fallback, not the baseline.

The protocol is deliberately simple: u32 offset, u16 length, u16 seq, u16 crc, then data. The receiver ACKs the highest contiguous offset every 32 datagrams; the sender retransmits from the first gap. A final whole-file CRC-32 is checked before the jump. If the load fails, the payload paints an error block and halts rather than jumping into a partial movie.

8.4 Rendering

All drawing goes through one assembly primitive:

; fill_rect(x, y, w, h, color)
;   x,y,w,h in screen pixels (already scaled by 2)
;   color 0 = black, 0xFFFF = white

It sets DADDR, DYDX, DPTCH=0x800, PSIZE=1, PMASK=0, CONTROL replace, and COLOR1, then executes FILL L. Constants are programmed once per run; only DADDR, DYDX and COLOR1 change per call, and FILL destroys DADDR/DYDX, so those two are reloaded each time. This is the same implied-operand discipline as harness/udpstream/vram.asm, which is already verified on the unit.

The C player converts each packed tile rect to screen coordinates:

sx = (tile_x*32 + x0) * 2
sy = (tile_y*32 + y0) * 2
sw = (w + 1) * 2       // stored as w-1
sh = (h + 1) * 2       // stored as h-1

and calls fill_rect. A dirty tile is one background fill plus its rects.

Hoisting: PSIZE, PMASK, CONTROL, DPTCH are set once at startup. CONVDP/CONVSP are not needed for FILL L (linear destination), but are set to 0x14 for consistency with the verified primitive.

8.5 Frame buffer and presentation

Baseline: one framebuffer, incremental dirty-tile updates. Drawing a record takes ~2–10 ms for a typical frame, a fraction of the 13.9 ms refresh. Updates are visible as they land; the TMS34010's VRAM is dual-ported, so the display does not stall the CPU. At 60 fps the eye usually reads this as motion, but a large change can show a brief top-to-bottom wipe.

Tear-free is a separate mode, not free. It needs either:

  • two framebuffers and a vblank flip. The real panel uses a 256-byte row stride, so one 1280×1024 buffer is 256 KB, not 160 KB; two are 512 KB. Whether the unit has that much VRAM is not established. It also doubles the fill work unless each delta is applied to both buffers, which costs the p95 frame its refresh.
  • beam chasing. Draw each dirty tile only after the scanout has passed it, so the new content appears on the next frame. This is tear-free with one buffer but needs VCOUNT scheduling and leaves one frame of latency.

Neither is in the baseline. human: incremental painting is acceptable for the prototype, so the baseline is one framebuffer and dirty-tile updates. Milestone 2 still measures VRAM capacity for the record, but a tear-free mode is not built unless the incremental look fails on the unit.

8.6 Pacing: 60 fps content on a 72 Hz panel

The player polls the TMS34010 display interrupt pending bit (INTPEND DI) at each vertical blank; interrupts remain disabled, so this is a poll, not an ISR. A 16.16 fixed-point accumulator advances the 60 Hz timeline:

each vsync:  now += 60/72
             if now >= next_frame_time:
                 draw next record          // never skip a record
                 next_frame_time += record.ticks
             if now - next_frame_time > 1.0:   // fell behind
                 next_frame_time = now         // slow down, do not skip

If a record's paint overruns a vsync, the movie stretches: the next record is drawn as soon as the CPU is free, and the timeline is re-based to now rather than accumulating debt. Records are never skipped, because each one is a delta from the previous picture. This produces the correct average rate on a unit that keeps up, and slow motion — not corruption — on one that does not. The 5:6 cadence (five source frames per six refreshes) is visible only as the usual 60-on-72 judder; it is a hardware property, not a decoder stall.

If INTPEND polling proves unreliable in MAME, the fallback is a DUART counter programmed as a 60 Hz timebase, polled with interrupts off. That is a builder detail; the timeline arithmetic is unchanged.

9. Verification

9.1 Host pipeline tests

  • Frame count, dimensions and LSB-first bit order (the harness's long-standing trap: an MSB-first packer mirrors every eight pixels).
  • Polygon round-trip: rasterised simplified polygons differ from the thresholded source only in the intended simplified regions; report PSNR and changed-pixel count, not a pass/fail on aesthetics.
  • Tile-delta round-trip: applying every record to a blank 640×512 canvas reproduces the encoded frame bit for bit.
  • Budget: total movie.bin ≤ 3.0 MB (the hard cap is 3.03 MB), p95 paint cost ≤ 13.9 ms including the C decoder and FILL call overhead.
  • Manifest records the actual numbers (data size, dirty-tile distribution, paint-cost distribution, dropped frames).

9.2 MAME pixel verdict

The player publishes a marker page in high RAM:

frame_index, ticks_played, last_record_bytes, paint_loops, data_error

VT1200_DUMP_RAM and VT1200_DUMP_VRAM at the same MAME frame give both the marker and the picture. A checker reads the marker, reconstructs the expected logical frame from the encoder's records, downsamples the 2×2 VRAM to 640×512, and compares exactly. A single wrong bit is a bug, not a PSNR.

Sample frames: 0, 60, 300, 600, 900, 1200, 1500, 1799, plus every scene cut. Also run a 5 s slice first; do not debug the full 30 s before one frame is exact.

9.3 MAME motion verdict

Run with -window, throttle at the emulated 72 Hz, and capture with x11grab as the existing run-gui.sh does. Check:

  • no half-painted or stale regions after a cut;
  • no accumulating offset (a delta applied twice or skipped);
  • the 30 s ends on the expected last frame;
  • the paint-cost histogram matches the encoder's estimate.

MAME cannot prove the real unit's 60 fps: its TMS34010 interpreter has a different instruction/cycle mix from silicon. It proves correctness and gives a relative cost. The unit is the performance verdict.

9.4 Real unit

The existing harness/badapple/demo.sh boot gate (wait for the OS's ICMP reply, then load over rdebug) is the model. The new bad-apple/load.sh:

  1. waits for the unit's stack;
  2. rdebug writes the bootstrap;
  3. jumps;
  4. sends movie.bin over the tap/interface;
  5. waits for the bootstrap's final ACK;
  6. observes the screen (there is no back-channel after the jump).

Record a video of the 30 s run and note the actual dropped frames. Recovery is a power cycle; say so before running.

10. Milestones

  1. Measure the stylised stream (host only). Extract, vectorise, tile-diff, and report the numbers. Gate: ≤3.0 MB, p95 paint cost within 13.9 ms. Produce a preview PNG/MP4 of the vectorised look for approval.
  2. One exact frame in MAME. Build the payload with a single hardcoded record, paint it, dump VRAM, compare bit-exact. This proves FILL geometry, the 2×2 scaling, the test harness and the marker page.
  3. Five-second player. Pack a 5 s slice, play it, compare every sampled frame, and measure the paint-cost histogram. No loader yet; use the Lua path.
  4. Full 30 s. Pack 1,800 frames, play in MAME, tune simplification and frame drops until the data and motion gates pass.
  5. Delivery. Build the rdebug bootstrap + LANCE loader, run the same 30 s in MAME over the tap, then on the real unit.
  6. Report. bad-apple/README.md and a short report with the measured data size, paint-cost distribution, dropped frames, unit behaviour and the remaining gaps.

11. Risks

risk why it matters mitigation
Potrace/RDP temporal jitter ("boiling") looks like a glitch even when every frame is exact 3-frame temporal median, spatial blur, minimum feature size; preview at 60 fps before committing to the full 30 s
Vectorised data exceeds the 3.03 MB data area no room for the 30 s measured 2.79 MB before vectorisation; gate at 3.0 MB; fallback is 4×4 blocks or stronger simplification, not a shorter clip without asking
Dirty tiles exceed the paint budget frames hold or stretch build-time ticks holds, adaptive simplification, per-frame cost estimate; report the distribution
Incremental updates show a wipe accepted for the prototype, but may look worse than expected on the unit baseline is incremental by human decision; if it reads as a glitch, milestone 2's VRAM measurement feeds a double-buffered/beam-chasing variant
The real unit is slower than the model the movie stretches rather than drops never skip a record; re-base the timeline; re-encode with stronger simplification from the measured unit cost
LANCE loader loses datagrams partial movie, dead machine offsets + CRC + ACK/retransmit; whole-file CRC before jump; error block and halt instead of jumping
Real C-LANCE high buffer addresses loader cannot receive above 64 KB baseline reuses vidstream.c's high block, documented as run on the unit; fallback is a low staging ring plus copy, measured in milestone 5
60 fps on 72 Hz unavoidable judder fixed-point 5:6 timeline; document it; do not call it a decoder bug
OS video mode not configured no picture after takeover inherit and publish the OS registers; boot path is "wait for the OS, then take over"
One demo per boot a hang costs a power cycle verify the image/checksum before jumping; README warning; no automatic retry after takeover
MAME timing is not silicon false confidence in 60 fps correctness in MAME, performance measured on the unit; report both

12. Left to the builder

  • Exact RDP epsilon, minimum feature size, and temporal-filter width: tune by preview, then freeze in params.json.
  • Whether the tile background choice is per-tile majority or a cost-minimising search; both are host-side and cheap.
  • The precise CONTROL mask and hoisting split for FILL L; copy the verified vram.asm discipline and measure once on the unit.
  • Whether a short slice uses the Lua/debugger load or the same bootstrap; the full 30 s uses raw LANCE because the OS is dead before the data lands.
  • The marker page layout and the exact MAME dump procedure.
  • Whether the first 30 s starts at t=0 (assumed) or at a musically chosen offset; t=0 is the default.
  • Whether a stop/pause key is worth keeping. It would require one live ISR or polling the keyboard DUART; the default is no input, as the OS is dead.

13. Decisions from this session

  • human: full screen, 1280×1024, first 30 s.
  • human: as close to hd60.mp4 as the medium allows.
  • human: flat black-and-white polygons in the Another World spirit; drop dither and simplify.
  • human: 60 fps target, drops allowed for complex scenes.
  • human: the program may take the whole 4 MB; the OS may die; recovery is a hardware restart.
  • human: bootstrap over rdebug; the existing network loader may be used to speed the big load.
  • human: new top-level bad-apple/ folder.
  • human: incremental dirty-tile painting is acceptable; a brief wipe on a large change is not a failure for this prototype.
  • human: stretch the 4:3 source to the 5:4 panel; keep all content.
  • call: 640×512 logical grid (2×2 blocks), 32×32 logical tiles.
  • call: FILL-only runtime; LINE reserved for debug outlines.
  • call: 60 Hz timeline on a 72 Hz panel, with build-time ticks holds.
  • call: 30 s from t=0.
  • call: a --fit crop switch remains available for comparison, but stretch is the default.