; 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 SRCP = $90000 ; PACKED frame 192*256 = 48KB (V8): two picture ; bytes per word, already interleaved by the ; encoder, so the blit is a straight copy 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 cmp.l #8,d0 beq v8 cmp.l #9,d0 beq v9 cmp.l #10,d0 beq v10 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 ; ---------------------------------------------------------------- V8 ; THE PACKED FULL-FRAME BLIT (FINDINGS 46.6/47.2). Identical in shape to V1 -- ; a row-linear movem.l chain out of a RAM frame into GVRAM -- and different in ; exactly one thing: a row is 128 WORDS, not 256, because R20 bit 11 lets one ; word carry two picture bytes. 256 = 5*48 + 16, so five 12-register bursts ; and a 4-register tail, against V1's ten and one. ; ; TIMING ONLY, and it does not set bit 11. MAME's gvram_w carries no timing in ; either arm (blit.lua's header), so the bit cannot move a cycle here; what it ; moves is the PICTURE, and the picture is what tools/bench/show_frame256_packed.lua ; and tools/bench/gvpack already verify pixel-exactly. Setting it here would ; make this variant's snapshot right and its measurement no different, and ; would put a display-mode change inside a timing loop for no gain. ; ; The source is PRE-INTERLEAVED by the host, which is the honest half of the ; claim: the packing is an encoder-side transform (46.3's argument for the text ; plane, and the same one here), so the decoder-free player's per-frame work is ; this copy and nothing else. If the interleave had to happen at run time this ; variant would be V2, not V1. v8: lea SRCP,a0 lea DST0,a1 lea DSTE,a6 v8row: 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-d3 movem.l d0-d3,240(a1) lea 1024(a1),a1 cmpa.l a6,a1 bne v8row subq.l #1,ITER.l bne v8 bra done ; ---------------------------------------------------------------- V9 ; WHAT THE PACKED LAYOUT COSTS A BLOCK DECODER (FINDINGS 47.6.4, open). ; ; V4 is the access pattern of a decoder that writes 4x4 codewords straight into ; GVRAM: 4 rows of 8 contiguous bytes at a 1024-byte stride, so each row is one ; `movem.l` of two registers. Under the packed layout that pattern is GONE. ; A block at columns x..x+3 owns the LOW bytes of four consecutive words -- four ; bytes at STRIDE 2 -- and the high bytes of those same words belong to the ; block 128 columns away. There is no burst that writes every other byte, so ; the block is sixteen `move.b`s. ; ; V9 does the pair together, low block then high block off one base, so it ; writes every byte it touches and covers the same 49,152 pixels V1/V4/V8 do. ; It is the HONEST version of "keep the codec and pack the screen": the mode ; map is unchanged, SKIP still works per block, and the writes go byte at a ; time. V10 below is the other option, and the comparison is the point. v9: lea SRCB,a0 lea DST0,a3 lea DSTE,a4 v9brow: move.l a3,a1 lea 256(a3),a5 ; 32 block PAIRS * 8 bytes v9blk: move.b (a0)+,(a1) move.b (a0)+,2(a1) move.b (a0)+,4(a1) move.b (a0)+,6(a1) move.b (a0)+,1024(a1) move.b (a0)+,1026(a1) move.b (a0)+,1028(a1) move.b (a0)+,1030(a1) move.b (a0)+,2048(a1) move.b (a0)+,2050(a1) move.b (a0)+,2052(a1) move.b (a0)+,2054(a1) move.b (a0)+,3072(a1) move.b (a0)+,3074(a1) move.b (a0)+,3076(a1) move.b (a0)+,3078(a1) move.b (a0)+,1(a1) move.b (a0)+,3(a1) move.b (a0)+,5(a1) move.b (a0)+,7(a1) move.b (a0)+,1025(a1) move.b (a0)+,1027(a1) move.b (a0)+,1029(a1) move.b (a0)+,1031(a1) move.b (a0)+,2049(a1) move.b (a0)+,2051(a1) move.b (a0)+,2053(a1) move.b (a0)+,2055(a1) move.b (a0)+,3073(a1) move.b (a0)+,3075(a1) move.b (a0)+,3077(a1) move.b (a0)+,3079(a1) addq.l #8,a1 cmpa.l a5,a1 bne v9blk lea 4096(a3),a3 cmpa.l a4,a3 bne v9brow subq.l #1,ITER.l bne v9 bra done ; ---------------------------------------------------------------- V10 ; THE OTHER OPTION: PAIR THE BLOCKS IN THE ENCODER. If the codec codes the ; block at x and the block at x+128 as ONE unit, the destination is whole words ; again and V4's `movem.l` shape comes straight back -- the same instructions, ; the same 32 bytes of source per unit, and TWICE the pixels, because a word now ; carries two of them. So V10 is V4's inner loop run half as many times. ; ; WHAT IT COSTS IS NOT IN THIS MEASUREMENT. A pair skips only if BOTH of its ; blocks skip, and the two are 128 columns apart with nothing in the picture ; relating them. That is a CONTAINER question -- what fraction of the mode map ; survives pairing -- and 08_mode_map.py has the data to answer it. V10 prices ; the paint; it does not price the SKIPs the pairing loses. v10: lea SRCP,a0 lea DST0,a3 lea DSTE,a4 v10brow: move.l a3,a1 lea 256(a3),a5 ; 32 block PAIRS * 8 bytes v10blk: movem.l (a0)+,d0-d7 ; 32 bytes = one PAIR of 4x4 blocks 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 v10blk lea 4096(a3),a3 cmpa.l a4,a3 bne v10brow subq.l #1,ITER.l bne v10 bra done done: move.l #$FF,FLAG.l ; timer stops here halt: bra.s halt