Measure the finer chain tail: 84/120 becomes 18/120, and the derivation was right by cancellation

blit.s gains v7 -- v6's 24-pixel movem chain plus a second chain whose unit is
one `move.l (a0)+,(a2)+`. Measured over 13 span lengths by span.sh, every config
pixel-exact:

    cycles = 66.0 per span + 9.143 per COARSE pixel + 9.978 per FINE pixel

fitting all 13 to within 0.2%. v5 and v6 re-measure to FINDINGS 30 exactly, so
the harness has not drifted underneath the new variant.

Rescored against the same scsi window and the same additive model, v7 takes
84/120 frames over budget to 18/120 -- exactly what FINDINGS 39.4 derived, and
that agreement is two cancelling errors: the derivation's 2-register movem tail
is 29% too dear per pixel, and its "nothing per span" for the second chain entry
is 22.3 clocks too cheap. The plain post-incrementing move.l is the right tail
instruction, and it makes the padding quantum 2 pixels, which a run of 4x4
blocks pads to exactly zero.

The DMAC stays dropped on a measurement now rather than an argument: v7 takes
back 37 of the 43 frames the array chain would, with no reserved channel and no
timing neither emulator here can verify. Break-even against all-V1 moves from
L=4 blocks to L=2.

The fine displacement is carried mid-stream rather than in the span record, so
the decoder holds nothing across the copy and keeps all 12 payload registers --
which is the whole reason the coarse unit is 24 pixels.

span.sh is now -seconds_to_run 200 (30 s wall, 36 configs) and takes its
expected snapshot count from the generated metadata instead of a literal 23.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-23 19:02:23 -07:00
parent c5ca56330e
commit c520a89e14
9 changed files with 522 additions and 52 deletions
+120
View File
@@ -2412,3 +2412,123 @@ end applies depends on how the MB89352 drives REQ and whether cycle-steal-with-
hold is used, which is a design decision the player has not made yet -- and it
is worth 7 clocks a word on a 480 KB/s stream, so it is worth making
deliberately.
---
## 40. The finer chain tail, measured: v7 (session 11)
FINDINGS 39.4 attributed **86% of the DMAC array-chain's advantage over v6** to
one thing that is not a property of the DMAC at all -- v6's 24-pixel padding
quantum -- and derived that fixing it in software would take the `scsi` window
from 55/120 frames over budget to 18/120. It labelled that figure DERIVED and
said it should not be believed until `span.sh` measured it. This measures it.
### 40.1 The result
`tools/bench/blit.s` gains **v7**: v6's 24-pixel coarse chain with a second,
finer chain appended. Measured over thirteen span lengths by
`tools/bench/span.sh`, every one of which drew a **pixel-exact** frame:
```
cycles = 66.0 per span + 9.143 per COARSE pixel + 9.978 per FINE pixel
```
fitting all 13 lengths to within **0.2%** -- and the fit is not flattered by its
own configs, because the three-term model was identified on span lengths chosen
so that every fine remainder a real span can have (0, 4, 8, 12, 16, 20) appears.
v5 and v6 re-measure to 97.9 + 10.459 and 43.7 + 9.152, reproducing FINDINGS 30
exactly, so the harness has not drifted underneath the new variant.
| clocks per 4x4 block, run of L blocks | L=1 | L=2 | L=3 | L=4 | L=8 | L=64 |
|---|---:|---:|---:|---:|---:|---:|
| v6 as built | 1053 | 527 | 351 | 263 | 241 | 154 |
| **v7** | **424** | **292** | **248** | **226** | **183** | **151** |
| DMAC chain (datasheet) | 288 | 216 | 192 | 180 | 162 | 146 |
**Break-even against all-V1 (299.9) moves from L=4 to L=2 blocks.** 39.4
predicted L=3; the measurement is better than the derivation.
### 40.2 The scoring, and a derivation that was right for the wrong reasons
Rescoring the same `scsi` window with `14_dmac_chain.py`, the same additive
model, the same mode maps:
| | frames over budget |
|---|---:|
| today | 84/120 |
| v6 span as built | 55/120 |
| **v7, MEASURED** | **18/120** |
| DMAC chain (datasheet) | 12/120 |
**18/120, exactly the derived figure.** That agreement is a coincidence of two
cancelling errors, and it is worth writing down because a match this clean would
otherwise be read as confirmation:
- 39.4 assumed a **2-register `movem` tail**, derived at 56 clocks per 4 pixels
= 14.0 clocks/pixel. The real tail costs **9.978** -- 29% cheaper.
- 39.4 assumed the second entry point costs **nothing per span**. It costs
**22.3 clocks** (66.0 against v6's 43.7), because it is a second
`move.w (a0)+,d0` and a second `jmp`.
The per-pixel win and the per-span loss are within a frame of each other over
this window. **The conclusion survives; the reasoning behind it did not.**
### 40.3 The instruction the derivation should have picked
A 2-register `movem` is the obvious "smaller unit of the same thing", and it is
the wrong instruction. Per 4 pixels:
| tail unit | bus cycles | clocks | per pixel |
|---|---:|---:|---:|
| `movem.l (a0)+,d0-d1` + `movem.l d0-d1,(a2)` + `lea` | 14 | 56 | 14.0 |
| **2 x `move.l (a0)+,(a2)+`** | **10** | **40** | **~10.0** |
`movem` pays two instruction words and a `lea` to move what two of the plainest
instructions on the machine move with post-increment on both sides. v7's fine
unit is therefore **one `move.l (a0)+,(a2)+` = 2 pixels**, which also makes the
padding quantum **2** rather than 4 -- and a span is a run of 4x4 blocks, so its
length is always a multiple of 4 and **the padding is exactly zero**. 39.4's
"caps the padding at 3 pixels" was pessimistic by three pixels.
The derived bus model predicts the measurement well once the right instruction
is in it: 5 bus cycles = 20 clocks per 2 pixels = 10.0/pixel against 9.978
measured, and 54 bus cycles = 216 clocks per 24 against 9.143*24 = 219.4.
### 40.4 Where the fine displacement lives, and why it is not in the record
Two chains need two entry points, and the second one has to survive the coarse
copy. Holding it in a register would cost a payload register -- v6's whole
reason for a 24-pixel unit is that it has exactly 12 free (`d0-d6/a1/a3-a6`).
So it is not in the span record. **It is in the stream**, after the coarse
pixels and before the fine ones. The coarse chain falls out into
`move.w (a0)+,d0 / jmp v7fh(pc,d0.w)`, and at that instant `d0` is dead payload
and `a0` is pointing exactly at it. The decoder holds nothing extra across the
copy and keeps all 12 registers.
The record is still `{u32 absolute GVRAM address, u16 coarse displacement}`;
the container costs **2 more bytes per span**, which `14_dmac_chain.py` charges.
### 40.5 The verdict, now measured rather than argued
**FINDINGS 39.5 stands: fix the quantum in software, drop the DMAC.** v7 takes
back **37 of the 43 frames** the DMAC chain would, using an instruction sequence
that needs no reserved channel, no two-region container, and no transfer timing
that neither emulator on this box can verify. 39.1 still holds if that ever
changes: the chain array and the span table are the same six bytes.
### 40.6 The 13-minute run that measured nothing
`span.sh` ran for 13 minutes producing an empty log and zero snapshots, and the
same MAME command with a shorter `-seconds_to_run` completed the identical work
in 30 seconds. The cause is still not identified. What matters is that **the
run was unobservable in both directions**: MAME's stdout did not reach the log
until exit even under `stdbuf -oL`, and the snapshots -- the one artefact that
would have shown progress -- may themselves only land at exit.
So the bisection that resolved it did not chase the hang. It **shrank the
stimulus** instead: `tmp/spans_meta.lua` carries byte offsets into a blob that
`prep_spans.py` writes once, so deleting config lines from the metadata runs any
subset in seconds against the same unmodified stream file. v5 alone, v7 alone
and the full set at a shorter run all completed; the wedge never reproduced.
This is the fourth instance of the pattern FINDINGS 34.1 named, and it is the
first where **the instrument was unobservable but the thing being measured was
fine**. `span.sh` now runs at `-seconds_to_run 200`, measured at 30 s wall for
all 36 configs, and asserts the snapshot count against the number of configs in
the generated metadata rather than a literal 23 -- so adding a config can no
longer silently weaken the pixel-exactness gate.
+112 -4
View File
@@ -1,7 +1,109 @@
# Status & next-session handoff — end of session 10 (2026-08-23)
# Status & next-session handoff — end of session 11 (2026-08-23)
## Where this stands
Session 11 measured the one item session 10 left at the top of the list, and it
paid: **`blit.s` v7, the literal span with a fine tail, is MEASURED and takes
the `scsi` window from 84/120 frames over budget to 18/120.** FINDINGS 40.
```
v7: cycles = 66.0 per span + 9.143 per COARSE pixel + 9.978 per FINE pixel
13 span lengths, all fitted to within 0.2%, all pixel-exact
```
| | frames over budget, 120-frame `scsi` window |
|---|---:|
| today (no spans) | 84/120 |
| v6 span as built | 55/120 |
| **v7, measured** | **18/120** |
| DMAC chain (datasheet) | 12/120 |
**The DMAC stays dropped, and now on a measurement rather than an argument.**
v7 takes back 37 of the 43 frames the DMAC chain would, with no reserved
channel, no two-region container, and no transfer timing neither emulator here
can verify. FINDINGS 39.1 still holds if that ever changes: a chain array entry
and a v6/v7 span record are the same six bytes.
**Break-even against all-V1 moves from L=4 blocks to L=2.** 39.4 predicted L=3.
**18/120 is exactly what 39.4 derived, and that is a coincidence of two
cancelling errors** — worth knowing before the next derived figure gets trusted
for landing on its measurement. 39.4 assumed a 2-register `movem` tail at 14.0
clocks/pixel (the real tail is 9.978, 29% cheaper) and assumed the second chain
entry costs nothing per span (it costs 22.3 clocks). The two nearly cancel over
this window. FINDINGS 40.2.
**The tail instruction the derivation should have picked is `move.l (a0)+,(a2)+`.**
A 2-register `movem` pays two instruction words and a `lea` to move what two
post-incrementing `move.l`s move: 14 bus cycles against 10 for the same 4
pixels. Taking the plain instruction also makes the padding quantum **2 pixels**
instead of 4 — and a span is a run of 4x4 blocks, so **its padding is exactly
zero**. FINDINGS 40.3.
**The fine displacement lives in the STREAM, not in the span record**, after the
coarse pixels and before the fine ones: the coarse chain falls out into
`move.w (a0)+,d0 / jmp`, where `d0` is dead payload and `a0` already points at
it. That is what lets v7 keep all 12 payload registers, which is the entire
reason v6's unit is 24 pixels. The container costs 2 more bytes a span.
FINDINGS 40.4.
**One process note.** `span.sh` ran 13 minutes producing an empty log and zero
snapshots; the same command with a shorter `-seconds_to_run` did the identical
work in 30 s, and the wedge never reproduced. The cause is unidentified. What
resolved it was not chasing the hang but **shrinking the stimulus**:
`tmp/spans_meta.lua` holds byte offsets into a blob `prep_spans.py` writes once,
so deleting lines from the metadata runs any subset in seconds against the same
stream file. Keep that trick. `span.sh` is now `-seconds_to_run 200` (30 s wall
for all 36 configs) and takes its expected snapshot count from the metadata
instead of a literal 23. FINDINGS 40.6.
Green light: `./tools/bench/check.sh` **ALL GREEN** at the end of this session.
## NEXT SESSION, in order
0. **Green light first.** `./tools/bench/check.sh` (~5 min, Blu-ray mounted).
Verified green at end of session 11.
1. **Build v7 into `src/player/decode.s`.** This is now the largest thing
standing between the measured decoder and the budget: 84/120 to 18/120, the
format is fully specified (FINDINGS 30.2, costs 40.1), the executor is
written and pixel-exact in `blit.s`, and the encoder side is
`prep_spans.py`'s v7 emitter. The container is `{u32 absolute GVRAM address,
u16 coarse displacement}` per span plus one `u16` fine displacement carried
mid-stream — see FINDINGS 40.4 before changing that layout, the register
pressure is the reason for it.
2. **Then re-run `14_dmac_chain.py` and `13_cpu_ratectl.py` against a container
the encoder actually emits with spans in it.** Every span figure so far is
scored against mode maps chosen without spans available, which FINDINGS 39.3
flags as a lower bound on what a span-aware encoder would find.
3. **Make sure the player actually gets DMA** — unchanged from session 10, and
still not an optimisation. Benchmark `x68000 -exp1 cz6bs1`, **never
`x68ksupr`**; MAME's internal SCSI has no DMA glue (`// TODO: duplicate DMA
glue from CZ-6BS1`) and would measure a PIO fallback the real machine does
not have.
4. **Re-decide the framerate.** 10 fps absorbs the DMA steal on current
estimates. Still the user's call, and now cheaper to defer: v7 buys back
enough of the budget that 12 fps is no longer obviously out of reach.
5. **Re-run the ring-buffer simulation at the surviving rate** and confirm the
488 KB/s figure's provenance (FINDINGS 29.5/30.7, still open).
**Do not start by hand-optimising `decode.s`.** Unchanged and still true: the
hand-derived timings agree with the measurements to 0.5% on V1 and 1% on RAW
(FINDINGS 28.4), 34 confirms the model on a second container, and the cycles to
be won are in the budget, not the loop.
**Always `stdbuf -oL` a MAME job that prints progress — and do not trust it.**
Session 11 added the case where even that is not enough (40.6). If a run is not
producing observable output, shrink the stimulus rather than waiting.
---
## What session 10 settled
Session 10 cross-checked the whole cycle model against a second emulator, then
found that the model was denominated in the wrong currency.
@@ -77,10 +179,16 @@ at **3.3%**, and it runs against us. Two incidental results worth keeping: MAME
rather than two tables; and FINDINGS 28.8's "V4 costs more than RAW" reproduces
independently. FINDINGS 37.
Everything below this line is from session 9 and still stands unless a session-10
section above says otherwise.
---
## NEXT SESSION, in order
## What session 10 put on the list
Kept for the reasoning, not the priorities: items 0 and 1 are done (FINDINGS 40)
and the rest are carried forward, reordered, in the session-11 list at the top.
Everything below this point is from session 9 or earlier and still stands unless
a later section above says otherwise.
### session 10's list
0. **Green light first.** `./tools/bench/check.sh` (~5 min, Blu-ray mounted).
Verified green at end of session 9. The gate container is now