Files
Dragon-s-Lair-X68k/docs/ROADMAP.md
T
prosolis f925a1dd9a Put sound on the wire, and find three LSBs are worth 25 dB
ROADMAP P6, everything in the item except the bus half session 20 closed.
tools/encoder/adpcm.py is an MSM6258 codec, tools/encoder/extract_audio.py
takes the same seconds of the same stream the frames come from,
tools/bench/verify_adpcm.py is the gate, tools/analysis/32_audio_wire.py the
container arithmetic.

There is no reference encoder -- ffmpeg has a decoder for this format and none
the other way -- so what is gated is the decoder the encoder runs INSIDE its
own nibble search, sample-exact against ffmpeg's over 4,268 nibbles. An
encoder that agrees with its own wrong decoder is what that catches. The Singe
window: 156,250 samples -> 78,125 B at 21.97 dB, which is 7,812.5 B/s to the
byte. Normalising the disc's -13.4 dBFS level moves the SNR 21.97 -> 21.97, so
the level is not a lever.

And the two published delta formulas are not the same codec. They differ by at
most 3 in 12-bit units; encode for one and decode on the other and the SNR
goes 21.97 -> -2.88 dB, the noise louder than the signal, because ADPCM is
recursive the way the video codec is temporally recursive. Which one the chip
runs is now P6a and it is a precondition on shipping any audio.

And audio is the first thing the packed branch's simplification has cost
anything for. A record has no index BY DESIGN, so audio cannot be per-record
without making records variable; it rides a fixed cadence (F, A), the obvious
F=1 wastes 57.3% of every audio sector, and the pick is F=11 A=14 -- 0.09%
padding, 14,336 B held, wire 582.0 -> 589.6 KB/s. The codec container, which
kept its index, pays zero.

The MAME experiment did not work and 65.5 says so: :okim6258 is there at
$E92001/$E92003, read out of the machine's own program map, and feeding it
from Lua recorded silence across control 0..3 x port C 0..15. The register
semantics were not guessed at further.

FINDINGS 65. check.sh ALL GREEN before and after, with a new stage.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 09:11:03 -07:00

878 lines
55 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.
# 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).
Amended end of session 22: P3 done (FINDINGS 54).
Amended end of session 23: P5 done (FINDINGS 55).
Amended end of session 24: G1 done (FINDINGS 56).
Amended end of session 25: P4 HALF done (FINDINGS 57).
Amended end of session 26: P4b done, P4a is the last open item before M2
(FINDINGS 58).
Amended end of session 27: P4a done at the transport level; THE RE-ENCODE
BUNDLE under P2 is now the only thing between this tree and M2, because 59.4
made sector-aligned records a precondition the transport enforces rather than a
preference (FINDINGS 59). **And 59.7 re-ranks what is left: the frame affords
6.74 clocks a byte, a dual-address byte costs 9, so B3 stopped being a constant
to look up and became the question of whether the design fits at all.**
Amended end of session 28: **THE RE-ENCODE BUNDLE IS DONE — all four items, one
re-measurement (FINDINGS 60).** The container is DLX5, sector-aligned, and the
disc now moves exactly the records. Two of the four closed as NEGATIVES: E2's
`--spans all` default is refused on measurement, and E3's joint span/lam
selection emits byte-identical containers because `lam` never leaves its floor.
**What is left of M2 is P4a's wiring** — the DMA channel behind `ring.i`'s
mailbox — and the budget did not move: headroom 6.74 -> 6.69 clk/B, so every
conclusion in 59.7 stands.
Amended end of session 29: **THE DECODER-FREE PACKED PLAYER IS BACK, MEASURED,
AND IT FITS THE CLOCK BUDGET THE CODEC MISSES (FINDINGS 61).** The packed
full-frame blit is **27.3%** of a 12 fps frame — measured, not assumed — a
channel fills GVRAM in buffer mode off the disc with the CPU halted, and it
walks the 1,024 B line stride itself through array chaining. At the 9 clk/B
floor the codec is 110.4% and a decoder-free packed player is **55.2%**. It asks
**576 KB/s, fixed**, against E7's 327 KB/s target. **So B1 stopped setting how
much headroom the player has and started deciding WHICH PLAYER EXISTS**, and B2
stopped being a nice-to-have. The codec cannot be packed: 47.6.4 is closed and
the answer is no, both ways (61.3).
Amended end of session 30: **K1 IS DONE AND THE ANSWER IS THE GOOD ONE
(FINDINGS 62).** A channel writes the palette registers at `$E82000` byte-exact,
and ONE array-chained start crosses from device registers into GVRAM — so a
frame is a palette entry and 192 row entries, started once, with the CPU halted
throughout, and the array is scene-constant. **It opened B4**: MAME models the
palette as plain `COMBINE_DATA` storage with no handler to be wrong about, so
the run bounds the model and not the board, and what a real palette register
does with a byte write is UNMEASURED. B4 is the cheapest hardware item in the
project and a negative costs 0.28% of a frame. **K2, the packed container, is
next.**
Amended end of session 31: **K2 IS DONE, AND IT COST THE BRANCH SOMETHING
(FINDINGS 63).** DLXP1 is a 49,664 B record = 97 sectors exactly, no index, no
decoder, 582.0 KB/s — exactly 61.9's prediction — and px68k's own `gvram.c`
renders the container's bytes index-exact with the harness computing no
interleave. 61.9's picture claim survives the real builder: **34.05 dB against
its 34.08**, and the hardware GRB555 word is charged on top for the first time
in this project (0.53 dB, on every row, so it moves nothing). **But the control
row landed exactly on the codec's ceiling**, so the whole +2.31 dB is the
PER-FRAME PALETTE and nothing else — and 90% of that palette changes every
frame, which makes a mismatched paint **12.8 dB worse** than the correct
pairing. **B2 now decides which packed CONTAINER exists, not only which player**
(63.4). The fallback is a flag: `--scene-palette --no-palette` is 30.79 dB, zero
churn, 576.0 KB/s and still +2.07 dB on the shipping codec. **K3 is next.**
Amended end of session 32: **K3 IS DONE, AND IT FOUND THE THING THAT DECIDES
THE BRANCH (FINDINGS 64).** `src/player/packed.s` brings up its own display,
builds its own 193-entry chain, keeps its own clock off V-DISP and fetches every
record itself off a CZ-6BS1: **120 of 120 frames pixel-exact, every one
compared, in both palette orders.** Two things came with it. **(1) The write
window is the frame.** A packed write needs R20 bit 11, buffer mode blanks the
layer, and a DMAC-direct player holds the window open for the whole data phase
— so the dark fraction of a slot is `record / (DATA-PHASE rate x slot)` and a
medium that exactly meets the 582.0 KB/s sustained requirement **displays none
of the frames it delivers on time**. The rate that matters here is the BURST
rate, which is a third hardware number **B1 has no test for**. It also
**reverses 61.5's ranking**: a packed player that DMAs to RAM and paints with
the CPU opens the window only for the measured 27.3% blit, so it is on screen
72.7% of every slot at any rate, and the two are equal only at **2,131 KB/s =
3.7x the wire**. **(2) A held channel costs the frame clock half its ticks and
the clock cannot tell** — 46.9% of V-DISP edges lost, zero late frames reported,
the player believing 12 fps while the screen ran at 6.37. **The open item is now
K4.**
Amended end of session 33: **P6 HAS AN ENCODER AND A PRICE (FINDINGS 65).**
`tools/encoder/adpcm.py` is an MSM6258 codec whose in-loop decoder is gated
SAMPLE-EXACT against ffmpeg's `adpcm_ima_oki` — there is no reference encoder
for this format, so that is the only check available and it is the one that
catches an encoder agreeing with its own wrong decoder. The Singe window encodes
to **78,125 B at 21.97 dB**, which is 7,812.5 B/s to the byte and 52's figure
arriving from the other direction, and **normalising the disc's 13.4 dBFS level
buys 0.00 dB**, so the level is not a lever. Two things came with it. **(1) The
two published delta formulas disagree by at most 3 in 12-bit units and that is
worth 25 dB** — encode with one and decode with the other and the SNR goes from
21.97 dB to **2.88 dB**, because ADPCM is recursive the way the codec is
temporally recursive (64.1). Which one the chip runs is now **P6a** and it is a
precondition on shipping any audio. **(2) The packed container's own best
property is what makes audio cost.** A record has no index BY DESIGN, so audio
cannot be per-record without making records variable; it rides a fixed cadence
`(F, A)`, the obvious cadence F=1 wastes **57.3%** of every audio sector, and the
pick is **F=11, A=14 — 0.09% padding, 14,336 B held, wire 582.0 → 589.6 KB/s**.
The codec container, which already has an index and variable records, pays
**zero** padding. That is the first cost anyone has found for the packed
branch's simplification, and 64's risk list predicted there would be one without
knowing what.
**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.** Session 27 added the row underneath it: with **no external request line** on the card (59.2) the channel is auto-requested and is charged **by time rather than by byte**, so at 460 KB/s a 50% bus share costs **10.61 clk/B** and a smaller share cannot carry the rate at all (59.3). |
| **The frame's headroom for a transport** | **6.69 clk/B** — 30.2% of a 12 fps frame, after the MEASURED decode (68.6%) and best-case audio (1.25%), at the DLX5 gate container's **37,585 B delivered record** (session 28: the sector pad is delivered, so `15_bus_occupancy.py` charges it). **It is the number every row above is read against**, and a dual-address byte's floor is 9. It was 6.74 against the DLX4 container; the bundle moved it by 0.05 and moved no conclusion. |
---
## 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, seek time AND the DATA-PHASE BURST
RATE.** The third one is session 32's (FINDINGS 64.2) and it is not a refinement
of the first: sustained throughput decides whether record *i* arrives before
slot *i*, and the **burst rate during the data phase** decides how much of the
slot the picture is on screen, because a DMAC-direct packed player holds the
GVRAM write window open for exactly as long as the transfer takes and buffer
mode blanks the layer. A drive with a read-ahead cache can pass the first and
fail the second. The acceptance test is **`record / (burst x slot)` = the dark
fraction**; at the container's own 582.0 KB/s it is 1.0, and the picture is never
displayed. **Session 29 gave this
a second acceptance test that is not a codec figure at all: 576 KB/s SUSTAINED,
which is what a decoder-free packed literal frame costs and cannot be talked down
from (FINDINGS 61.5).** A codec's bitrate is a lever; a literal frame's is
geometry. So the measurement now has three thresholds to be read against —
453.6 KB/s (the gate container needs no prefill), 327 KB/s (E7's target at the
dual-address floor) and 576 KB/s (no decoder at all) — and which of them the
medium clears decides which player gets built.
Throughput has an acceptance test already derived from real record sizes:
**513.2 KB/s** for the session-14 candidate, **453.6 KB/s** for the DLX5 gate
container (`19_ring_stream.py`, FINDINGS 49.5; the figure was 451.4 before
session 28's re-encode and the sector pad raised it). 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).
> **Session 29 raised what this is worth, and gave it a number to be worth
> (FINDINGS 61).** It used to gate a derived halving. It now gates a player that
> has been measured to fit a budget the shipping design misses — 55.2% of a frame
> against 110.4% at the 9 clk/B floor. And 61.6 found an asymmetry worth carrying
> to the board: the black interval is the PAINT, not the frame, so a CPU-painted
> packed player is dark for **27.3%** of a frame while the cheaper DMAC-direct one
> is dark for **30..113%**. Under MAME's reading the cheap architecture is the
> dark one. **B2 and B1 are now the same decision from two sides, and B2 is the
> five-minute half.**
>
> **Session 31 raised it again, from the other direction (FINDINGS 63.4).** If
> buffer mode does NOT blank, the packed player's per-frame palette is not
> merely visible during the paint — **231 of 256 entries change every frame**,
> and a picture under the neighbouring frame's palette is **12.8 dB worse** than
> the correct pairing, on screen for roughly half of every frame slot at 12 fps.
> So a non-blanking board does not just cost the packed player a blank interval,
> it may cost it the per-frame palette — which is **the whole +2.31 dB** the
> branch has over the codec's ceiling. The fallback is already an encoder flag
> (`pack.py --scene-palette --no-palette`: 30.79 dB, no churn, 576.0 KB/s), so
> what B2 decides is now **which packed container ships**, not whether one can.
**B4. Does a real palette register take a BYTE write?** (62.4, new session 30.)
`$E82000` is 256 16-bit registers. The decoder-free packed player's per-frame
palette rides the frame's array chain as a 193rd entry, which means a
dual-address channel with an 8-bit device port writes it **one byte at a time**,
even bytes into the high half of a register and odd into the low. **MAME cannot
be asked**: it maps the palette to `palette_device::read16/write16`, which is
`memory_array`'s `COMBINE_DATA` over plain storage — RAM that honours
`mem_mask`, with no handler that could refuse. So the run in 62 is a statement
about the model and not about the board.
**It is the cheapest item on this list, cheaper than B2**: write `$A5` to
`$E82000` and `$5A` to `$E82001` from the CPU and read the word back. If it
comes back `$A55A` the palette rides the chain. **The blast radius of a negative
is small and known** — the palette leaves the chain and the CPU writes 256 words
a frame at 61.9's derived 0.28% of a frame — which is why this is B4 and not
above B2.
**B3. Single-address vs dual-address DMA — and now, first, DOES THE CARD DRIVE
`#EXREQ`?** 242 KB/s and 0.69 dB was the old framing. **Session 27 promoted this
to the item that decides whether the design fits at all (59.7).** The frame
affords **6.74 clocks a byte**; a dual-address byte is **9** — a 4-clock read of
the device plus a 5-clock write to memory — so **no dual-address configuration
fits this container at 12 fps, at any delivery rate and any GCR share.** Single
address is 5 clk/B and fits at 92.2% of the frame with 7.8% to spare, and it
needs the device to ACK the DMAC directly, which needs the request line.
**MAME cannot answer it**: `x68k_scsiext.cpp`'s `drq_w` only stores a flag and
the expansion slot has no request path to the HD63450 at all (59.2). The slot
PINOUT has `#EXREQ` at B36 and `#EXACK` at B37, so the provision exists on the
real board. **What is wanted from hardware is therefore narrower and sharper
than "disassemble the ROM": does a CZ-6BS1 assert `#EXREQ` during a data phase,
and will the HD63450 run a single-address channel against it?** A scope or a
logic analyser answers the first; the second is a program the player can run.
Sourcing `scsiexrom.bin` (8 KB, CRC `7be488de`) and disassembling its DMAC init
is still the cheapest paper route to the same answer, because whatever Sharp's
own driver programs into DCR's DTYP is a statement about what the card supports.
FINDINGS 48.4. Not on this machine (checked, session 18).
**Ranking, amended session 29.** It was: B1 sets how much headroom the player
has, B3 decides whether there is any. FINDINGS 61 adds a third reading — **B1
and B2 together decide which player exists.** If the medium clears 576 KB/s
sustained and buffer mode does not blank, the decoder-free packed literal fits at
the dual-address floor B3 cannot get under, and B3 stops mattering for video at
all. If it does not, B3 is still the question. The three hardware facts are no longer
independent, and B2 is by far the cheapest of them.
> **ENCODER WORK IS PARKED — USER DECISION, session 29.** The first draft of this
> amendment said "nothing here is a reason to stop work on the codec". That does
> not survive its own arithmetic. It rested on comparing the packed player's
> 576 KB/s against **E7's 327 KB/s target, which does not exist**: the codec that
> exists is 440 KB/s and 110.4% of a frame, so the real gap is 1.31x, not 1.76x.
> And the branches are not symmetric. **Packed needs two facts** — buffer mode
> does not blank, medium clears 576 KB/s. **The codec needs E7 to succeed** —
> unproven, and 60.4/60.5 measured two of its three levers inert — **AND** the
> medium to clear 327, **AND** it ships at ~100% of the frame with no margin,
> which is where 55.2% is now.
>
> **E7 and E4 are both parked**, E4 included: `H.build`'s k-means builds VQ
> codebooks and a literal player has no VQ. C1 is gated by E4 and follows.
>
> **The codec is KEPT AND NOT BUILT ON.** That is inventory, not work. B2 is
> unanswered and 48.1's prior leans against packing — an assertion against a
> silence — and if buffer mode blanks there is no version of the packed player
> that is merely expensive (48.3), at which point the codec is the only path
> left. Keeping a working decoder on disk costs nothing; building on it costs
> sessions.
**The older ranking, which still holds inside the codec branch:** B1 (throughput
and seek) sets how much headroom the finished player has. **B3 decides whether
there is any.** If the card drives `#EXREQ`, the ladder applies and the design fits with
room. If it does not, the fallback is limited-rate auto-request at a share the
player picks (P4c), and the container has to come down to **328 KB/s of payload**
to fit at the 9 clk/B floor — 34% below where the gate container sits (59.7).
> **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.
---
## The packed branch — what building it means (session 29, USER DECISION)
**This is where the work goes now.** FINDINGS 61: a decoder-free packed literal
player is **55.2% of a frame at the 9 clk/B dual-address floor** against the
codec's 110.4%, and **+4.89 dB** on the shipping container because a literal
frame is not tied to a scene palette the codec's codewords index into. It costs
**582 KB/s, fixed, with no lever.** Three items, in order.
~~**K1. Can a DMA channel write the palette registers at `$E82000`?**~~
**DONE, session 30 — FINDINGS 62. YES, in this model.** `dmagate.s` runs 79:
512 B off the disc into the whole graphic palette, **byte-exact in 256
register words** read back out of `$E82000` by the 68000; the same transfer
aimed at RAM leaving the palette as the CPU poisoned it, which is what
attributes the first run to the channel's `MAR`; and **ONE array-chained start
crossing from device registers into GVRAM**, which is the shape of a whole
frame. The destination is POISONED first (62.1) because "it matches" was a
weak claim against a record that is mostly pad, and the host counts the
poison's discriminating power rather than assuming it: 511 of 512.
**And the array is SCENE-constant** (62.3) — the packed layout spends both
256-colour pages, so there is no page to flip and the 193 destinations never
change; the 1,158 B array is built once at scene setup. What is left on the
CPU per frame in the video path is the channel start and the READ(10), and
neither is priced — say "no per-frame PAINT work", not "no per-frame CPU work".
**It opened B4** (62.4): MAME models the palette as a generic `palette_device`
over `memory_array`, whose `write16` is a plain `COMBINE_DATA`, so it has no
handler to be wrong about and cannot discriminate. And it filed one open
design choice, 62.5: palette FIRST or 193rd is visible on screen and is not
decided. **Session 31 PRICED it and it is a wash — 20.32 dB against 20.33
(63.4) — so it is a container flag (`--palette-last`) and K3 runs both.**
~~**K2. A packed container.**~~
**DONE, session 31 — FINDINGS 63.** `tools/encoder/dlxp.py` is DLXP1 and
`pack.py` writes it: 254 colours with index 0 held free and black at 255,
column *i* interleaved with *i+128*, a per-frame palette, and a **49,664 B
record that is 97 sectors EXACTLY** — the alignment that cost session 28 a
re-encode is free here because a packed record's length is geometry. **No
index and no length word**, for the same reason: nothing has to be walked.
**582.0 KB/s, which is 61.9's prediction to the tenth.** Encodes in 3.3 s
because there is no k-means in it.
**The re-derivation is done and 61.9 survives it: 34.05 dB against 34.08**,
and the GRB555 word is charged for the first time (63.3) — 0.53 dB, on every
row of the table, so it moves no comparison. **The two reserved entries cost
0.0003 dB.**
**What it also found** (63.4): the SCENE-palette control lands exactly on the
codec's ceiling, so the whole +2.31 dB is the per-frame palette; 90% of that
palette changes every frame; and a mismatched paint is 12.8 dB worse, for
roughly half of every frame slot, if buffer mode does not blank.
- ~~**K3. End to end, off the disc.**~~ **DONE, session 32 — FINDINGS 64.**
`src/player/packed.s`, `tools/bench/packed.lua`, `tools/bench/packed_run.sh`,
`tools/bench/verify_packed.py`, `tools/analysis/31_display_duty.py`. Palette,
page-1 X-scroll 384, priority `vc1 = 0x0002`, R20 bit 11, one chained DMA a
frame, **120 of 120 pixel-exact in both palette orders** — and the gate checks
every frame rather than the last, because a packed frame is a literal and the
codec's recursion was what made one comparison audit 120.
**What it found is K4's whole content**, and it is in the amendment above: the
window is the frame, the burst rate decides visibility, and a held channel
eats the clock.
- **K4. THE PACKED PLAYER THAT IS ON SCREEN.** 64.2's option B: DMA the record
into one of two RAM buffers with the window SHUT, then paint it with the
packed `movem` blit (`blit.s` V8, **measured** at 227,553 clocks = 27.3% of a
slot). **82.2% of a frame at the 9 clk/B dual-address floor** against A's
54.9%, **99,328 B of RAM**, and a picture on screen **72.7% of every slot at
any delivery rate** instead of 0% at the container's own wire.
It is not a rewrite of K3: `packed.s` keeps its display bring-up, its clock,
its transport and its record arithmetic, and what changes is the chain's
destination and the addition of a paint. **The one thing in it that has never
been run is the overlap** — a channel filling buffer *i+1* while the CPU
paints buffer *i*, which is the first time in this project that the DMAC and
the 68000 have had to want the bus at the same time for a whole scene.
**K4 is conditional on B2 the same way K3's ranking was**, and the condition
now cuts the other way: if buffer mode does NOT blank, A is on screen the whole
slot and K4 is 27.3% of a frame spent on nothing. **Do not build K4 before B2
is answered** — that is the same rule 61.7 wrote for the codec, applied to the
branch that replaced it.
**What K3 deletes, and why that is a risk and not a win to be banked:** a
DMAC-direct packed player has **no ring**`ring.i`, `xfer.i` and most of
`stream.s` leave the video path, and **P4a's wiring is parked with them.** A
simplification that large usually hides something, and 61.7.2 names the specific
untested thing: a chained transfer has never run back to back at 12 fps.
**K1 and K2 survive a bad answer to B2. K3 does not.** ~~Do K1 first.~~ ~~K1 is
done (session 30, FINDINGS 62); K2 is next.~~ **Both are done. K3 is next — and
63.4 added a second thing for it to run: BOTH palette orders, which is a flag
(`--palette-last`) and not a re-encode.**
---
## 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.**~~ **DONE, session 28 — FINDINGS 60.**
The half that was open was the encoder's, and it closed with the whole bundle
below. Session 21 — FINDINGS 53 — did the packing itself:
~~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).**~~ **DONE, session 28 — 60.3.** `VQ.scene_palette` quantises
the picture into 255 entries and holds index 0 at (0,0,0); `pack_palette` gives
it `I = 0` by its own minimum-squared-error rule, so 23.3's "the bars sit at
RGB (4,4,4)" needed no special case. **0.04 dB** on the Singe window, palette
ceiling unmoved. Black is reserved, not withheld — the mapper may still spend
index 0 on genuinely black pixels; what it buys is that index 0 is black
whatever the scene contains.
~~**THE RE-ENCODE BUNDLE, collected here because it is now four items and they
share one re-measurement.**~~ **DONE, session 28 — FINDINGS 60. Two of the four
closed as NEGATIVES, which is the more useful half.** The four were:
1. ~~reserve palette index 0 as black, `I = 0`~~ **DONE** (23.4, 60.3);
2. ~~`--spans all` as the default~~ **MEASURED AND REFUSED** (E2, 60.4).
267.9 KB/s / 28.88 dB / 2 frames late at `need`, against 448.2 KB/s /
29.07 dB / 1 late at `all`: **+67% of the wire for +0.19 dB and one frame
of 120**, against a container the budget already says is 35% too big.
`need` stays the default. **It was called "the loaded lever on the byte
side" and it is — it is just loaded the wrong way**, and E7 is why. The
GATE container keeps `all`: it is a fixture, not a recipe;
3. ~~re-derive span selection jointly with `lam`~~ **IMPLEMENTED, MEASURED,
NO-OP** (E3, 60.5). `--joint-spans` hands the span pass's freed bytes back
to the lam search and re-spans; all four cells of `{need,all} x
{greedy,joint}` are BYTE-IDENTICAL, and so is `--rc-floor open`. **`lam`
never leaves its floor on any of 120 frames at either floor the encoder
offers** (44.3), so there is nothing to spend the freed bytes on. The code
stays, defaulted off, because a container that moved `lam` off its floor
would make the question live again;
4. ~~**sector-align every record**~~ **DONE — the container is DLX5** (58.3
option C, promoted to a precondition by 59.4; 60.1). Realised cost
**+0.48%** on the wire against the +0.43% predicted, zero clocks, and
**120/120 records start on a sector boundary** where 3/120 did. The disc
and the ring now move the SAME 4,510,208 B and check.sh gates on that
identity, both figures read out of the container instead of written into
the script — the old literals went red on the re-encode, correctly.
**The consumer had to be told too** (60.2): `stream.s` released the ring
to the last byte it READ, which strands up to 511 B of pad a record, and
the ring's own audit caught it on frame 0 while every frame still decoded
pixel-exact. The release rounds to `RECALN` now.
The letterbox no longer gets the palette's closest thing to black — item 1 put
true black at index 0, and `load.i` needed no change, as it said it would not.
~~**P3. A real frame clock.**~~ **DONE, session 22 — FINDINGS 54.**
`src/player/clock.i` derives the tick from the CRTC's own V-DISP through the
MFP, with a remainder-keeping divider whose two constants are read out of the
CRTC at init. **Exactly 12.000000 fps, by construction** — measured at 649 ticks
over 3,000 refreshes where 649.1429 were due, so the remainder still held and
nothing accumulated. **181.35 clocks per V-DISP, 838 per frame, 0.1006% of the
budget**, timed by the 68000 itself because the host's 17.64 ms granularity
cannot see it. `PACEON=0` free-run is untouched and so is the wait loop; the
free-running path executes none of the new code.
The item said "MFP timer or VBL" and **neither can do it alone**: 4e6/12 is not
an integer and no prescale/data pair reaches 12 Hz, while the slowest MFP tick
of any kind is 78.125 Hz; and the raster's 55.4577 Hz has no whole divide near
12 either (4 gives 13.86, 5 gives 11.09). `tools/analysis/23_frame_clock.py`
walks the whole space rather than asserting it.
**What it exposed is bigger than the item.** 12 fps on a 55.4577 Hz raster is
4.6215 refreshes, so a frame gets **4 refreshes (72.13 ms) or 5 (90.16 ms)** and
**there is no 83.33 ms frame** — that figure is the mean slot, and 37.9% of slots
are 13.4% under it. The cadence was ALREADY in every host-paced result in
FINDINGS 49/51, because `stream.lua`'s tick is sampled at frame boundaries and
its gaps were always 4 or 5; nothing had named it. On the gate container it
costs 4 frames of 120 their idle against 1 for the nominal model. **It is not a
dropped frame** — the pace gate lets an overrun eat the next frame's idle and
the clock recovers — but it means every budget in this project is priced against
a slot 37.9% of frames do not get. 54.4.
**Also struck: MAME's raster runs 2.22% fast** (`refresh_mode()` builds the frame
period from `htotal - 8`), so the tree's "1/55.46 s granularity" was 1/56.69 s
throughout. No 68000 cycle figure moves — the CPU clock is unrelated to the
screen — but anything paced by the raster does. 54.5.
**P4. Real transport. P4b DONE, session 26 — FINDINGS 58. P4a DONE at the
transport level, session 27 — FINDINGS 59. What is now between this tree and M2
is THE RE-ENCODE BUNDLE under P2, because the channel refuses a windowed read
(59.4) and 117 of 120 records need one.**
~~Drive the MB89352 instead of a host file.~~ `src/player/scsi.i` selects a SCSI
target and issues READ(10) on the 68000, with no IOCS and no host in the
transfer path: **4,096 B from LBA 0 and 2,048 B from LBA 1000, both byte-exact**
against the host's copy of the same volume.
**This item was listed as blocked and was not.** Session 21 recorded "MAME's
`x68000` has no MB89352 path"; `-exp1 cz6bs1` instantiates one, and FINDINGS
32.4 had read that card's DMA glue back in session 9. The real gap was the 8 KB
`scsiexrom.bin` MAME needs to instantiate the card and **the player never
executes**; a blank placeholder on a separate rompath settles it. **B3 still
wants the real ROM's bytes** and is untouched by this.
**What is left is the half that decides the project**, and it is now two pieces:
~~**P4a. A DMAC configuration that HOLDS THE BUS.**~~ **DONE at the transport
level, session 27 — FINDINGS 59.** `src/player/dma.i` programs HD63450 channel 1
and takes the DATA IN phase: **the same 2,048 B off the disc three ways — PIO,
held, stealing — all three byte-exact.** 57.3's warning was met rather than
worked around: the evidence never reads `$EA0015`. **MTC is sampled by the
instruction after the one that starts the channel, and held it reads zero of
2,048** — the whole transfer happened between two instructions, because the
68000 did not execute in between — against the full count and 426 CPU loop trips
for the stealing configuration. Put the stealing registers in the held slot and
every byte still arrives and the gate goes **red**, which is what says the
counter can come out different (58.3's vacuous-counter trap, avoided
deliberately).
**Three bounds on the apparatus, read out of MAME's source and not inferred**
(59.2): the card has **no request line to the DMAC** (its flow control is
DTACK), so external request — the mode the `W`=5 and `W`=12 rows assume —
cannot be run; **single address** cannot be run either (only channel 0 has
device callbacks); and **only burst is modelled as held**. Of the four rows of
the ladder exactly one, dual address held, has a code path here, and it is the
one demonstrated. The slot pinout has `#EXREQ` at B36, so a real card plausibly
drives it — **that is now B3's sharpest form**.
~~**What is left of P4a is downstream of the container, not of the DMAC**
(59.4)~~**and that block is GONE as of session 28.** `sc_in_data` refused a
windowed read because a channel cannot drop the 300 B in front of a record; the
container is DLX5 now and no record asks for a window. `xfer.i`'s sector
arithmetic already degenerates correctly — `SC_WSKIP` is 0 and `SC_WKEEP` is the
whole record on every one of the 120 — so **what is left of P4a is the wiring:
which loop moves the bytes.** `dma_run.sh`'s windowed-read refusal stays as a
negative control rather than as a description of the container. **This is the
next item, and it is the last one before M2** (60.9).
**P4c (new, and it is a DESIGN CHOICE the tree had not named).** Auto-request is
charged **by time, not by byte** — the channel spends its share of the bus
whether or not a byte is there, so halving the delivery rate DOUBLES the CPU
cost of the same record. The MC68450's GCR sets that share: `BT`/`BR`, four
values, 50/25/12.5/6.25%. `tools/analysis/28_autorequest_cost.py` prices it
against an explicit rate; at 460 KB/s **only the 50% share carries this
container**, at 10.61 clk/B and 47.6% of a frame per record, against 40.4% for
the `W`=9 row and 391.8% measured for PIO. **If B3 comes back saying the real
card drives `#EXREQ`, the ladder applies and this is the fallback; if it does
not, this IS the cost model** and the GCR pair is a number the player has to
choose.
~~**P4b. `scsi.i` behind `ring.i`'s `XF_*` mailbox.**~~ **DONE, session 26 —
FINDINGS 58.** `src/player/xfer.i` answers the mailbox with a real READ(10) per
record: **120 records, 4,488,588 B, pixel-exact, out of the same 256 KB ring,
with a real mid-stream seek in a second pass**. The tiling is the SAME 18 wraps
and 14.7 KB mean hole that 49.4's host producer and 55.4's modelled transport
produced — a third transport, same placement, which is the assertion that
`ring.i` could not tell which side of the seam answered it. The change above the
seam is two `bsr`s, and the one in `ring_seek`'s quiet-wait is not optional:
with the transport inside the machine, that loop is the only thing that can
retire an outstanding request.
**What it cost is the finding, and it re-prices P4a.** `tools/bench/
xfer_cost.sh` subtracts the same 120 frames run twice and gets **87.28 clocks
per delivered byte** — against the 68000's own cycle table for the loop, which
says **87.15**. **0.2% apart**, so it is the instruction stream and not MAME's
device model, and it is therefore the first number this rig has produced that
survives leaving the emulator. At this container's 37,405 B mean record that is
**391.8% of a 12 fps frame**, and the machine's own V-DISP clock agrees from the
other end: **2.57 fps**.
W = 5 single address, bus HELD ............................ 22.4%
W = 9 dual address, held .................................. 40.4%
W = 12 single address, arbitrated .......................... 53.9%
W = 19 dual address, arbitrated -- the IPL ROM's own (52.5) . 85.3%
PIO 87 MEASURED, session 26 ................................ 391.8%
**So P4a is worth 4.6x the worst DMA configuration in this tree and 17.5x the
best**, where before this session it was worth 9 against 19. `W` itself **did
not move by one clock** and is still the largest open number — but what depends
on it just got much larger.
**One more thing P4a inherits (58.3).** A record is not a sector: 117 of 120
start part way into one. PIO absorbs that for free because the CPU is already
touching every byte and simply does not store the ones outside the window — a
property that **disappears the moment the DMAC takes over**, because a channel
writes a contiguous run and cannot drop bytes. The three ways out price as
+1.34% wire and no DMA (windowed PIO), +1.34% wire and **+5 clk/B of copy**
(bounce buffer, which is exactly the cost `aligned` was chosen over `split` to
avoid), or **+0.43% wire and zero clocks** (sector-aligned records in the
container). The last one wins on both axes and is a **re-encode**; see the
bundle under P2. **P4a should be attempted against a sector-aligned container,
not against this one.** *(Session 27: it was, in the only sense that mattered —
the transport now REFUSES the windowed case rather than being trusted not to
reach it, so the bundle is a precondition rather than a plan. 59.4. Session 28:
the container IS one — the realised wire cost is +0.48% against the +0.43%
predicted here, and 120/120 records start on a sector boundary. 60.1.)*
*(original item, still the standing description of the `W` question:)*
Drive the MB89352 instead of a host file. **Session 23
added a second axis to it:** `W` is the clocks stolen per delivered byte, and
55.3 measured that the player's own request loop gives away 3-7% of the pipe
before `W` is even asked about. A transport design has to answer both. 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.**~~ **DONE, session 23 — FINDINGS 55.**
`src/player/ring.i` fills the ring on the 68000: `aligned` placement, the
descriptor ring, a prefill policy, 51.2's slack rule as arithmetic the player
can run (`ring_may_seek`), and a seek that quiets the channel and re-addresses
the stream out of the index. It reproduces the host producer's tiling exactly —
18 wraps, 14.7 KB mean hole, pixel-exact — and the host now AUDITS every
placement instead of making it.
The index is a **container change**: DLX4 carries `nframes` u16 record lengths
in the scene header, because `aligned` needs a record's length before it fetches
it and walking the stream is precisely what a player cannot do. Frame payloads
are byte-identical to the DLX3 encode; the scene header goes 5,920 to 6,164 B.
**What it exposed is bigger than the item.** A channel only moves bytes while it
has a request and only the CPU can issue one, so the disc **stands still between
records** by an amount set by the player's loop rather than by the medium — and
no host-filled run could see it. At 488 KB/s in a 256 KB ring, a one-deep
request queue gives away **6.8% of the pipe and underruns 59 of 120 frames**; a
two-deep one gives away 3.4% and underruns none. The container's whole surplus
over the wire at that rate is 8.7%, so the player's own loop was spending most
of the slack 51.3 accumulates. **Prefill is the weaker lever** — six records of
it still leaves 24 underruns at depth 1 — and the fix costs no clocks and no
bytes. 55.3, 55.4.
**P5a (open, and it belongs with P4).** The two-deep queue is modelled as two
mailbox slots. On the machine it is two DMAC channels or one channel with a
chained descriptor array, and which of those is affordable is a `W` question.
**P7. Boot.** The player as an executable loading from the SCSI volume.
Buildable, and empty until P4: there is nothing to boot from yet.
---
## 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`.~~ **THREE OF THE
FOUR ARE DONE, session 33 — FINDINGS 65.** `tools/encoder/extract_audio.py`
takes the same seconds of the same stream the frames come from;
`tools/encoder/adpcm.py` encodes them (**78,125 B, 21.97 dB**, gated against
ffmpeg's decoder sample-exact by `tools/bench/verify_adpcm.py`);
`tools/analysis/32_audio_wire.py` is the interleave and the wire. **The packed
container's cadence is `F=11, A=14`** — 0.09% padding, 14,336 B held, 582.0 →
**589.6 KB/s** — and the naive one-lump-per-record cadence would have wasted
57.3% of every audio sector. **The codec container pays zero padding**, because
it already has the index the packed one deleted (65.4).
**What is left in P6 is the fourth: the refill climb with a second consumer
through a real branch point** (51.3, 55.4). The slack table is in
`32_audio_wire.py` — at 582.0 KB/s exactly, silent breaks even and sounded
starves — but a branch point has not been run with audio on the wire.
**P6a. WHICH DELTA FORMULA DOES THE MSM6258 RUN? (new, session 33, FINDINGS
65.2 — and it is a precondition, not a refinement.)** ffmpeg computes
`((2*(n&7)+1) * step) >> 3`; the OKI datasheet's form truncates per term. They
differ by **at most 3 in 12-bit units per sample** and, because ADPCM is
recursive, **encoding for one and decoding on the other costs 25 dB — the noise
comes out louder than the signal.** No encoder output can be committed to a
container before this is answered.
**It does not need a board.** MAME's x68000 HAS the chip: `:okim6258`, with the
68000 reaching it at **`$E92001` and `$E92003`** — both read out of the machine's
own program map by `tools/bench/probe_adpcm.lua`, not from folklore. What did
NOT work is feeding it from Lua: `probe_adpcm2.lua` / `probe_adpcm3.lua` swept
control 0..3 against PPI port C 0..15 and `-wavwrite` recorded silence
throughout (65.5). The gap is the register semantics, and the way to close it is
**from 68000 code with the IPL ROM's own channel-3 DMAC configuration**, which
`21_iplrom_dmac.py` already reads out of the ROM — the one ADPCM path in this
machine that is known-correct because Sharp wrote it. That is also the real
design, so it is not scaffolding.
**P6b. A CONTAINER WITH SOUND IN IT.** DLXP1 has no audio section. 65.3 is the
arithmetic a DLXP2 is built from and no byte of one is written. It waits on P6a,
because a container full of audio encoded against the wrong formula is 25 dB of
work to redo.
**E6. Container v2** — audio interleave, per-record index, scene table. Depends
on P6's answer and on P5's index.
**E7. A BYTE TARGET, AND IT COMES OUT OF THE BUS BUDGET RATHER THAN OUT OF
TASTE (new, session 27, FINDINGS 59.7; re-measured session 28, 60.7).** The
frame affords **6.69 clocks a byte** after the measured decode and the audio,
and a dual-address byte costs **9**. So *if* B3 comes back saying the card
cannot drive `#EXREQ`, the container has to reach **27,924 B a frame — 327 KB/s
of payload** to fit at 12 fps, where the DLX5 gate container delivers 37,585 B
and 440 KB/s: **35% too big.** (It was 6.74 / 328 / 34% against the DLX4
container. **The bundle moved the target by one KB/s and moved no conclusion**,
which is what a precondition is supposed to do.)
Three things make this less alarming than the number looks, and one makes it
worse:
- The gate container is **deliberately the heaviest thing the encoder emits**
(span-heavy, the 488 recipe, every block mode exercised). It is a test
fixture, not a shipping target.
- A lighter container **also decodes cheaper**, so the 68.5% decode term falls
with the byte term. 328 KB/s is the pessimistic reading of the lever.
- `rc_fr_singe_scsi_cpufit.dlx` already exists — the encoder has had a
CPU-fitting mode since session 11.
- **Worse:** `15_bus_occupancy.py` REFUSES to price the cpufit container,
correctly, because the C68K measurement it cross-checks against belongs to
the gate container. **So E7 starts with a harness re-run**
(`tools/bench/c68k/run.sh`) against whichever container is to be the target,
and until that is done "34% too big" is a statement about the fixture rather
than about the project.
~~**G1. Import the scene graph — early, because it is a measurement input.**~~
**DONE, session 24 — FINDINGS 56.** It was pulled ahead for exactly the reason
given, and it paid: **the worst gap between two consecutive decision points is
zero**, and 5.4% of the game's 612 branch transitions are. Two seeks can fall
back to back with no play between them, so 51.2's slack rule can be answered NO
by the content rather than by the buffer.
It does not break the design — a branch on an empty ring costs the prefill
(149.7 ms, 1.80 frame slots at 488 KB/s), not the climb — but it removes the
margin: at 488 KB/s in a 256 KB ring, **76% of this game's branch points arrive
before the ring has refilled**, and a 512 KB ring makes that 90%. **The ring is
not the lever; the surplus is.**
Two constraints on the input layer came with it: the arcade needs **eight
directions**, and the shortest input window is **98 ms** against a 72.13/90.16 ms
frame slot, so input cannot be polled on the frame tick (56.7).
**The cross-check plan was wrong and is struck.** The SNES chapters are
*derived* from DirkSimple, by their own README, so there is one transcription and
not two; the diff catches conversion errors only (56.2). **Nothing is vendored:**
`tools/import/scenegraph.py` is the one file coupled to those projects and it
writes this project's own `DLXSCENE1` schema into gitignored `tmp/`
(USER DECISION, session 24).
---
## 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, and parked with it (session 29).**
- ~~**E4. `H.build` k-means**, 51 s of a 55 s run, once per scene.~~ **PARKED,
session 29 (USER DECISION).** It was the thing to attack before C1. It builds
**VQ codebooks**, and a decoder-free packed player has no VQ — so this is
encoder work on the branch that is no longer being built on. It comes back if
and only if B2 goes MAME's way. C1 is gated by it and is parked with it.
- ~~**E2. `--spans all` as default.**~~ **MEASURED AND REFUSED, session 28 —
60.4.** It was "a recommendation, not a measurement" since 43.6.1 and it is a
measurement now: **+67% of the wire for +0.19 dB and one frame of 120.** It
IS the loaded lever on the byte side (44.3) — it is loaded the wrong way, and
E7 is why. What the entry predicted is exactly what it does: it raises `wire`,
which shrinks `pipe - wire`, which lengthens the refill climb. `need` stays
the default; the GATE container keeps `all` because it is a fixture.
- ~~**E3. Re-derive span selection jointly with `lam`** (39.3).~~
**IMPLEMENTED, MEASURED, NO-OP, session 28 — 60.5.** `--joint-spans` emits
byte-identical containers in all four `{need,all} x {greedy,joint}` cells and
at both lam floors, because **`lam` never leaves its floor on any of 120
frames**. Kept and defaulted off.
- **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
```
P4a DONE (59): the channel drives the data phase and
holds the bus -- 391.7% of a frame becomes 40..95%
B3 #EXREQ? ──┬─ YES ─> single address, 5 clk/B, 92.4% ── FITS ──┐
│ │
└─ NO ──> auto-request, 9 clk/B FLOOR, 110.4% ──> E7 byte target
(the frame affords 6.69; 59.7, 60.7) 327 KB/s
P2 re-encode bundle DONE (60): DLX5, records ARE sectors ─────────┤
E2 refused on measurement, E3 a no-op -- 60.4, 60.5 │
E7/E4/C1 PARKED session 29: encoder work waits on B2 ───────────┤
K1 palette-register DMA? ─> K2 packed container ─> K3 end to end ──┤
K1 DONE s30 (62), K2 DONE s31 (63), K3 DONE s32 (64): │
120/120 pixel-exact, both palette orders, off a real volume │
└─> and K3 found that the WRITE WINDOW IS THE FRAME, so: │
K4 (DMA to RAM + the measured 27.3% paint) is the player │
that is ON SCREEN below a 2,131 KB/s BURST rate -- which │
is 3.7x the wire, so below every rate anyone has proposed │
P4a WIRING (the channel behind ring.i's mailbox) <- THE LAST ITEM ─┤
P1 P2 P3 P4b P5 P7, P6 bus cost (52), G1 scene graph (56) ────────┼─> M2 ─>
B1 seek+rate (sets HEADROOM, not fit) ─────────────────────────────┘ M3 ─> M4
B2 blanking ─┬─ NOT blanked ─> K3's DMAC-DIRECT player is the one: 54.9% of a
│ frame at the 9 clk/B floor against the codec's
│ 110.4%, on screen the whole slot, and K4's paint
│ would be 27.3% spent on nothing (61.4, 61.5)
└─ blanked ──────> K3's player is on screen for
1 - record/(BURST x slot) of every slot, which is
ZERO at the container's own wire -- so K4 is the
player, at 82.2% of a frame and 99,328 B of RAM
(64.2). Neither answer kills the branch and each
picks a different player.
B1 BURST rate (NEW, 64.2) ──> which of the two K3/K4 wins, if B2 blanks
P6 audio DONE bar one (65): encoder gated, cadence F=11/A=14, 589.6 KB/s
└─> P6a WHICH DELTA FORMULA? ─┬─ needs no board: MAME has :okim6258
worth 25 dB, so it is a │ at $E92001/$E92003 (65.5)
PRECONDITION not a tweak └─> then P6b, a container with sound in it
```
**Read that top-left branch as the project's live question.** Everything else
on the diagram is work; `#EXREQ` is a fact about a board nobody here has, and it
decides which of the two lower paths the player is on.
## 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.