"""Bus-cycle cost of src/player/decode.s and of tools/bench/blit.s's v6 spans. A 68000 bus cycle is 4 clocks (S0-S7) with no wait states, and the 68000 prefetches every instruction word over the same bus. So a block's bus cost is `instruction words + data accesses`, a long access counting twice on the 16-bit bus and `movem.l` of N registers counting 2N. The per-path word counts are read off tools/bench/decode.lst and tools/bench/blit.s. tools/analysis/15_bus_occupancy.py checks the DATA half of this table against tools/bench/c68k/c68k_bench, which counts every bus callback the C68K core makes: they agree to 0.04%. The prefetch half cannot be measured from either emulator -- MAME does not expose a fetch count and C68K reads opcodes through a host pointer with no callback -- so it rests on that check. """ BUS_CLK = 4 # --- decode.s, per block --------------------------------------------------- # dispatch move.b (a1),d0 / lsr.b / and.w #3 / beq .sk 6w, 1 read # + subq / beq .v1 -> 8w # + subq / bne .rw -> 10w # V4 body $10090..$100E2 = 82 B = 41w; 4 x (1 byte read # + movem.l 2 = 4 reads + 2 move.l = 4 writes) = 36 # V1 body $100E2..$10106 = 36 B = 18w; 1 byte read # + movem.l 8 = 16 reads + 4 x movem.l 2 = 16 wr = 33 # RAW body $10106..$10164 = 94 B = 47w; 8 x (2 byte reads # + 1 move.l = 2 writes) = 32 BODY = {0: (0, 0), 1: (18, 33), 2: (41, 36), 3: (47, 32)} DISPATCH = {0: 6, 1: 8, 2: 10, 3: 10} SK_TAIL = 1 # addq.l #8,a4 GROUP_HEAD = 3 # tst.b (a1) + beq allskip GROUP_TAIL = 4 # addq.l #1,a1 / cmpa.l a5,a4 / bne byteloop ALLSKIP = 9 # the whole four-block fast path, tst.b included ROW_HEAD, ROW_TAIL = 3, 7 # --- blit.s v6 spans ------------------------------------------------------- # One chain unit moves 12 registers = 48 B = 24 pixels: # movem.l (a0)+,12 = 2w instr + 24 word reads = 26 # movem.l 12,(a2) = 2w instr + 24 word writes = 26 # lea 48(a2),a2 = 2w instr = 2 # Per span: move.l (a0)+,a2 (1w + 2 reads) + move.w (a0)+,d0 (1w + 1 read) # + jmp v6ch(pc,d0.w) (2w) + dbra (2w) = 9 V6_UNIT_PX = 24 V6_UNIT_BUS = 54 V6_SPAN_BUS = 9 V6_SPAN_CYC = 43.7 # MEASURED, FINDINGS 30 V6_PX_CYC = 9.152 # MEASURED, FINDINGS 30 # --- a DMAC array-chaining span ------------------------------------------- # SOURCED, MC68450 Direct Memory Access Controller, Motorola, Jul 1989 # (bitsavers). These replace session-10's first pass, which guessed 2 bus # cycles a pixel from bus arithmetic and was 12% optimistic. # # Fig 4-25 sheet 4, DUAL ADDRESS / OPERAND SIZE IS WORD / DEVICE SIZE IS # 16-BITS, D->M or M->D: {WORD READ, WORD WRITE} = 9 CLOCKS. # Confirmed by the long-operand row: two of each = 18 clocks. # Fig 4-25 note 2: reads are 4 clocks and WRITES ARE 5. That extra clock on # every write is the whole story -- it is why the DMAC does not beat a 68000 # movem chain, which writes in 4. DMA_PX_CLK = 9 # Fig 4-25 sheet 1, SEQUENTIAL ARRAY CHAINING: 36 CLOCKS per entry (three # word reads to fetch the 6-byte entry, plus reload). DMA_CHAIN_CLK = 36 # Sect 4.5.2.1 front-end overhead 5 clocks best case, 8 worst; 4.5.2.2 # back-end 2 clocks best. Once per period of bus ownership, not per span. DMA_FRONT_CLK, DMA_BACK_CLK = 5, 2 # Fig 4-25 sheet 3, SINGLE ADDRESS: W/B READ 4 clocks, W/B WRITE 5 clocks. # A device->memory disk transfer is one memory WRITE = 5 clocks if the DMAC # holds the bus, or 5 + front + back = 12 if it arbitrates per word. # FINDINGS 5's long-standing 8 clk/word ESTIMATE sits inside that range. DMA_DISK_CLK_WORD_HELD, DMA_DISK_CLK_WORD_ARB = 5, 12 # The 68000 cannot execute while another master owns the bus: no cache, and a # two-word prefetch queue that empties immediately. So DMA time is ADDITIVE to # CPU time, not overlapped -- which is what FINDINGS 35's flat debit assumed # and session 10's first pass wrongly "refined". DMA_OVERLAPS = False def pad24(npix): return -(-npix // V6_UNIT_PX) * V6_UNIT_PX def block_bus(mode_map, spanned=None): """(instruction words, data accesses) for one frame's CPU block decode. `spanned` is a boolean array the same shape as mode_map marking blocks a span will paint instead; those blocks still cost their dispatch, because the mode map is walked either way, but not their body.""" nby, nbx = mode_map.shape pref = nby * (ROW_HEAD + ROW_TAIL) data = 0 for by in range(nby): row = mode_map[by] sp = spanned[by] if spanned is not None else None for gi in range(0, nbx, 4): g = row[gi:gi + 4] if (g == 0).all(): pref += ALLSKIP data += 1 continue pref += GROUP_HEAD + GROUP_TAIL - 1 # BLOCK 0 has no lsr.b data += 1 for k, b in enumerate(g): b = int(b) if sp is not None and sp[gi + k]: b = 0 # the span paints it pw, pd = BODY[b] pref += DISPATCH[b] + pw + SK_TAIL data += 1 + pd return pref, data # --- 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, 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 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.""" p, d = v7_span_split(npix) return p + d def v7_span_split(npix): """(instruction words, data accesses) for one v7 span, separately. 15_bus_occupancy.py needs the two apart, because the DATA half is what the C68K harness can check and the PREFETCH half is what rides on that check. per span move.l (a0)+,a2 1 word + 2 reads move.w (a0)+,d0 1 word + 1 read (coarse displacement) jmp (pc,d0.w) 2 words move.w (a0)+,d0 1 word + 1 read (fine, from mid-stream) jmp (pc,d0.w) 2 words dbra 2 words -> 9 words, 4 accesses per coarse 2 movem.l of 12 + lea = 6 words, 24 reads + 24 writes per fine move.l (a0)+,(a2)+ = 1 word, 2 reads + 2 writes """ k, r = divmod(pad2(npix), V6_UNIT_PX) f = r // V7_FINE_PX return (9 + k * 6 + f * 1, 4 + k * 48 + f * 4) # Per FRAME, decode.s's paint_spans entry and exit: the span count read, the # guard branch, and the push/pop of a1 that buys back a twelfth payload # register. Two long accesses a frame against 24 pixels a chain unit. V7_FRAME_PREF, V7_FRAME_DATA = 7, 7 def v6_span_bus(npix): k = pad24(npix) // V6_UNIT_PX return V6_SPAN_BUS + k * V6_UNIT_BUS