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
+16 -5
View File
@@ -11,6 +11,10 @@ decoder occupies 86.7% of it once instruction prefetch is counted, and 52 of the
53 frames that miss the 12fps budget miss it on the bus, not the CPU
(FINDINGS 38). Read that before optimising anything for cycles.
The largest measured win on the table is the **literal span with a fine tail**
(`blit.s` v7): it takes the worst `scsi` window from 84/120 frames over budget
to 18/120, and `src/player/decode.s` does not implement it yet (FINDINGS 40).
**Green-light check:** `./tools/bench/check.sh` (~3 min, needs the Blu-ray
mounted) re-runs both display regression tests, the rate-control drift test, the
display-path coherency counterexample and a 120-frame 68000 decode, then prints
@@ -47,8 +51,10 @@ tools/analysis/ measurement scripts, numbered in the order they were written
scene cut still fits at 12fps; 13 measures what fitting the
CPU budget costs in dB (FINDINGS 31) and caches H.build so the
search loop is seconds, not minutes.
14 prices the HD63450 array-chain against the v6 span
(FINDINGS 39) and prints the sensitivity that decides it;
14 prices the HD63450 array-chain against the v6 and v7
spans (FINDINGS 39/40) and prints the sensitivity that decides
it -- v7 is measured, and takes 37 of the 43 frames the DMAC
would, so the DMAC stays dropped;
15 measures how much of the 68000's LOCAL bus the decoder
occupies (FINDINGS 38) and exits non-zero if its derived
model stops matching the harness's measurement.
@@ -58,9 +64,14 @@ tools/bench/ MAME Lua injection harness + 68000 benchmark sources.
`blit.s`/`blit.lua` time the full-frame GVRAM blit on the
68000 itself (FINDINGS 24) — not part of check.sh, because
wall timings would make the green-light check host-sensitive.
`span.sh` (prep_spans.py + span.lua + blit.s v5/v6) measures
the literal-span mode the same way (FINDINGS 30, ~25 s); it
also asserts all 23 timing configs drew a pixel-exact frame.
`span.sh` (prep_spans.py + span.lua + blit.s v5/v6/v7)
measures the literal-span mode the same way (FINDINGS 30 and
40, ~30 s); it also asserts that every one of its 36 timing
configs drew a pixel-exact frame, the count taken from the
generated metadata so a new config cannot weaken the gate.
v7 is v6 with a second, 2-pixel chain for the span tail:
66.0 cycles/span + 9.143 per coarse pixel + 9.978 per fine
pixel, MEASURED, which is the win FINDINGS 39.4 predicted.
`crtc_mode.lua` is the single source of truth for CRTC R00-R08
and R20 — do not write CRTC values anywhere else.
`prep_dlx.py`/`decode.lua`/`verify_decode.py` load, time and
+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
+15 -10
View File
@@ -41,8 +41,11 @@ CPU + DMA, additive. Session 10's first pass used max(CPU, bus) and got 53/120
where the additive model gives 84/120; FINDINGS 35's flat debit was right.
So the only material difference left is v6's 24-pixel padding quantum -- and
that is a property of v6's unrolled chain, not of the CPU. The `v6 fine tail`
column prices fixing it in software instead.
that is a property of v6's unrolled chain, not of the CPU. The `v7 fine tail`
column prices fixing it in software instead, and as of session 11 that column
is MEASURED on the 68000 (blit.s v7, tools/bench/span.sh, FINDINGS 40) rather
than derived: 66.0 clocks per span + 9.143 per coarse pixel + 9.978 per fine
pixel, with a 2-pixel quantum that a run of 4x4 blocks pads to exactly.
"""
import sys, os, argparse
sys.path.insert(0, "tools/encoder")
@@ -100,8 +103,8 @@ def span_cost(design, L):
if design == "v6":
px = B.pad24(4 * L)
return 4 * px, 4 * (B.V6_SPAN_CYC + px * B.V6_PX_CYC)
if design == "v6fine":
px, c = B.v6_fine(4 * L)
if design == "v7":
px, c = B.v7_span(4 * L)
return 4 * px, 4 * c
px = 4 * L
return 4 * px, 4 * (B.DMA_CHAIN_CLK + px * a.dma_px_clk)
@@ -127,7 +130,9 @@ def score(design):
cur_b = sum(BLK_B[int(b)] for b in m[by][i:j])
px, sc = span_cost(design, L)
sc += L * C_SKIP_MIXED # the dispatch still happens
span_b = 4 * SPAN_HDR + px * SPAN_BYTES_PX
# v7 carries a second u16 (the fine displacement) per span.
hdr = B.V7_SPAN_HDR if design == "v7" else SPAN_HDR
span_b = 4 * hdr + px * SPAN_BYTES_PX
if sc < cur_c:
cand.append((cur_c - sc, span_b - cur_b, by, i, j, sc, L))
cand.sort(key=lambda s: -(s[0] / max(s[1], 1)))
@@ -154,7 +159,7 @@ def score(design):
DESIGNS = [("today", "none"), ("v6 span", "v6"),
("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")]
("v7 fine tail", "v7"), ("DMAC chain", "dmac")]
res = {n: score(k) for n, k in DESIGNS}
print(f"{a.container}: {d.nframes} frames, {d.nb} blocks, {a.fps:g} fps")
@@ -188,11 +193,11 @@ print(f"\n ADDITIVE: frame = CPU + span painting + disk DMA. The 68000 has no"
# What is left of the case, isolated.
v6m = int((res["v6 span"][0] > FRAME_CYC).sum())
finem = int((res["v6 fine tail"][0] > FRAME_CYC).sum())
finem = int((res["v7 fine tail"][0] > FRAME_CYC).sum())
dmam = int((res["DMAC chain"][0] > FRAME_CYC).sum())
print(f"\nWHAT THE DMAC ACTUALLY BUYS, decomposed")
print(f" v6 as built {v6m}/{d.nframes} frames over")
print(f" v6 with a finer chain tail (software) {finem}/{d.nframes}")
print(f" v7, a finer chain tail (MEASURED) {finem}/{d.nframes}")
print(f" DMAC chain {dmam}/{d.nframes}")
print(f" -> of the gap between v6 and the DMAC, "
f"{100*(v6m-finem)/max(v6m-dmam,1):.0f}% is the 24-pixel padding")
@@ -206,9 +211,9 @@ print(f" in software. The rest is 1.7% a pixel and 7.7 clocks a span.")
print(f"\nbreak-even against all-V1 ({C_V1:.1f} cycles/block), clocks per block")
print(f" {'L':<16}" + "".join(f"{L:>8}" for L in (1, 2, 3, 4, 8, 16, 64)))
for nm, dz in (("v6 as built", "v6"), ("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")):
for nm, dz in (("v6 as built", "v6"), ("v7 fine tail", "v7"), ("DMAC chain", "dmac")):
print(f" {nm:<16}" + "".join(f"{span_cost(dz, L)[1]/L:>8.0f}"
for L in (1, 2, 3, 4, 8, 16, 64)))
for nm, dz in (("v6 as built", "v6"), ("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")):
for nm, dz in (("v6 as built", "v6"), ("v7 fine tail", "v7"), ("DMAC chain", "dmac")):
brk = next((L for L in range(1, 65) if span_cost(dz, L)[1] < L * C_V1), None)
print(f" {nm:<16} beats all-V1 from L={brk} blocks up")
+48 -14
View File
@@ -110,20 +110,54 @@ def block_bus(mode_map, spanned=None):
return pref, data
# --- v6 with a finer tail (PROPOSAL, unmeasured -- Claude's, session 10) ----
# --- v7: v6 with a finer tail (MEASURED, session 11, FINDINGS 40) ----------
# v6 pads every span up to 24 pixels because its unrolled chain is built from
# 12-register movem units. Adding a second, finer chain of 2-register units
# (4 pixels) for the tail caps the padding at 3 pixels instead of 23, for the
# price of some more unrolled code and nothing per span.
# A 4-pixel unit: movem.l (a0)+,2 = 2w instr + 4 reads; movem.l 2,(a2) = 2w +
# 4 writes; lea = 2w. 14 bus cycles for 4 pixels = 56 clocks, against a full
# unit's 24 x 9.152 = 220 for 24. Dearer per pixel, paid at most once a span.
V6_TAIL_PX, V6_TAIL_CLK = 4, 56
# 12-register movem units, and FINDINGS 39.3 attributed 86% of the DMAC array
# chain's advantage over v6 to exactly that padding. v7 keeps the coarse chain
# and appends a second chain whose unit is one `move.l (a0)+,(a2)+` -- 2 pixels,
# so the quantum is 2 and a run of 4x4 blocks pads to NOTHING.
#
# Session 10 proposed a 2-REGISTER MOVEM tail (4 pixels, derived at 56 clocks)
# and that would have been the wrong instruction: movem.l (a0)+,d0-d1 plus
# movem.l d0-d1,(a2) plus the lea is 14 bus cycles for 4 pixels, where two plain
# move.l are 10. The plainest instruction on the machine wins the tail.
#
# The second entry point needs a second dispatch, and the fine displacement is
# carried MID-STREAM (after the coarse pixels, before the fine ones) rather than
# in the span record, so the decoder holds nothing extra across the copy and
# keeps all 12 payload registers. Costed as 2 more bytes per span.
#
# MEASURED by tools/bench/span.sh (blit.s v7, 13 span lengths, every config
# pixel-exact): cycles = 66.0/span + 9.143/coarse pixel + 9.978/fine pixel,
# fitting all 13 to within 0.2%.
V7_SPAN_CYC = 66.0 # MEASURED, FINDINGS 40
V7_CPX_CYC = 9.143 # MEASURED, FINDINGS 40 (24-pixel coarse unit)
V7_FPX_CYC = 9.978 # MEASURED, FINDINGS 40 (2-pixel fine unit)
V7_FINE_PX = 2
# Bus: per span v6's 9 plus a second {move.w (a0)+,d0 ; jmp} = 2 + 2.
# Per fine unit: move.l (a0)+,(a2)+ = 1 instruction word + 2 reads + 2 writes.
V7_SPAN_BUS = 13
V7_FINE_BUS = 5
V7_SPAN_HDR = 8 # {u32 address, u16 coarse disp} + u16 fine disp
def v6_fine(npix):
"""(pixels carried, CPU clocks) for a span with the finer tail."""
k, r = divmod(npix, V6_UNIT_PX)
t = -(-r // V6_TAIL_PX)
return (k * V6_UNIT_PX + t * V6_TAIL_PX,
V6_SPAN_CYC + k * V6_UNIT_PX * V6_PX_CYC + t * V6_TAIL_CLK)
def pad2(npix):
return -(-npix // V7_FINE_PX) * V7_FINE_PX
def v7_span(npix):
"""(pixels carried, CPU clocks) for a v7 span of npix pixels."""
k, r = divmod(pad2(npix), V6_UNIT_PX)
return (k * V6_UNIT_PX + r,
V7_SPAN_CYC + k * V6_UNIT_PX * V7_CPX_CYC + r * V7_FPX_CYC)
def v7_span_bus(npix):
"""Bus CYCLES a v7 span occupies -- instruction words plus data accesses."""
k, r = divmod(pad2(npix), V6_UNIT_PX)
return V7_SPAN_BUS + k * V6_UNIT_BUS + (r // V7_FINE_PX) * V7_FINE_BUS
def v6_span_bus(npix):
k = pad24(npix) // V6_UNIT_PX
return V6_SPAN_BUS + k * V6_UNIT_BUS
+90
View File
@@ -69,6 +69,27 @@
; And with row and remainder handling gone, 12 registers are free again
; (d0-d6/a1/a3-a6), which is why the unit is 24 pixels and not V5's 16.
;
; V7 v6 with a SECOND, finer chain for the tail (FINDINGS 39.4). v6 pays for
; its 24-pixel quantum in padding: an average span wastes ~11 pixels, and
; FINDINGS 39.3 attributes 86% of the DMAC array-chain's advantage over v6
; to exactly that. V7 keeps the 24-pixel coarse chain and appends a chain
; of 2-pixel units, so a span is 24*c + 2*f pixels and the padding is at
; most one pixel -- ZERO for the real case, where a span is a run of 4x4
; blocks and its length is a multiple of 4.
;
; The fine unit is `move.l (a0)+,(a2)+` (20 cycles, 2 pixels), NOT a
; 2-register movem: movem.l (a0)+,d0-d1 plus movem.l d0-d1,(a2) plus the
; lea is 52+8 cycles for 4 pixels, so the obvious "smaller movem" tail is
; 50% dearer per pixel than the plainest instruction on the machine.
;
; The second entry point costs a second dispatch, and the trick that pays
; for it is that the fine displacement is NOT in the span record: it sits
; in the STREAM, after the coarse pixels and before the fine ones. The
; coarse chain falls out into `move.w (a0)+,d0 / jmp`, by which point d0
; is dead payload and a0 is pointing exactly at it. So v7 holds nothing
; extra across the copy and keeps all 12 payload registers -- a record is
; still {u32 address, u16 displacement}, with one more u16 mid-span.
;
; 12 registers per movem burst (d0-d7/a2-a5 = 48 bytes) is the maximum
; available: a0=src, a1=dst, a6=end sentinel. The row counter lives in the
; a1-vs-a6 compare rather than a d-register for exactly this reason.
@@ -88,6 +109,10 @@ DSTE = $C38000 ; GVRAM + 224*1024 (one past last)
ROWS = 192 ; picture rows a V5 stream describes
V6UNIT = 12 ; bytes of code per V6 chain unit
V6MAX = 11 ; chain units = 11*24 = 264 pixels >= one row
V7CU = 12 ; bytes of code per V7 COARSE unit (24 px)
V7CN = 11 ; coarse units: 11*24 = 264 px >= one row
V7FU = 2 ; bytes of code per V7 FINE unit (2 px)
V7FN = 11 ; fine units: 11*2 = 22 px > one coarse unit
org $10000
start:
@@ -103,6 +128,8 @@ start:
beq v5
cmp.l #6,d0
beq v6
cmp.l #7,d0
beq v7
bra v3
; ---------------------------------------------------------------- V1
@@ -282,5 +309,68 @@ v6ch:
bne v6
bra done
; ---------------------------------------------------------------- V7
; a0 stream, a2 destination, d7 spans remaining; everything else is payload.
; Stream per span: u32 dest, u16 coarse disp, c*48 B pixels,
; u16 fine disp, f*4 B pixels.
v7: move.l SPTR.l,a0
move.w (a0)+,d7 ; total spans in the frame
subq.w #1,d7
v7span: move.l (a0)+,a2 ; absolute GVRAM destination
move.w (a0)+,d0 ; (V7CN - coarse) * V7CU
jmp v7ch(pc,d0.w)
v7ch:
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
movem.l (a0)+,d0-d6/a1/a3-a6
movem.l d0-d6/a1/a3-a6,(a2)
lea 48(a2),a2
v7cx: move.w (a0)+,d0 ; (V7FN - fine) * V7FU, from mid-stream
jmp v7fh(pc,d0.w)
v7fh:
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
move.l (a0)+,(a2)+
dbra d7,v7span
subq.l #1,ITER.l
bne v7
bra done
done: move.l #$FF,FLAG.l ; timer stops here
halt: bra.s halt
+55 -3
View File
@@ -31,6 +31,20 @@ v6 -- the same spans with that arithmetic moved here, where it is free:
line stride is 1024 bytes and only the first 512 are displayed. The jump
displacement selects an entry point into the decoder's unrolled copy chain.
v7 -- v6 plus a second, FINER chain for the tail (FINDINGS 39.4). Padding to
v6's 24-pixel quantum wastes ~11 pixels on an average span, and FINDINGS
39.3 attributes 86% of the DMAC array-chain's advantage over v6 to it. A
v7 span is 24*c + 2*f pixels, so the quantum is 2 and a run of 4x4 blocks
(always a multiple of 4 pixels) pads to NOTHING:
u16 nspans
nspans * { u32 absolute GVRAM address, u16 coarse displacement,
c * 48 bytes of pixels,
u16 fine displacement, f * 4 bytes of pixels }
The fine displacement is in the STREAM rather than the record because that
is what lets the decoder keep all 12 payload registers: the coarse chain
falls out into a `move.w (a0)+,d0 / jmp` with d0 dead and a0 pointing at
it. Costed here as an 8-byte record, since it is 2 more bytes a span.
Pixels are word-expanded with the palette index in the low byte; the high byte
is whatever we put there because gvram_w masks it off (x68k_crtc.cpp:501).
"""
@@ -58,8 +72,18 @@ CONFIGS = [(4, 0), (8, 0), (12, 0), (16, 0), (16, 1), (20, 0), (24, 0),
# v6 geometry, and it must match blit.s: 12 registers per movem = 48 bytes =
# 24 pixels per chain unit, 11 units in the chain.
UNITPX, UNITSZ, UNITS = 24, 12, 11
# v7 geometry, and it must match blit.s: coarse unit as v6, fine unit is one
# `move.l (a0)+,(a2)+` = 2 bytes of code = 2 pixels, 11 of them (22 px > 24).
FINEPX, FINESZ, FINES = 2, 2, 11
GVRAM, YOFF, STRIDE = 0xC00000, 32, 1024
# v7 span lengths, in pixels. Multiples of 4 (a real span is a run of 4x4
# blocks), chosen so the fine remainder P mod 24 takes every value a real span
# can: 0, 4, 8, 12, 16, 20. 4/8/12/16/20 are pure-fine, 24/48/72/120/240 are
# pure-coarse, the rest mix -- which is what makes the three-term fit
# cycles = A*spans + Bc*coarse_px + Bf*fine_px identifiable.
V7CONFIGS = [4, 8, 12, 16, 20, 24, 28, 44, 48, 72, 100, 120, 256]
blob, metas = bytearray(), []
for P, x0 in CONFIGS:
off = len(blob)
@@ -78,7 +102,8 @@ for P, x0 in CONFIGS:
blob += idx[y, x:x+n].astype(">u2").tobytes()
nspans += 1; npix += n
metas.append(dict(name=f"{P}{'u' if x0 else ''}", p=P, x0=x0, off=off,
len=len(blob)-off, nspans=nspans, npix=npix, var=5))
len=len(blob)-off, nspans=nspans, npix=npix,
cpx=npix, fpx=0, var=5))
# v6: one config per chain depth, so the fit sees spans from 24 to 264 pixels.
for units in range(1, UNITS+1):
@@ -100,7 +125,33 @@ for units in range(1, UNITS+1):
blob += px.astype(">u2").tobytes()
nspans += 1; npix += P
metas.append(dict(name=f"{P}", p=P, x0=0, off=off, len=len(blob)-off,
nspans=nspans, npix=npix, var=6))
nspans=nspans, npix=npix, cpx=npix, fpx=0, var=6))
# v7: same tiling, but the span is cut at a 2-pixel quantum instead of 24.
for P in V7CONFIGS:
units, fine = divmod(P, UNITPX)
assert fine % FINEPX == 0 and fine // FINEPX <= FINES, P
assert units <= UNITS, P
off = len(blob)
nspans = npix = 0
rows = []
for y in range(H):
x = 0
while x < W:
rows.append((y, x)); x += P
blob += struct.pack(">H", len(rows))
for y, x in rows:
assert x + P <= STRIDE // 2, (P, x) # the overrun must stay on the line
blob += struct.pack(">IH", GVRAM + (YOFF+y)*STRIDE + x*2,
(UNITS-units)*UNITSZ)
px = np.concatenate([idx[y, x:x+P], np.zeros(max(0, x+P-W), np.uint8)])
blob += px[:units*UNITPX].astype(">u2").tobytes()
blob += struct.pack(">H", (FINES - fine//FINEPX)*FINESZ)
blob += px[units*UNITPX:].astype(">u2").tobytes()
nspans += 1; npix += P
metas.append(dict(name=f"{P}", p=P, x0=0, off=off, len=len(blob)-off,
nspans=nspans, npix=npix,
cpx=nspans*units*UNITPX, fpx=nspans*fine, var=7))
open(OUT, "wb").write(blob)
with open(META, "w") as f:
@@ -108,7 +159,8 @@ with open(META, "w") as f:
f.write(f" W={W}, H={H}, total={len(blob)},\n configs = {{\n")
for m in metas:
f.write(" {{var={var}, name=\"{name}\", p={p}, x0={x0}, off={off},"
" len={len}, nspans={nspans}, npix={npix}}},\n".format(**m))
" len={len}, nspans={nspans}, npix={npix}, cpx={cpx},"
" fpx={fpx}}},\n".format(**m))
f.write(" },\n}\n")
print(f"{SRC} {W}x{H} -> {OUT} {len(blob)} B, {len(metas)} configs")
+57 -11
View File
@@ -108,7 +108,8 @@ local function launch(cfg)
push(STREAM, blob, cfg.off+1, cfg.len)
clear_picture()
-- ~4 emulated seconds per config: 1/55.46 s granularity costs under 0.5%.
local est = cfg.nspans*(cfg.var == 6 and 50 or 60) + cfg.npix*10
local est = cfg.nspans*(cfg.var == 5 and 60 or (cfg.var == 6 and 50 or 70))
+ cfg.npix*10
cfg.iter = math.max(4, math.floor(4*CPUHZ/est))
SP:write_u32(FLAG, 0)
SP:write_u32(VAR, cfg.var)
@@ -143,6 +144,34 @@ local function fit(rs)
return (sy*pp - py*sp)/det, (ss*py - sp*sy)/det
end
-- v7 has two per-pixel costs -- the 24-pixel coarse chain and the 2-pixel fine
-- chain -- so its fit is cycles = A*spans + Bc*coarse_px + Bf*fine_px, solved
-- by plain Gaussian elimination on the 3x3 normal equations. prep_spans.py
-- picks span lengths so every fine remainder a real span can have (0,4,..,20)
-- appears, which is what makes the three terms separable.
local function fit3(rs)
local M3 = {{0,0,0,0},{0,0,0,0},{0,0,0,0}}
for _,r in ipairs(rs) do
local x = {r.cfg.nspans, r.cfg.cpx, r.cfg.fpx}
for i=1,3 do
for j=1,3 do M3[i][j] = M3[i][j] + x[i]*x[j] end
M3[i][4] = M3[i][4] + x[i]*r.cyc
end
end
for c=1,3 do
local piv = c
for r=c+1,3 do if math.abs(M3[r][c]) > math.abs(M3[piv][c]) then piv=r end end
M3[c], M3[piv] = M3[piv], M3[c]
for r=1,3 do
if r ~= c then
local f = M3[r][c]/M3[c][c]
for k=c,4 do M3[r][k] = M3[r][k] - f*M3[c][k] end
end
end
end
return M3[1][4]/M3[1][1], M3[2][4]/M3[2][2], M3[3][4]/M3[3][3]
end
local step, st, t0 = 0, "boot", nil
SUB = emu.add_machine_frame_notifier(function()
@@ -159,7 +188,7 @@ SUB = emu.add_machine_frame_notifier(function()
report(SPEC.configs[step], t - (t0 or t))
st = "snap"; return
end
if t > 300 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end
if t > 900 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end
return
end
if st == "snap" then
@@ -174,7 +203,7 @@ SUB = emu.add_machine_frame_notifier(function()
end
if st == "finish" then
P("---- measured (instruction cycles only; real GVRAM adds wait states) ----")
for _,v in ipairs{5,6} do
for _,v in ipairs{5,6,7} do
local sub = {}
for _,r in ipairs(results) do if r.cfg.var == v then sub[#sub+1] = r end end
-- v5's fit is over its BURSTING configs only (span length a multiple of
@@ -183,13 +212,22 @@ SUB = emu.add_machine_frame_notifier(function()
-- instead, which is where the remainder shows up as error.
local fitset = {}
for _,r in ipairs(sub) do
if v == 6 or r.cfg.p % 16 == 0 then fitset[#fitset+1] = r end
if v ~= 5 or r.cfg.p % 16 == 0 then fitset[#fitset+1] = r end
end
local A, Bp, Bf
if v == 7 then
A, Bp, Bf = fit3(fitset)
P(string.format("-- v7: cycles = %.1f per span + %.3f per COARSE pixel"
.." + %.3f per FINE pixel (fitted on %d of %d configs)",
A, Bp, Bf, #fitset, #sub))
else
A, Bp = fit(fitset)
Bf = Bp
P(string.format("-- v%d: cycles = %.1f per span + %.3f per pixel"
.." (fitted on %d of %d configs)", v, A, Bp, #fitset, #sub))
end
local A, Bp = fit(fitset)
P(string.format("-- v%d: cycles = %.1f per span + %.3f per pixel"
.." (fitted on %d of %d configs)", v, A, Bp, #fitset, #sub))
for _,r in ipairs(sub) do
local model = A*r.cfg.nspans + Bp*r.cfg.npix
local model = A*r.cfg.nspans + Bp*r.cfg.cpx + Bf*r.cfg.fpx
P(string.format(" span %4s px %8.0f cyc %5.2f cyc/px %6.1f cyc/span"
.." vs fit %+6.1f%%", r.cfg.name, r.cyc,
r.cyc/r.cfg.npix, r.cyc/r.cfg.nspans, 100*(model/r.cyc-1)))
@@ -199,9 +237,17 @@ SUB = emu.add_machine_frame_notifier(function()
-- v6 pads each to a whole 24-pixel chain unit.
local line = " -> cycles per 4x4 block in a run of L blocks: "
for _,L in ipairs{1,2,4,8,16,64} do
local px = 4*L
if v == 6 then px = math.ceil(px/24)*24 end
line = line..string.format("L=%d %.0f ", L, 4*(A + px*Bp)/L)
local px, cyc = 4*L, nil
if v == 6 then
px = math.ceil(px/24)*24
cyc = A + px*Bp
elseif v == 7 then
local c = math.floor(px/24)*24
cyc = A + c*Bp + (px-c)*Bf -- a multiple of 4 pads to nothing
else
cyc = A + px*Bp
end
line = line..string.format("L=%d %.0f ", L, 4*cyc/L)
end
P(line.."(V1 is 299.9)")
if v == 5 then
+9 -5
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Measure the cost of a row-linear literal span on the 68000 (FINDINGS 30).
# ~25 s. Run from the repo root. Needs tmp/frame256.bin (check.sh makes it).
# ~45 s. Run from the repo root. Needs tmp/frame256.bin (check.sh makes it).
#
# NOT part of check.sh, for the same reason blit.s is not: the output is a wall
# timing, so gating on it would make the green light host-sensitive. What IS
@@ -14,18 +14,22 @@ python3 tools/bench/prep_spans.py
tools/vasm/vasmm68k_mot -Fbin -o tmp/blit.bin tools/bench/blit.s > /dev/null
mkdir -p tmp/snap_span
rm -f tmp/snap_span/x68000/*.png
( cd tmp && SDL_VIDEODRIVER=dummy timeout -k 5 1800 mame x68000 -bios ipl10 \
( cd tmp && SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 1800 mame x68000 -bios ipl10 \
-ramsize 2M -video soft -window -sound none -nothrottle -plugins \
-autoboot_script ../tools/bench/span.lua \
-snapshot_directory ./snap_span -snapview native -seconds_to_run 150 \
-snapshot_directory ./snap_span -snapview native -seconds_to_run 200 \
> span.log 2>&1 )
grep -a "^\[SPAN\]" tmp/span.log
# One snapshot per config, and the expected count comes from the generated
# metadata rather than a literal: adding a config must not silently weaken the
# assertion that every one of them drew the picture.
want=$(grep -c '{var=' tmp/spans_meta.lua)
n=0
for f in tmp/snap_span/x68000/*.png; do
python3 tools/bench/verify_frame256.py "$f" > /dev/null || {
echo "FAIL: $f is not pixel-exact"; python3 tools/bench/verify_frame256.py "$f"; exit 1; }
n=$((n+1))
done
[ "$n" -eq 23 ] || { echo "FAIL: $n snapshots, expected 23"; exit 1; }
echo "OK $n/23 span configs drew a pixel-exact frame"
[ "$n" -eq "$want" ] || { echo "FAIL: $n snapshots, expected $want"; exit 1; }
echo "OK $n/$want span configs drew a pixel-exact frame"