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
This commit is contained in:
@@ -4477,3 +4477,144 @@ Relevant to 51's underrun analysis, which models delivery as a smooth rate.
|
||||
6. **The frame-period accounting assumes the DMAC does not overlap the CPU**
|
||||
(`buscost.DMA_OVERLAPS = False`), which is FINDINGS 35's premise: no cache,
|
||||
a two-word prefetch queue. A stolen bus cycle is a stopped 68000.
|
||||
|
||||
---
|
||||
|
||||
## 53. The loader moves onto the 68000, and a scene change finally has a price (session 21)
|
||||
|
||||
ROADMAP P1 and P2. Since session 1 the two load-time transforms have been done
|
||||
**host-side**, in `tools/bench/dlxload.py`, with the rigs pushing the *result*
|
||||
into emulated RAM: the codebooks expanded to word-per-pixel form (CB1 to 32 B an
|
||||
entry, CB4 to 8 B) and the 24-bit palette packed to `GGGGGRRRRRBBBBBI` with the
|
||||
shared LSB chosen per entry. That was the right call while the inner loop was
|
||||
what was being measured — charging a once-per-scene cost to the per-frame path
|
||||
would have flattered or damned it for no reason — but **a player has no host.**
|
||||
|
||||
`src/player/load.i` does both on the 68000, out of the raw container header as
|
||||
it comes off the disc. `tools/bench/loadgate.s` is its front-end, the way
|
||||
`decode.s` is `frame.i`'s.
|
||||
|
||||
**NAME THE LAYER.** Everything here is **emulated**: MAME 0.277 `x68000`,
|
||||
`-bios ipl10`, stock 10 MHz / 2 MB, cross-checked on px68k's C68K core. Nothing
|
||||
has run on real hardware.
|
||||
|
||||
### 53.1 It reproduces dlxload.py exactly, on both cores
|
||||
|
||||
`dlxload.py` stays the reference — what changed is **where the transforms run,
|
||||
not what they produce** — so the gate is byte-for-byte, not "close enough":
|
||||
|
||||
- **CB1 8,192 B, CB4 2,048 B, palette 512 B: identical.** A wrong codebook byte
|
||||
is a wrong colour in every block that uses that codeword, in every frame of
|
||||
the scene, and a wrong shared LSB is a *slightly* wrong colour, which is
|
||||
exactly the sort of defect that gets attributed to the codec.
|
||||
- **The palette half is read back out of the palette registers at `$E82000`**,
|
||||
not out of a RAM shadow, so "the words reached the hardware" is part of what
|
||||
passes.
|
||||
- **The darkest-entry index agrees too** (255 on the gate container). It comes
|
||||
out of an `argmin` whose tie-break has to match numpy's — first index at the
|
||||
minimum wins — and it is what the letterbox is filled with.
|
||||
- **Both CPU cores produced the same 10,752 bytes**, and the same as the host.
|
||||
|
||||
`tools/bench/load_run.sh` runs it and `check.sh` gates it.
|
||||
|
||||
### 53.2 What it costs, measured on two cores
|
||||
|
||||
| stage | MAME clocks | C68K clocks | Δ | data bus (C68K) |
|
||||
|---|---|---|---|---|
|
||||
| scratch tables (boot only) | 52,919 | 61,622 | +16.4% | 6.5% |
|
||||
| **P1** codebook expansion | 92,609 | 95,304 | +2.9% | **43.2%** |
|
||||
| **P2** palette entries | 97,019 | 97,348 | +0.4% | 14.3% |
|
||||
| BOOT: all three | 246,957 | 253,614 | +2.7% | 23.2% |
|
||||
| **SCENE CHANGE: P1 + P2** | **189,627** | 192,322 | +1.4% | 28.6% |
|
||||
|
||||
**A scene change costs 18.96 ms of 68000 time — 22.8% of one 12 fps frame.**
|
||||
Boot costs 24.70 ms. Bus occupancy is data accesses only (C68K does not see
|
||||
prefetch), so it is a lower bound; the expansion is the bus-heaviest thing here
|
||||
because it is a pure copy, and it still runs alone.
|
||||
|
||||
**The stages are exactly additive on the exact core.** `P1 + P2 - SCENE = 330`
|
||||
clocks, and `tables + P1 + P2 - 2x330 = 253,614 = BOOT`, to the clock — 330 is
|
||||
the front-end's own per-pass overhead. On MAME the same identity closes to 1.8%,
|
||||
which is one tick of its 1/55.46 s clock over the 0.99 s run. Two instruments,
|
||||
two granularities, one arithmetic.
|
||||
|
||||
### 53.3 The scratch tables are scene-independent, so they are not in the scene path
|
||||
|
||||
Packing a palette entry needs the squared error of both choices of the shared
|
||||
LSB, per channel. That is three table reads and a sign test here, out of three
|
||||
tables — the 6-bit-to-8-bit rendering the CRTC performs, its square, and the
|
||||
per-channel error difference — and **not one of them describes the scene.** They
|
||||
describe the machine. `pal_tables` is therefore a separate entry point from
|
||||
`pal_pack`, built once at boot: **5.29 ms saved on every scene change**, 22% of
|
||||
what a naive port of `dlxload.py` would have charged per scene.
|
||||
|
||||
### 53.4 The two cores disagree only where the multiplies are, and C68K is wrong in kind
|
||||
|
||||
The table build is the only code in this tree that multiplies, and it is the
|
||||
only stage where the two cores disagree by more than 3%. **px68k's C68K charges
|
||||
a flat 50 clocks for `MULU` and `MULS`** regardless of the operand
|
||||
(`c68kmacro.h:1869/1883`, `RET(50 + EA_CLOCKS_...)`); the 68000 charges
|
||||
**38 + 2n**, n counting bits in the source. For the 576 multiplies this code
|
||||
executes, the real total is 24,192 clocks against C68K's 28,800: **the flat rate
|
||||
explains 4,608 of the 8,703 clock gap, and 4,095 clocks — 7.7% of the stage —
|
||||
are NOT explained.** Recorded as open rather than rounded away; the residual is
|
||||
somewhere else in the two cycle tables and this stage is not worth the hunt.
|
||||
|
||||
**The consequence is general and belongs in the reader's head:** where a future
|
||||
measurement contains multiplies, C68K over-charges them, and it is the second
|
||||
opinion this tree leans on for every cycle figure. Nothing else in
|
||||
`src/player/` multiplies — index scaling is `lsl.w #5`/`#3` by construction —
|
||||
so no figure in FINDINGS 24-52 is affected.
|
||||
|
||||
### 53.5 Where the cost actually lands: the scene change, priced
|
||||
|
||||
`tools/analysis/22_scene_load.py`, cycle counts parsed out of the rig's own log
|
||||
rather than pasted in as constants. Three costs in three units, and **the third
|
||||
is the one that compounds**:
|
||||
|
||||
- **BYTES.** The header region is **5,920 B** (palette 768 + CB1 4,096 + CB4
|
||||
1,024 + 32) and it must arrive before frame 0 can be decoded. It is not part
|
||||
of any frame record, so **no rate table in this tree has ever counted it.**
|
||||
- **CLOCKS.** 189,627, from 53.2.
|
||||
- **ACCUMULATED SLACK.** Those bytes are bytes the pipe did not spend filling
|
||||
the ring, so they cost play-time at the surplus rate `pipe - wire` — the
|
||||
currency FINDINGS 51.3 established a branch point spends.
|
||||
|
||||
| pipe KB/s | header ms | + load ms | total | frame slots | surplus KB/s | slack cost |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 451.4 | 12.81 | 18.96 | 31.77 | 0.38 | 5.3 | **1.099 s** |
|
||||
| 488 | 11.85 | 18.96 | 30.81 | 0.37 | 41.9 | 0.138 s |
|
||||
| 513.2 | 11.27 | 18.96 | 30.23 | 0.36 | 67.1 | 0.086 s |
|
||||
| 600 | 9.64 | 18.96 | 28.60 | 0.34 | 153.9 | 0.038 s |
|
||||
|
||||
(Rates are explicit arguments with no default, FINDINGS 50. `wire` is 446.1
|
||||
KB/s on the gate container, audio included.)
|
||||
|
||||
**Two readings, and the second is the finding.** First: the whole fixed cost of
|
||||
a scene change is **about a third of one frame slot** — it is not what makes a
|
||||
branch point expensive, the seek and the refill climb are. Second: **the slack
|
||||
cost is hypersensitive to the rate**, because it is divided by a surplus that
|
||||
goes to zero. At 488 KB/s the header lengthens the climb by 138 ms; at 451.4
|
||||
KB/s — the *arrival-deadline* rate for this same container, 49.5 — the same
|
||||
5,920 bytes cost **1.1 seconds of play**. The header is cheap only where the
|
||||
pipe already has room, which is the same place everything else in this project
|
||||
is cheap.
|
||||
|
||||
### 53.6 The alternative that was not taken
|
||||
|
||||
The encoder could ship the codebooks pre-expanded and P1 would not exist. That
|
||||
trades **9.26 ms of 68000 time for 5,120 more bytes in every scene header** —
|
||||
10.5 ms of pipe at 488 KB/s, and 5,120 bytes that lengthen the climb again by
|
||||
the arithmetic above. **Derived, not measured**, from the two figures either
|
||||
side of it. It is close to a wash in milliseconds and it is not a wash in
|
||||
*kind*: the CPU is idle during a seek and the pipe is the resource this project
|
||||
is short of. The transform stays on the 68000.
|
||||
|
||||
### 53.7 What is still open in P2
|
||||
|
||||
**The encoder still does not reserve a black entry** (23.4), so the letterbox
|
||||
gets the palette's closest thing to black — index 255 here — rather than a true
|
||||
black with `I = 0`. That half of P2 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. `load.i` is ready for it: it reads
|
||||
whatever the palette section holds and reports the darkest index either way.
|
||||
|
||||
+27
-11
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
@@ -96,17 +97,32 @@ 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.
|
||||
~~**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.** 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).
|
||||
**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
|
||||
@@ -229,7 +245,7 @@ Listed for completeness; past M3 these are scope, not risk.
|
||||
B1 seek+rate ─┐
|
||||
B3 DTYP ──────┴─> P4 transport ─┐
|
||||
├─> M2 ─> M3 (COMPLETION TARGET) ─> M4
|
||||
P1 P2 P3 P5 P7 ─────────────────┘ ^
|
||||
P1 P2(half) P3 P5 P7 ───────────┘ ^
|
||||
│
|
||||
P6 (bus cost DONE, 52) ──────────────────┤
|
||||
G1 scene graph (fetch, do early) ─────────┘
|
||||
|
||||
@@ -1,3 +1,93 @@
|
||||
# Status & next-session handoff — end of session 21 (2026-08-24)
|
||||
|
||||
## Session 21: the loader moves onto the 68000, and a scene change gets a price
|
||||
|
||||
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
|
||||
of this and ALL GREEN after**, 120/120 on both cores, no `TRUNCATED`, plus a new
|
||||
load-time stage.
|
||||
|
||||
**ROADMAP P1 is DONE and P2 is half done. FINDINGS 53.** Session 20's handoff
|
||||
named P4 as the item that decides the project, and P4 is **blocked in this
|
||||
tree** — re-checked, not assumed: there is still no `scsiexrom.bin` anywhere on
|
||||
this machine (`~/mame/roms/x68000.zip` holds six files, four IPLs, a cgrom and
|
||||
an sram), MAME's `x68000` has no MB89352 path, and `hd63450.cpp` decodes no
|
||||
DTYP. **Nothing here can measure W.** P1+P2 was the M2 item that could be built
|
||||
here, and it is the one that touches an already-measured number: it lands at a
|
||||
scene change, where FINDINGS 51.3's refill climb is.
|
||||
|
||||
**1. The transforms are on the 68000 and they are byte-exact.** `src/player/
|
||||
load.i` expands both codebooks to word-per-pixel form and packs the palette to
|
||||
`GGGGGRRRRRBBBBBI` with the shared LSB chosen per entry, out of the RAW
|
||||
container header. Gated **byte-for-byte against `tools/bench/dlxload.py`**,
|
||||
which stays the reference — what changed is where the transforms run, not what
|
||||
they produce. The palette half is read back **out of the palette registers at
|
||||
`$E82000`**, so "the words reached the hardware" is part of what passes. Both
|
||||
CPU cores emit the same 10,752 B. 53.1.
|
||||
|
||||
**2. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame.**
|
||||
Boot costs 24.70 ms. Split: codebooks 92,609 clocks, palette entries 97,019,
|
||||
scratch tables 52,919. Cross-checked on px68k's C68K, which agrees to 1.4% on
|
||||
the scene-change figure. 53.2.
|
||||
|
||||
**3. The scratch tables describe the machine, not the scene.** They are the
|
||||
CRTC's 6-to-8-bit rendering, its square, and the per-channel error difference —
|
||||
so `pal_tables` is a separate entry point, built once at boot. **5.29 ms off
|
||||
every scene change**, 22% of what a naive port of `dlxload.py` would have cost
|
||||
per scene. 53.3.
|
||||
|
||||
**4. THE ONE THAT MOVES SOMETHING: the scene header is 5,920 bytes nothing has
|
||||
ever counted.** Palette + CB1 + CB4, and it must arrive before frame 0 can be
|
||||
decoded. It is not part of any frame record, so no rate table in this tree
|
||||
includes it. `tools/analysis/22_scene_load.py` prices it across explicit rates.
|
||||
The whole fixed cost of a scene change is about **a third of one frame slot** —
|
||||
but its cost in FINDINGS 51.3's currency, accumulated slack, is divided by the
|
||||
surplus `pipe - wire` and so is hypersensitive: **138 ms of extra climb at 488
|
||||
KB/s, and 1.099 s at 451.4 KB/s**, for the same 5,920 bytes. 53.5.
|
||||
|
||||
**5. An instrument disagreement worth carrying forward.** The two CPU cores
|
||||
agree to <3% on every stage except the table build, where they differ by 16.4%
|
||||
— and that is the only code in this tree that multiplies. **px68k's C68K charges
|
||||
a flat 50 clocks for `MULU`/`MULS`** regardless of operand (`c68kmacro.h:1869`),
|
||||
where the 68000 charges 38+2n. That explains 4,608 of the 8,703 clock gap;
|
||||
**4,095 clocks are not explained and are recorded as open.** Nothing in
|
||||
`src/player/` outside these three instructions multiplies (checked), so no
|
||||
figure in FINDINGS 24-52 is affected — but the second opinion this tree leans on
|
||||
over-charges multiplies, and a future measurement containing one must not be
|
||||
taken from it uncorrected. 53.4.
|
||||
|
||||
**6. Shipping pre-expanded codebooks was considered and refused.** It would
|
||||
trade 9.26 ms of 68000 time for 5,120 more bytes in every scene header — 10.5 ms
|
||||
of pipe at 488 KB/s, and bytes that lengthen the climb. Close to a wash in
|
||||
milliseconds, not a wash in kind: the CPU is idle during a seek and the pipe is
|
||||
what this project is short of. **Derived, not measured.** 53.6.
|
||||
|
||||
**New in the tree:** `src/player/load.i` (the transforms) and
|
||||
`src/player/loadgate.s` (its front-end, 488 B); `tools/bench/prep_load.py`,
|
||||
`load.lua`, `verify_load.py`, `load_run.sh` (the rig, both cores);
|
||||
`tools/analysis/22_scene_load.py` (the pricing). `tools/bench/c68k/harness.c`
|
||||
gains a `--loadraw` mode, which also makes its flag-watch address a variable
|
||||
rather than a constant. `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, the
|
||||
same reason `blit.s` and `span.sh` are not in it.
|
||||
|
||||
**`decode.s` and `stream.s` are unchanged.** Nothing in the per-frame path was
|
||||
touched; `decode.bin` is still 1,296 B at the same MD5.
|
||||
|
||||
**Still open in P2:** the encoder does not reserve a black entry (23.4), so the
|
||||
letterbox still gets the palette's closest thing to black (index 255 here). That
|
||||
half is encoder-side, it changes the container, and it moves every constant
|
||||
fitted to the gate container — a re-encode plus a re-measurement, not an edit.
|
||||
|
||||
**Next:** P3 (a real frame clock from the MFP or VBL) and P5 (per-record index,
|
||||
prefill policy, the accumulated-slack rule in the player rather than the rig)
|
||||
are both buildable here. G1 (import the scene graph) is the one that would let
|
||||
this tree ask a question it currently cannot: what is the worst gap between
|
||||
consecutive decision points, and does the refill climb survive it. P4 still
|
||||
decides the project and still cannot be measured here.
|
||||
|
||||
---
|
||||
|
||||
# Status & next-session handoff — end of session 20 (2026-08-24)
|
||||
|
||||
## Session 20: the DMAC configuration was in the IPL ROM the whole time
|
||||
|
||||
Reference in New Issue
Block a user