Two sessions that were never separated in the working tree, so they land as one commit. check.sh ALL GREEN before and after both. SESSION 19 -- the ring rig gets a frame clock (FINDINGS 51). src/player/stream.s had no frame clock: it asked for record i the instant it finished i-1, outran any finite pipe, and never let the ring back up. The 49.1 sweep passing at 48 KB was therefore a wrap-correctness result and nothing else. PACE/PACEON ($18034/$18038) hold the decoder to 12 fps, so FR_HEAD-FR_TAIL finally means what it reads as: whole frames the decoder could still draw with delivery stopped dead. PACEON=0 free-runs and is what the wrap gate still uses, so every figure in 49 is unmoved. Paced, on the gate container: 64 KB holds 2 frames, 256 KB holds 7-8, 512 KB holds 14-15, all pixel-exact. Tolerance is ceiling-1, measured by cutting the pipe: 256 KB buys 500 ms of dead pipe, not 583. SLACK IS ACCUMULATED, NOT OWNED. It is built out of pipe-wire and a seek spends all of it. At 488 KB/s a 256 KB ring needs 4.83 s of play to reach its ceiling from empty; 512 KB needs 8.42 s to reach 14. A bigger ring raises the ceiling AND lengthens the climb, so a branch point does not ask "is the buffer big enough" but "has there been enough play since the last one" -- and Dragon's Lair's decision points are seconds apart. The rig now also says WHICH resource is binding: at 460 KB/s every ring from 192 KB to 512 KB is rate-bound at ceiling 4 and never fills, so larger rings are dead RAM in that scene. 20_seek_slack.py is the same model rewritten in Python from record sizes, sharing no code with the Lua producer: 35/35 ceilings inside its bracket. SESSION 20 -- the DMAC configuration was in the IPL ROM the whole time (FINDINGS 52). ROADMAP's "do this first" was to put the ADPCM stream on the bus. That needs a clocks-per-byte figure for the audio channel, and 11_cpu_budget.py was charging audio the DISK's rate -- 5 clk/B, its own help text calling it "single-address, bus held". Audio was being charged the favourable end of B3, a 242 KB/s open question. It never had to be a guess. The IPL ROM programs all four HD63450 channels itself and MAME boots the rig with it, so 21_iplrom_dmac.py reads the configuration out of the image and decodes the MC68450 fields. Eight (address, expected bytes, meaning) sites; a mismatch or an unknown revision exits non-zero. In check.sh, no emulator, milliseconds. ch3 DCR=$80, OCR=$32: dual address, 8-bit port, cycle steal WITHOUT hold, REQG=10 external request. The DMAC arbitrates once per byte with no burst to amortise the 5..8 + 2 over, so an audio byte is 16..19 clocks, not 5 -- the old debit was 3.2x..3.8x small. And on the bus it is still nothing: 651 B/frame is 1.25%..1.48% of a frame, about 4% of what the decoder leaves. P6's bus risk does not materialise. The unit worry was worth checking and nearly right: 15.6 kHz is 8 MHz/512 = 15,625 samples/s, two 4-bit samples to a byte = 7,812.5 B/s exactly, and AUDIO_KBPS=7.8 is that in decimal kB while the tool multiplied by 1024. THE DISK CHANNEL IS PROGRAMMED IDENTICALLY. ch1 (SASI) is DCR=$80 too, and so is ch0. That is 16..19 clocks per delivered byte, where 42.4 brackets W at 5..12 and 42.5 has W=8 already missing 47/120 frames. The only worked example of a disk DMA configuration on this machine sits above the entire bracket, and at that price nothing fits at any container size. It is not scsiexrom.bin so B3 stays open -- what changed is that a cheap configuration is now the thing that has to be SHOWN. W <= 12 is a requirement on the player's DMAC programming, not a range the hardware hands us, and it is now the largest open number in the project, ahead of the rate. An unforced cross-check fell out: 15_bus_occupancy.py's new W sweep puts W=8 at 105.7% of the frame, agreeing with 42.5's 47/120, from mode histograms and bus clocks respectively, two models sharing no code. Also: ADPCM outranks the disk at the arbiter (CPR 1 against 2), so an audio byte never waits and a video byte does -- relevant to 51's smooth-rate delivery model. README MEDIA. stream.lua gains DLX_SNAP_EVERY=1 (needs DLX_PACE, off by default, on no path check.sh takes) and tools/media/make_readme_media.py turns the PNGs into docs/img/. The stills and both clips are MAME's own screen pixels. Building it turned up something worth recording. 116 of 119 captured frames are pixel-exact against dlx.py; three are TORN -- frame n on top, frame n-1 below the tear line -- because MAME captured the screen while the block loop was partway down it. decode.s writes straight to the displayed page (one display path, 28.1), so a real player tears the same way, and this is the first time that consequence has been visible rather than argued. The script ASSERTS the tear and refuses to build otherwise, rather than trimming three frames and reporting "every frame I kept is exact". Second correction the capture forced: the snapshot fires before frame n is decoded, so the obvious reading is that it holds frame n-1 -- it does not, because MAME renders the screen at the end of the machine frame, by which time the 68000 has finished frame n. 11_cpu_budget.py's "validated to within 1 pt" line is also corrected: the model reads 2..10 pt HIGH and by more as the frame gets harder, which was already true before either session. src/player/decode.s is unchanged; decode.bin is still 1,296 B at the same MD5. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
245 lines
13 KiB
Markdown
245 lines
13 KiB
Markdown
# Roadmap — remaining work to a completion target
|
|
|
|
Written end of session 19 (2026-08-24), against a tree that is ALL GREEN.
|
|
|
|
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
|
|
tree — a decision point, two outcomes, a death clip — with audio, streaming from
|
|
a real SCSI volume on a stock 2 MB machine, playable. That is the point at which
|
|
every layer of this design has been shown to work at once. M4 is listed because
|
|
it is real work, but past M3 it is content grinding rather than open questions.
|
|
|
|
`docs/STATUS.md` remains the session-by-session record and the handoff. This file
|
|
is the shape of what is left; where the two disagree about what is done, STATUS
|
|
is the one with the measurements and this one is the one that goes stale. Both
|
|
were wrong about two encoder gaps until this file was written — see "What was
|
|
already done" below.
|
|
|
|
---
|
|
|
|
## Status of the four resources
|
|
|
|
The project's own framing, restated because every item below is priced in one of
|
|
these units:
|
|
|
|
| resource | state |
|
|
|---|---|
|
|
| **68000 local bus** | the binding one. Decoder occupies 86.7%; 52 of 53 missed frames miss on the bus, not the clock (FINDINGS 38). |
|
|
| **68000 clocks** | measured, and the rate controller binds on them. |
|
|
| **Delivery rate** | **no working figure, deliberately** (FINDINGS 50, USER DECISION). Every tool REQUIRES an explicit rate. |
|
|
| **Seek time** | **no figure at all, and never had one.** 51.3/51.4 made it matter. |
|
|
| **W, clocks stolen per delivered byte** | bracketed 5..12 (42.4); the IPL ROM's own disk channel is **16..19** (52.5). **The largest open number in the project.** |
|
|
|
|
---
|
|
|
|
## What was already done, and was still on the list
|
|
|
|
Found while inventorying for this file. Both had been closed in code for several
|
|
sessions and were still listed as open gaps in `docs/STATUS.md`:
|
|
|
|
- **4-byte record padding.** `DLX2`, `encode.py:139-156`, inside rate-control
|
|
accounting, reported per frame and per second.
|
|
- **CPU cost in the mode decision.** `vq_hybrid.py:218`, priced against measured
|
|
per-mode cycles with the exact clustered SKIP rule.
|
|
|
|
Both entries are now struck in STATUS. **The lesson is procedural: a gap list
|
|
that is only ever appended to manufactures phantom work.** Anything crossed off
|
|
below should be crossed off in STATUS in the same sitting.
|
|
|
|
---
|
|
|
|
## Blocked on hardware this tree does not have
|
|
|
|
None of these block M2 or M3 software work, because session 18 forced every rate
|
|
to be an explicit argument. They set constants, and two of them decide how much
|
|
headroom the finished player has.
|
|
|
|
**B1. Measure the BlueSCSI — throughput AND seek time.**
|
|
Throughput has an acceptance test already derived from real record sizes:
|
|
**513.2 KB/s** for the session-14 candidate, **451.4 KB/s** for the gate
|
|
container (`19_ring_stream.py`, FINDINGS 49.5). Seek time has nothing.
|
|
51.3/51.4 is why the second half matters: slack is *accumulated* out of
|
|
`pipe - wire`, so what a branch point costs is set by the rate and the time since
|
|
the last branch, not by the ring size. At 460 KB/s every ring from 192 KB to
|
|
512 KB is rate-bound and never fills. **Do not substitute a guess** — run at
|
|
several explicit rates and report the sensitivity. That is exactly how the
|
|
retired pipe constant survived five sessions after 42.1 called it folklore.
|
|
|
|
**B2. Does buffer mode blank the display?** `probe_bit11_blank.lua` is written
|
|
and settles it in minutes on a real board. FINDINGS 48 shifted the prior toward
|
|
MAME and toward "unusable" — **do not pre-build on 1.0 B/pixel**. Same sitting:
|
|
the priority register `0xE82500` at `0x0000` (47.3).
|
|
|
|
**B3. Single-address vs dual-address DMA.** 242 KB/s and 0.69 dB. Needs
|
|
`scsiexrom.bin` (8 KB, CRC `7be488de`) sourced, then its DMAC init disassembled
|
|
for DCR's DTYP: `10`/`11` = single (5.0 clk/B), `00`/`01` = dual (9.0).
|
|
FINDINGS 48.4. Not on this machine (checked, session 18).
|
|
**This is also P4's input** — the handshake the player drives is the same
|
|
question from the software side.
|
|
|
|
> **Session 20 moved the prior hard, and it moved the wrong way (FINDINGS 52.5).**
|
|
> The IPL ROM *is* on this machine, and `tools/analysis/21_iplrom_dmac.py` reads
|
|
> its HD63450 setup: the on-board disk channel (ch1, SASI) is `DCR = $80` —
|
|
> **dual address, 8-bit port, cycle steal WITHOUT hold**, with `REQG = 10`
|
|
> external request, i.e. a full bus arbitration per byte. That is **16..19
|
|
> clocks per delivered byte**, above the whole 5..12 bracket 42.4 costs P4 in.
|
|
> Same vendor, same DMAC, same class of 8-bit port — but it is *not*
|
|
> `scsiexrom.bin`, so B3 stays open. What it changes is that a cheap
|
|
> configuration is now the thing that has to be **shown**, not assumed.
|
|
|
|
---
|
|
|
|
## M2 — a player, as opposed to a decoder
|
|
|
|
`decode.s` draws pixel-exact frames from RAM Lua pre-loaded; `stream.s` decodes
|
|
out of a bounded ring fed by a host file on a paced clock. Neither is a player.
|
|
|
|
**Exit criterion: boots from a real SCSI volume on a stock 2 MB X68000, plays
|
|
one scene at 12 fps from disc, no host-file pipe, no Lua in the loop. Silent.**
|
|
|
|
**P1. Codebook expansion on the 68000.** `dlxload.py:19` expands CB1 to 32 B per
|
|
entry and CB4 to 8 B, host-side, because at the time it was a load-time cost that
|
|
would have flattered or damned the inner loop. The player must do it: **8 KB +
|
|
2 KB per scene**. Note where that lands — *at a scene change, when the ring is
|
|
empty because of the seek*. It compounds with 51.3 and should be priced against
|
|
the refill climb, not treated as free setup.
|
|
|
|
**P2. Palette packing on the 68000.** The encoder still emits RGB888; the X68000
|
|
word packing is Lua-side. Whatever writes real palette words must pick `I` per
|
|
entry by minimum squared error (**1.96 dB**, FINDINGS 23.3) and reserve index 0
|
|
as black with `I = 0` (23.4).
|
|
|
|
**P3. A real frame clock.** `stream.s` has `PACE`/`PACEON` (`$18034`/`$18038`)
|
|
but the 12 fps tick comes from the Lua producer. Needs MFP timer or VBL. Keep
|
|
`PACEON=0` free-run working — the wrap gate uses it and every FINDINGS 49 figure
|
|
depends on it.
|
|
|
|
**P4. Real transport.** Drive the MB89352 instead of a host file. The `W`
|
|
handshake — clocks stolen per delivered byte, bracketed 5..12 by MC68450 Fig
|
|
4-25 — is listed in "Decisions locked" as UNDECIDED and as the thing that
|
|
decides the project: `W<=6` fits 0/120 frames, `W=8` misses 47/120. It is a
|
|
property of how the player drives the SPC, **so it is ours to choose, not to
|
|
receive** (FINDINGS 42.4-42.6). B3 informs it.
|
|
|
|
**Session 20 promoted this to the project's biggest open number.** FINDINGS 52.5
|
|
found the only worked example of a disk DMA configuration on this machine — the
|
|
IPL ROM's own — sitting at 16..19 clk/B, outside the bracket entirely, where the
|
|
whole design fails at any container size (`15_bus_occupancy.py` sweeps it).
|
|
`W <= 12` is now a **requirement on the player's DMAC programming**, not a range
|
|
the hardware hands us, and demonstrating a configuration that meets it is P4's
|
|
first job rather than its last.
|
|
|
|
**P5. Seek and branch.** Per-record index (the `aligned` producer needs one
|
|
anyway, 49.3), prefill policy, and the accumulated-slack rule from 51.3 made
|
|
explicit in the player rather than implied by the rig.
|
|
|
|
**P7. Boot.** The player as an executable loading from the SCSI volume.
|
|
|
|
---
|
|
|
|
## M3 — the vertical slice, and the completion target
|
|
|
|
**Exit criterion: one decision point, two outcomes, a death clip, with audio,
|
|
playing from disc on stock hardware.**
|
|
|
|
**P6. Audio — and it is the largest unpriced risk left in the project.**
|
|
MSM6258 ADPCM, 15.6 kHz mono, **7.8 KB/s**. That figure is in `ratectl.py`'s
|
|
budget and nowhere else: not extracted, not encoded, not interleaved into the
|
|
container, and **never priced on the bus**. Two reasons to treat it as a risk
|
|
rather than a task:
|
|
|
|
1. A second DMA consumer attacks **the bus** — the resource this project already
|
|
established is the binding one, at 86.7% occupied. Clock headroom says
|
|
nothing about whether it fits.
|
|
2. 7.8 KB/s is a *byte* figure. The last time a byte/word unit error went
|
|
unexamined in a delivery budget it cost the project a 2x error in every table
|
|
since FINDINGS 5 (session 14, the MB89352 being an 8-bit SPC).
|
|
|
|
~~**Price it before writing it**: add the ADPCM DMA stream to `15_bus.py` and see
|
|
what it does to the 86.7%.~~ **DONE, session 20 — FINDINGS 52.** It is in
|
|
`15_bus_occupancy.py` and the answer is **1.25%..1.48% of the frame**, about 4%
|
|
of what the decoder leaves. The per-byte cost is no longer a guess borrowed from
|
|
the disk: `tools/analysis/21_iplrom_dmac.py` reads the IPL ROM's own HD63450
|
|
configuration and finds ch3 dual-address, 8-bit port, cycle steal without hold,
|
|
external request — **16..19 clocks per byte**, where `11_cpu_budget.py` had been
|
|
charging audio the disk's 5. Both worries above resolve:
|
|
|
|
1. **The bus concern does not materialise.** A second DMA consumer at 7.8 kB/s
|
|
is not what a bus at 88% occupancy is short of.
|
|
2. **The unit was checked and is nearly right.** 15.6 kHz = 8 MHz ÷ 512 =
|
|
15,625 samples/s, 4 bits each, two to a byte = **7,812.5 B/s exactly**. The
|
|
7.8 was decimal kB being multiplied by 1024; 2.4% high, now derived from the
|
|
sample rate in `buscost.ADPCM_BYTES_PER_S`.
|
|
|
|
**What is still open in P6 is everything except the bus:** extraction, encode,
|
|
container interleave, and what a second stream does to `wire` — and therefore to
|
|
`pipe - wire`, and therefore to 51.3's refill climb. That last one is the
|
|
interaction to price next, and it is E2's question with a second consumer in it.
|
|
|
|
**E6. Container v2** — audio interleave, per-record index, scene table. Depends
|
|
on P6's answer and on P5's index.
|
|
|
|
**G1. Import the scene graph — early, because it is a measurement input.**
|
|
SNES project `data/events/` (MIT, cleared) diffed against DirkSimple (zlib),
|
|
which transcribed the same data independently, to catch transcription errors
|
|
before anything reaches 68000 tables. **Neither is on this box** — both need
|
|
fetching.
|
|
|
|
The reason to pull this ahead of the game logic that consumes it: 51.3 says
|
|
4.83 s of play to refill a 256 KB ring at 488 KB/s, and Dragon's Lair's decision
|
|
points are seconds apart. **Nothing in this tree can currently say what the worst
|
|
gap between consecutive decision points is** — only the scene table knows, and
|
|
until it is imported, whether this design survives a back-to-back branch is an
|
|
open question nobody is able to ask.
|
|
|
|
---
|
|
|
|
## M4 — the whole game
|
|
|
|
Listed for completeness; past M3 these are scope, not risk.
|
|
|
|
- **C1. Full-disc survey**, 22.8 minutes. Classify **content / menu / bonus** —
|
|
not menu vs content: the two largest streams are bonus material and look like
|
|
content by size, duration and bitrate alike (25.1). Run
|
|
`07_motion_survey.py` per stream first for a hot-window shortlist.
|
|
**Gated by E4.**
|
|
- **E4. `H.build` k-means**, 51 s of a 55 s run, once per scene. The thing to
|
|
attack before C1, and not anything in the per-frame path (27.6).
|
|
- **E2. `--spans all` as default.** Still a recommendation, not a measurement
|
|
(43.6.1), and the only loaded lever on the encoder's byte side (44.3). **It
|
|
spends every profitable byte, which raises `wire`, which shrinks `pipe - wire`,
|
|
which lengthens the refill climb after every branch.** That interaction is not
|
|
priced, and M3 is where it becomes measurable.
|
|
- **E3. Re-derive span selection jointly with `lam`** (39.3).
|
|
- **C2. Framing** — crop vs squash vs wide (FINDINGS 12). Needs an eyeball
|
|
against arcade reference, not a measurement. Cheap; blocks only final encodes.
|
|
- **C3. Disk image packaging**, ~1.09 GiB at the candidate rate.
|
|
- **G2/G3.** Branching, input windows, death clips, attract mode; playtest.
|
|
|
|
---
|
|
|
|
## Dependency summary
|
|
|
|
```
|
|
B1 seek+rate ─┐
|
|
B3 DTYP ──────┴─> P4 transport ─┐
|
|
├─> M2 ─> M3 (COMPLETION TARGET) ─> M4
|
|
P1 P2 P3 P5 P7 ─────────────────┘ ^
|
|
│
|
|
P6 (bus cost DONE, 52) ──────────────────┤
|
|
G1 scene graph (fetch, do early) ─────────┘
|
|
B2 blanking ─> (page 1; do not pre-build on it)
|
|
```
|
|
|
|
## Standing rules that apply to all of it
|
|
|
|
- **Green light first and last.** `./tools/bench/check.sh`, ALL GREEN, before and
|
|
after. **Never two MAME jobs at once** — session 18 did it, two `decode.lua`
|
|
runs shared a log file, and it produced a 0-byte log and 15 wasted minutes.
|
|
- **Name the layer.** Emulated, or real hardware. Every progress claim.
|
|
- **Label measured / estimated / folklore.** A rate with no provenance is
|
|
folklore even when it is plausible, and this project has already paid for that
|
|
twice.
|
|
- **No new default constants.** Rates stay explicit arguments. If a measurement
|
|
is not available, report the sensitivity across several rates rather than
|
|
picking one.
|