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
267 lines
14 KiB
Markdown
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.
|