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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user