diff --git a/README.md b/README.md index 5d1cff9..527dc06 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index cc66bb6..5d5048f 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -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. diff --git a/docs/STATUS.md b/docs/STATUS.md index e39dd2e..b598034 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 diff --git a/tools/analysis/14_dmac_chain.py b/tools/analysis/14_dmac_chain.py index 4ca9a15..604eaa9 100644 --- a/tools/analysis/14_dmac_chain.py +++ b/tools/analysis/14_dmac_chain.py @@ -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") diff --git a/tools/analysis/buscost.py b/tools/analysis/buscost.py index 2e59c89..fe10225 100644 --- a/tools/analysis/buscost.py +++ b/tools/analysis/buscost.py @@ -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 diff --git a/tools/bench/blit.s b/tools/bench/blit.s index 3655652..4477561 100644 --- a/tools/bench/blit.s +++ b/tools/bench/blit.s @@ -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 diff --git a/tools/bench/prep_spans.py b/tools/bench/prep_spans.py index cc4e6ca..696feac 100644 --- a/tools/bench/prep_spans.py +++ b/tools/bench/prep_spans.py @@ -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") diff --git a/tools/bench/span.lua b/tools/bench/span.lua index 4929d90..7dd7cb0 100644 --- a/tools/bench/span.lua +++ b/tools/bench/span.lua @@ -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 diff --git a/tools/bench/span.sh b/tools/bench/span.sh index 9408633..7635b9e 100755 --- a/tools/bench/span.sh +++ b/tools/bench/span.sh @@ -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"