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.