Files
Dragon-s-Lair-X68k/tools/bench/blit.s
T
prosolis c520a89e14 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
2026-08-23 19:02:23 -07:00

377 lines
16 KiB
ArmAsm

; Full-frame GVRAM blit cost on a stock 68000 @ 10MHz.
;
; Answers: what fraction of a 12fps frame budget (833,333 cycles) does simply
; PUTTING a decoded 256x192 frame on screen cost, before any decoding?
;
; Geometry (tools/bench/crtc_mode.lua): 256-colour page, one pixel per WORD of
; CPU address space, 1024-byte line stride, picture in rows 32..223 of a
; 256-row page. So a row is 512 contiguous bytes of writes, then a 512-byte
; skip. 192 rows = 98,304 bytes of GVRAM write traffic per frame.
;
; Confirmed from MAME 0.277 x68k_crtc.cpp:501 (gvram_w, case 0x0100): a CPU
; write in 256-colour mode is masked to 0x00ff, so the HIGH byte of every word
; written is discarded by the hardware. V1 exploits this -- it never has to
; clear the odd bytes of its source.
;
; Three variants, selected by VAR, each looped ITER times:
; V1 movem.l blit from a word-expanded RAM frame (96KB). The realistic
; "decode to RAM, then blit" design. Reads 96KB, writes 96KB.
; V2 naive byte-source expansion (move.b / move.w per pixel). The obvious
; implementation, kept as the baseline V1 has to beat.
; V3 write-only floor: registers preloaded once, no source read at all.
; Nothing that puts this many pixels on screen can beat V3. The gap
; V1-V3 is the price of reading a source frame at all.
; V4 the SAME 96KB of writes, but issued in 4x4 BLOCK order instead of
; row-linear order. This is the access pattern a decoder that writes
; codewords straight into GVRAM actually has, and it is the number that
; picks the decoder architecture: compose-in-RAM-then-blit (V1) versus
; decode-direct-to-GVRAM (V4 scaled by the fraction of non-SKIP blocks).
; Each block is 4 rows of 8 bytes at a 1024-byte stride, so the
; destination displacements 0/1024/2048/3072 all fit a 16-bit offset and
; the block needs only one base pointer. V4 deliberately scrambles the
; picture (it reads a row-linear source in block order); it is a timing
; probe, which is why the correctness snapshot is taken after V1.
; V5 ROW-LINEAR LITERAL SPANS, the mode priced in FINDINGS 29 and never
; measured. Walks a stream of per-row span records
; row: u16 nspans, then nspans * { u16 x, u16 npix, npix*u16 pixels }
; for 192 rows, copying each span's word-expanded pixels straight from
; the stream buffer into GVRAM. Unlike V1-V4 the work per call is set by
; the STREAM, not by the code, so one variant measures every span length:
; tools/bench/prep_spans.py generates a stream per span length and
; tools/bench/span.lua times them and fits cycles = A*spans + B*pixels.
; The point of the measurement is A -- the per-span overhead FINDINGS 29
; guessed at 50 cycles -- and how much B degrades from V1's 9.08 when a
; span is too short to burst. Every config covers the whole frame, so
; V5 draws the SAME picture V1 does and can be verified, not just timed.
;
; Bursts are 8 registers (d0-d3/a3-a6 = 32 bytes = 16 pixels), not V1's
; 12: a0/a1/a2 and d4-d7 are all live across a span (stream, row base,
; destination, and three counters). The remainder is copied move.l at a
; time with a leading move.w when it is odd, so a 4-pixel span never
; reaches a movem at all -- which is exactly the case FINDINGS 29's
; full-row-width extrapolation flatters.
;
; V6 the SAME spans with the arithmetic moved into the encoder. V5 measures
; a decoder that is handed (x, npix) and has to work out how to copy it;
; most of its per-span cost is that working-out, and an encoder can do it
; once at build time instead of 12 times a second. V6's record is
; { u32 absolute GVRAM address, u16 jump displacement } -- no row
; structure, no counters, no remainder logic -- and the displacement
; jumps into an unrolled chain of 24-pixel copy units, so a span of any
; supported length is straight-line code with no loop at all.
; GVRAM sits at a fixed $C00000 on every X68000, so absolute destinations
; are a legitimate thing for an encoder to bake in.
;
; Two consequences of the format. Span lengths are multiples of 24
; pixels, and a span may overrun the 256 visible pixels of its row by up
; to 23 -- harmless, because the line stride is 1024 bytes and only the
; first 512 are displayed, so the overrun lands in the invisible half.
; 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.
; 512 = 10*48 + 32, hence ten 12-register bursts and one 8-register tail.
; Destination uses (d16,a1) displacement rather than post-increment because
; movem cannot post-increment a destination; the displacement costs 4 cycles
; per burst but saves an 8-cycle lea, so it is the cheaper of the two.
FLAG = $18000 ; 0 idle / 1 running / $FF done
VAR = $18004 ; variant selector, written by Lua
ITER = $18008 ; iteration count, written by Lua
SPTR = $1800C ; V5 span stream pointer, written by Lua
SRCW = $60000 ; word-expanded frame 192*512 = 96KB
SRCB = $80000 ; byte-per-pixel frame 192*256 = 48KB
DST0 = $C08000 ; GVRAM + 32*1024 (first picture row)
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:
move.l VAR.l,d0
move.l #1,FLAG.l ; timer starts here
cmp.l #1,d0
beq v1
cmp.l #2,d0
beq v2
cmp.l #4,d0
beq v4
cmp.l #5,d0
beq v5
cmp.l #6,d0
beq v6
cmp.l #7,d0
beq v7
bra v3
; ---------------------------------------------------------------- V1
v1: lea SRCW,a0
lea DST0,a1
lea DSTE,a6
v1row: movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,48(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,96(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,144(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,192(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,240(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,288(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,336(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,384(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,432(a1)
movem.l (a0)+,d0-d7
movem.l d0-d7,480(a1)
lea 1024(a1),a1
cmpa.l a6,a1
bne v1row
subq.l #1,ITER.l
bne v1
bra done
; ---------------------------------------------------------------- V2
v2: lea SRCB,a0
lea DST0,a1
lea DSTE,a6
v2row: move.w #255,d1
v2px: move.b (a0)+,d0
move.w d0,(a1)+ ; high byte is discarded by gvram_w
dbra d1,v2px
lea 512(a1),a1 ; skip the unused half of the line
cmpa.l a6,a1
bne v2row
subq.l #1,ITER.l
bne v2
bra done
; ---------------------------------------------------------------- V3
v3: lea SRCW,a0
movem.l (a0),d0-d7/a2-a5 ; load the burst once, outside the loop
lea DST0,a1
lea DSTE,a6
v3row: movem.l d0-d7/a2-a5,(a1)
movem.l d0-d7/a2-a5,48(a1)
movem.l d0-d7/a2-a5,96(a1)
movem.l d0-d7/a2-a5,144(a1)
movem.l d0-d7/a2-a5,192(a1)
movem.l d0-d7/a2-a5,240(a1)
movem.l d0-d7/a2-a5,288(a1)
movem.l d0-d7/a2-a5,336(a1)
movem.l d0-d7/a2-a5,384(a1)
movem.l d0-d7/a2-a5,432(a1)
movem.l d0-d7,480(a1)
lea 1024(a1),a1
cmpa.l a6,a1
bne v3row
subq.l #1,ITER.l
bne v3
bra done
; ---------------------------------------------------------------- V4
v4: lea SRCW,a0
lea DST0,a3 ; base of the current block row
lea DSTE,a4 ; one past the last block row
v4brow: move.l a3,a1
lea 512(a3),a5 ; 64 blocks * 8 bytes
v4blk: movem.l (a0)+,d0-d7 ; 32 bytes = one 4x4 block, expanded
movem.l d0-d1,(a1)
movem.l d2-d3,1024(a1)
movem.l d4-d5,2048(a1)
movem.l d6-d7,3072(a1)
addq.l #8,a1
cmpa.l a5,a1
bne.s v4blk
lea 4096(a3),a3 ; next block row is 4 picture lines
cmpa.l a4,a3
bne v4brow
subq.l #1,ITER.l
bne v4
bra done
; ---------------------------------------------------------------- V5
; a0 stream, a1 row base, a2 span destination, d7 rows, d6 spans, d5 pixels,
; d4 burst/tail counter. Everything else (d0-d3/a3-a6) is burst payload.
v5: move.l SPTR.l,a0
lea DST0,a1
move.w #ROWS-1,d7
v5row: move.w (a0)+,d6 ; spans in this row
subq.w #1,d6
bmi.s v5eor ; a row may legitimately have none
v5span: move.w (a0)+,d0 ; x, in pixels
add.w d0,d0 ; one pixel = one word
lea 0(a1,d0.w),a2
move.w (a0)+,d5 ; pixels in this span
move.w d5,d4
lsr.w #4,d4 ; 16-pixel bursts
beq.s v5tail
subq.w #1,d4
v5burst: movem.l (a0)+,d0-d3/a3-a6 ; 32 bytes straight out of the stream
movem.l d0-d3/a3-a6,(a2)
lea 32(a2),a2
dbra d4,v5burst
v5tail: moveq #15,d4
and.w d5,d4 ; 0..15 pixels left
beq.s v5eos
lsr.w #1,d4 ; C = odd pixel count
bcc.s v5t2
move.w (a0)+,(a2)+
v5t2: subq.w #1,d4
bmi.s v5eos
v5tl: move.l (a0)+,(a2)+
dbra d4,v5tl
v5eos: dbra d6,v5span
v5eor: lea 1024(a1),a1
dbra d7,v5row
subq.l #1,ITER.l
bne v5
bra done
; ---------------------------------------------------------------- V6
; a0 stream, a2 destination, d7 spans remaining; everything else is payload.
v6: move.l SPTR.l,a0
move.w (a0)+,d7 ; total spans in the frame
subq.w #1,d7
v6span: move.l (a0)+,a2 ; absolute GVRAM destination
move.w (a0)+,d0 ; (V6MAX - units) * V6UNIT, from the encoder
jmp v6ch(pc,d0.w)
v6ch:
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
dbra d7,v6span
subq.l #1,ITER.l
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