Files
Dragon-s-Lair-X68k/docs/ROADMAP.md
T
prosolis 7179339bd2 Move the loader onto the 68000, and find 5,920 bytes nobody counted
src/player/load.i expands both codebooks to word-per-pixel form and packs the
palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against
tools/bench/dlxload.py on both CPU cores.  The palette half is gated on words
read back out of the palette registers at $E82000, so "the words reached the
hardware" is part of what passes.  ROADMAP P1 is done; P2's encoder half (a
reserved black entry, 23.4) is not, and is a re-encode rather than an edit.

A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot
costs 24.70 ms.  The scratch tables describe the CRTC, not the scene, so
pal_tables is a separate entry point built once at boot -- 5.29 ms off every
scene change.

The one that moves something: the scene header is 5,920 B that no rate table in
this tree included, because it belongs to no frame record.  In FINDINGS 51.3's
currency it is divided by the surplus pipe - wire, so it is hypersensitive:
138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the
same bytes.  tools/analysis/22_scene_load.py prices it across explicit rates.

Recorded as open: the two CPU cores agree to <3% on every stage but the table
build, where they differ by 16.4%.  px68k's C68K charges a flat 50 clocks for
MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains
4,608 of the 8,703 clock gap.  4,095 clocks are unexplained.  Nothing else in
src/player/ multiplies, so no figure in FINDINGS 24-52 is affected.

decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same
MD5.  check.sh gains a stage that gates byte-exactness on both cores and
deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a
wall timing would make the green light host-sensitive.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-24 20:20:40 -07:00

267 lines
14 KiB
Markdown

# Roadmap — remaining work to a completion target
Written end of session 19 (2026-08-24), against a tree that is ALL GREEN.
Amended end of session 21: P1 done, P2 half done (FINDINGS 53).
**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** | 5 single-address held, 9 dual held, 12 single arbitrated; 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.**~~ **DONE, session 21 — FINDINGS 53.**
`src/player/load.i` expands both codebooks out of the raw container header,
byte-exact against `dlxload.py` on both CPU cores. **9.26 ms**, and it was
priced where it lands rather than treated as free setup: the scene header is
**5,920 B that no rate table in this tree counted**, and in the currency of
51.3 — accumulated slack — those bytes lengthen the refill climb by 138 ms at
488 KB/s and by **1.099 s at 451.4 KB/s**, because the surplus they are divided
by goes to zero. The whole fixed cost of a scene change is about a third of one
frame slot; what makes a branch point expensive is still the seek and the climb.
Shipping the codebooks pre-expanded was considered and refused: it trades
9.26 ms of CPU for 5,120 more header bytes, which is a wash in milliseconds and
not a wash in kind (53.6).
**P2. Palette packing on the 68000. HALF DONE, session 21 — FINDINGS 53.**
~~The encoder still emits RGB888; the X68000 word packing is Lua-side.~~ The
packing is on the 68000: `pal_pack` writes 256 words straight into `$E82000`
with `I` chosen per entry by minimum squared error (**1.96 dB**, 23.3), gated on
the words read back **out of the palette registers**. 9.70 ms per scene, plus
5.29 ms of scene-independent table build hoisted to boot (53.3).
**What is left is the other half of the sentence: reserve index 0 as black with
`I = 0` (23.4).** That is ENCODER-side, it changes the container, and it moves
every constant fitted to the gate container, so it is a re-encode plus a
re-measurement rather than an edit. Until then the letterbox gets the palette's
closest thing to black (index 255 on the gate container); `load.i` reports
whichever index that is and needs no change when it becomes 0.
**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**, where the whole design fails at any
container size (`15_bus_occupancy.py` sweeps it). The per-byte ladder is 5 clk/B
single-address with the bus held, 9 dual-address held, 12 single-address
arbitrated, 16..19 dual-address arbitrated. **Getting the DMAC to hold the bus
is the difference between 9 and 19**, it is a property of how the player
programs the channel, and demonstrating a configuration that does it is P4's
first job rather than its last.
**Do not quote 42.4's `W <= 6` / `W = 8` sensitivity table for this.** It is in
clocks per WORD and FINDINGS 43 voided it; 52.5 cited it in byte units when
first written and strikes it.
**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(half) 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.