; 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. ; ; 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 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 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 done: move.l #$FF,FLAG.l ; timer stops here halt: bra.s halt