joeylib2/src/m68k/planarFill.s
2026-09-24 22:01:56 -05:00

503 lines
20 KiB
ArmAsm

| Shared 68000 separate-plane area fills -- hand-rolled.
|
| Used by BOTH separate-plane 68k ports, the Amiga and the X68000: a 320px
| 4bpp surface is four plane buffers of 40 bytes per row on either machine, so
| a span of pixels is the same bytes in the same places. (The Atari ST is
| word-interleaved and keeps its own St* routines.)
|
| Home of the span-filling primitives that write a run of pixels into all
| four plane buffers at once. Currently:
|
| surface68kFillCircle jlFillCircle: the whole exact-disk walk
| surface68kFillRect jlFillRect: a rect that does not span whole rows
|
| A span is a partial left byte, a run of whole middle bytes, and a partial
| right byte, in each of four planes. Three things make this much cheaper
| than the general case, and the PLANEFILL macro below is built around them:
|
| * A PLANE'S FILL VALUE IS ONLY EVER ALL-ONES OR ALL-ZEROS. It is one bit
| of the colour smeared across a byte. So a masked edge is not a read,
| two ANDs, an OR and a store; it is `or.b mask,(a0)` for a set plane and
| `and.b ~mask,(a0)` for a clear one -- one instruction, one bus cycle.
| * THE COLOUR IS FIXED FOR THE WHOLE SHAPE, so which of those two it is
| need not be decided at run time. The body is emitted once per colour
| value and dispatched to through a jump table, exactly as
| src/x68000/circle.s does. Set planes then run as a group with d6 = -1
| and clear planes as a group with d6 = 0 and the masks complemented
| once, so the middle run is the same code for both.
| * THE MIDDLE RUN IS WHOLE BYTES of that value, so it goes out a long at
| a time through a dbra loop once the pointer is aligned.
|
| ABI: cdecl (gcc-amigaos / elf2x68k), d2-d7/a2-a6 callee-save. 16-bit
| arguments are sign-extended to 4 bytes on the call stack.
|
| Every entry point carries BOTH symbol spellings at the same address:
| gcc-amigaos and gcc-atari-mint prepend an underscore to C names and
| elf2x68k does not, and one extra unused symbol is cheaper than forking the
| file over a naming convention.
.text
.even
.globl surface68kFillCircle
.globl _surface68kFillCircle
.globl surface68kFillRect
.globl _surface68kFillRect
| Bytes per plane row: SURFACE_WIDTH / 8, which is AMIGA_BYTES_PER_ROW and
| X68K_BYTES_PER_ROW both. Every planar surface on either port is
| SURFACE_WIDTH x SURFACE_HEIGHT, so it is a constant here as well as in the
| C, and both ports' C guards the radius bound that depends on it.
.equ ROW_BYTES, 40
| Stack locals. The walk state lives here rather than in registers because it
| is touched once per row while the span body runs twice, so the registers are
| worth more to the span.
.equ FC_XX, 0 | x*x tracker, word
.equ FC_XLIMIT, 2 | r*r - y*y, word
.equ FC_X, 4 | current x, word
.equ FC_T, 6 | 2*y + 1, word
.equ FC_COUNT, 8 | rows left, word
.equ FC_DOWN, 10 | (cy + y) * 40, word
.equ FC_UP, 12 | (cy - y) * 40, word
.equ FC_FIRST, 14 | span's first byte in a row
.equ FC_LOCAL, 16
| 16 locals + 44 saved registers + 4 return address
.equ FC_ARG, (FC_LOCAL + 44 + 4)
.equ FC_PLANES, FC_ARG + 0
.equ FC_CX, FC_ARG + 4 + 2
.equ FC_CY, FC_ARG + 8 + 2
.equ FC_R, FC_ARG + 12 + 2
.equ FC_COLOR, FC_ARG + 16 + 2
| ---- PLANEFILL: one span in one plane ----
| \areg = that plane's base, \op = or (set) or and (clear).
| d7.w = span's byte offset, d2 = left mask, d3 = right mask, d4 = middle
| byte count, d6 = the fill value (0 or -1, matching \op).
| Trashes d0, d1, d5, a0.
.macro PLANEFILL areg, op
lea (\areg,%d7.w),%a0
\op\().b %d2,(%a0)+ | left edge
move.w %d4,%d5
beq.s 9f
1: move.l %a0,%d1 | byte-align, then longs
and.w #3,%d1
beq.s 2f
move.b %d6,(%a0)+
subq.w #1,%d5
bne.s 1b
bra.s 9f
2: move.w %d5,%d0
and.w #3,%d5 | d5 = trailing bytes
lsr.w #2,%d0 | d0 = whole longs
beq.s 5f
subq.w #1,%d0
3: move.l %d6,(%a0)+
dbra %d0,3b
5: tst.w %d5
beq.s 9f
6: move.b %d6,(%a0)+
subq.w #1,%d5
bne.s 6b
9: \op\().b %d3,(%a0) | right edge
.endm
| ---- SPAN: one span in all four planes, for a known colour ----
| Set planes first with d6 = -1, then the clear planes with d6 = 0 and the
| masks complemented, so `and.b %d2` is a single instruction there too.
.macro SPAN colour
.if (\colour) != 0
moveq #-1,%d6
.if ((\colour) & 1)
PLANEFILL %a2, or
.endif
.if ((\colour) & 2)
PLANEFILL %a3, or
.endif
.if ((\colour) & 4)
PLANEFILL %a4, or
.endif
.if ((\colour) & 8)
PLANEFILL %a5, or
.endif
.endif
.if (\colour) != 15
moveq #0,%d6
not.b %d2
not.b %d3
.if !((\colour) & 1)
PLANEFILL %a2, and
.endif
.if !((\colour) & 2)
PLANEFILL %a3, and
.endif
.if !((\colour) & 4)
PLANEFILL %a4, and
.endif
.if !((\colour) & 8)
PLANEFILL %a5, and
.endif
not.b %d2 | the row's second span wants
not.b %d3 | the masks back
.endif
.endm
| ---- CIRCLEBODY: the whole exact-disk walk for one colour ----
|
| Bit-identical to the chunky reference loop in src/core/draw.c jlFillCircle
| and to the C walk this replaces: for each y in [0, r] find the largest x
| with x*x + y*y <= r*r incrementally, then fill the span (cx-x, cx+x) at
| rows cy+y and cy-y. Each row is filled exactly once -- no octant overlap.
|
| The arithmetic is 16-bit throughout. r*r must fit a word, which holds for
| r <= 181; the caller's contract (the bounding box is on-surface, and a
| surface is 200 rows tall) caps r at 99, and halPlanar.c has a build-time
| check on that.
.macro CIRCLEBODY colour
.Lrow\colour:
| walk x down until x*x + y*y <= r*r again
move.w FC_XX(%sp),%d0
move.w FC_XLIMIT(%sp),%d1
cmp.w %d1,%d0
bls.s .Lgeom\colour
move.w FC_X(%sp),%d4
.Lshrink\colour:
move.w %d4,%d5
add.w %d5,%d5
subq.w #1,%d5 | 2x - 1
sub.w %d5,%d0 | xx -= 2x - 1
subq.w #1,%d4 | x--
cmp.w %d1,%d0
bhi.s .Lshrink\colour
move.w %d0,FC_XX(%sp)
move.w %d4,FC_X(%sp)
.Lgeom\colour:
| span geometry, computed once for the row's two spans
move.w FC_X(%sp),%d0
move.w %a6,%d1
sub.w %d0,%d1 | left = cx - x
add.w %a6,%d0 | right = cx + x
move.w %d1,%d4
lsr.w #3,%d4 | first byte
move.w %d4,FC_FIRST(%sp)
and.w #7,%d1
move.b (%a1,%d1.w),%d2 | left mask
move.w %d0,%d1
lsr.w #3,%d1 | last byte
and.w #7,%d0
move.b 8(%a1,%d0.w),%d3 | right mask
sub.w %d4,%d1
bne.s .Lmulti\colour
| Both ends in one byte. Combining the masks covers it, and
| zeroing the right mask makes the right edge write the NEXT
| byte back unchanged instead of needing its own code path.
| (The plane allocation carries a spare byte for this.)
and.b %d3,%d2
moveq #0,%d3
moveq #0,%d4
bra.s .Ldown\colour
.Lmulti\colour:
subq.w #1,%d1
move.w %d1,%d4 | middle byte count
.Ldown\colour:
move.w FC_DOWN(%sp),%d7
add.w FC_FIRST(%sp),%d7
SPAN \colour
| the mirrored row, unless y is zero and it IS this row
move.w FC_UP(%sp),%d7
cmp.w FC_DOWN(%sp),%d7
beq.w .Lstep\colour
add.w FC_FIRST(%sp),%d7
SPAN \colour
.Lstep\colour:
addi.w #ROW_BYTES,FC_DOWN(%sp)
subi.w #ROW_BYTES,FC_UP(%sp)
| y*y grows by 2y+1, so the x*x limit shrinks by it
move.w FC_T(%sp),%d0
sub.w %d0,FC_XLIMIT(%sp)
addq.w #2,%d0
move.w %d0,FC_T(%sp)
subq.w #1,FC_COUNT(%sp)
bne.w .Lrow\colour
bra.w fcDone
.endm
| void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy,
| uint16_t r, uint16_t color);
surface68kFillCircle:
_surface68kFillCircle:
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
sub.l #FC_LOCAL,%sp
move.l FC_PLANES(%sp),%a0
move.l (%a0),%a2
move.l 4(%a0),%a3
move.l 8(%a0),%a4
move.l 12(%a0),%a5
move.w FC_CX(%sp),%a6
move.w FC_CY(%sp),%d0
move.w FC_R(%sp),%d1
move.w %d1,%d2
mulu.w %d2,%d2 | r*r, a word for r <= 181
move.w %d2,FC_XX(%sp)
move.w %d2,FC_XLIMIT(%sp)
move.w %d1,FC_X(%sp)
move.w #1,FC_T(%sp)
addq.w #1,%d1
move.w %d1,FC_COUNT(%sp) | r + 1 rows
| cy * 40 as (cy << 5) + (cy << 3)
move.w %d0,%d1
lsl.w #5,%d0
lsl.w #3,%d1
add.w %d1,%d0
move.w %d0,FC_DOWN(%sp)
move.w %d0,FC_UP(%sp)
lea gFillMaskLut,%a1
move.w FC_COLOR(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
move.w fcJump(%pc,%d0.w),%d0
jmp fcJump(%pc,%d0.w)
fcJump:
.word fcC0-fcJump, fcC1-fcJump, fcC2-fcJump, fcC3-fcJump
.word fcC4-fcJump, fcC5-fcJump, fcC6-fcJump, fcC7-fcJump
.word fcC8-fcJump, fcC9-fcJump, fcC10-fcJump, fcC11-fcJump
.word fcC12-fcJump, fcC13-fcJump, fcC14-fcJump, fcC15-fcJump
fcC0: CIRCLEBODY 0
fcC1: CIRCLEBODY 1
fcC2: CIRCLEBODY 2
fcC3: CIRCLEBODY 3
fcC4: CIRCLEBODY 4
fcC5: CIRCLEBODY 5
fcC6: CIRCLEBODY 6
fcC7: CIRCLEBODY 7
fcC8: CIRCLEBODY 8
fcC9: CIRCLEBODY 9
fcC10: CIRCLEBODY 10
fcC11: CIRCLEBODY 11
fcC12: CIRCLEBODY 12
fcC13: CIRCLEBODY 13
fcC14: CIRCLEBODY 14
fcC15: CIRCLEBODY 15
fcDone:
add.l #FC_LOCAL,%sp
movem.l %sp@+,%d2-%d7/%a2-%a6
rts
| ---- RECTBODY: every row of the rect, for one colour ----
|
| A rect is one span repeated, so the masks and the middle byte count are
| computed once at entry and stay in d2/d3/d4 for the whole shape; the row
| loop only walks the offset. d7 counts up by a row's worth of bytes until it
| meets a6, which is one row past the last.
.macro RECTBODY colour
.Lrect\colour:
SPAN \colour
addi.w #ROW_BYTES,%d7
cmp.w %a6,%d7
bne.w .Lrect\colour
bra.w frDone
.endm
| ---- THINPLANE / THINBODY: a rect only one byte wide ----
|
| Worth its own body rather than folding into SPAN. A vertical line is this
| shape (draw.c turns one into a 1px-wide rect), and it has no middle run and
| only one masked byte per row, so the generic span pays for a middle-run test
| and a second edge it does not need. Here the plane loop is outside the row
| loop, which leaves a dbra over a single read-modify-write: d2 holds the one
| combined mask and d4 the row count less one.
.macro THINPLANE areg, op
move.w %d4,%d5
lea (\areg,%d7.w),%a0
1: \op\().b %d2,(%a0)
lea ROW_BYTES(%a0),%a0
dbra %d5,1b
.endm
.macro THINBODY colour
.if (\colour) != 0
.if ((\colour) & 1)
THINPLANE %a2, or
.endif
.if ((\colour) & 2)
THINPLANE %a3, or
.endif
.if ((\colour) & 4)
THINPLANE %a4, or
.endif
.if ((\colour) & 8)
THINPLANE %a5, or
.endif
.endif
.if (\colour) != 15
not.b %d2
.if !((\colour) & 1)
THINPLANE %a2, and
.endif
.if !((\colour) & 2)
THINPLANE %a3, and
.endif
.if !((\colour) & 4)
THINPLANE %a4, and
.endif
.if !((\colour) & 8)
THINPLANE %a5, and
.endif
.endif
bra.w frDone
.endm
| 44 saved registers + 4 return address
.equ FR_ARG, 48
.equ FR_PLANES, FR_ARG + 0
.equ FR_X, FR_ARG + 4 + 2
.equ FR_Y, FR_ARG + 8 + 2
.equ FR_W, FR_ARG + 12 + 2
.equ FR_H, FR_ARG + 16 + 2
.equ FR_COLOR, FR_ARG + 20 + 2
| void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y,
| uint16_t w, uint16_t h, uint16_t color);
|
| w and h must both be at least 1 -- draw.c clips before it calls, so an empty
| rect never arrives. A rect covering whole rows is better served by one flat
| run of words per plane and halPlanar.c keeps that case to itself; everything
| else, partial pixel edges included, lands here.
surface68kFillRect:
_surface68kFillRect:
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
move.l FR_PLANES(%sp),%a0
move.l (%a0),%a2
move.l 4(%a0),%a3
move.l 8(%a0),%a4
move.l 12(%a0),%a5
lea gFillMaskLut,%a1
| span geometry, once for the whole rect
move.w FR_X(%sp),%d0 | left
move.w FR_W(%sp),%d1
add.w %d0,%d1
subq.w #1,%d1 | right
move.w %d0,%d5
lsr.w #3,%d5 | first byte
and.w #7,%d0
move.b (%a1,%d0.w),%d2 | left mask
move.w %d1,%d0
lsr.w #3,%d0 | last byte
and.w #7,%d1
move.b 8(%a1,%d1.w),%d3 | right mask
sub.w %d5,%d0
bne.s 1f
| One byte wide: one combined mask, and d4 becomes the dbra
| row count for THINBODY instead of a middle-run length.
and.b %d3,%d2
move.w FR_H(%sp),%d4
subq.w #1,%d4
moveq #1,%d6 | take the thin dispatch
bra.s 2f
1: subq.w #1,%d0
move.w %d0,%d4 | middle byte count
moveq #0,%d6
2: | d7 = y * ROW_BYTES + firstByte, a6 = one row past the last
move.w FR_Y(%sp),%d0
move.w %d0,%d1
lsl.w #5,%d0
lsl.w #3,%d1
add.w %d1,%d0 | y * 40, as (y << 5) + (y << 3)
add.w %d5,%d0
move.w %d0,%d7
move.w FR_H(%sp),%d0
move.w %d0,%d1
lsl.w #5,%d0
lsl.w #3,%d1
add.w %d1,%d0 | h * 40
add.w %d7,%d0
move.w %d0,%a6
move.w FR_COLOR(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
| The jump table goes through a1: a PC-relative index has only
| an 8-bit displacement, and the thin table is 4 KB out.
tst.w %d6
bne.s .Lthin
lea frJump(%pc),%a1
bra.s .Ldispatch
.Lthin:
lea ftJump(%pc),%a1
.Ldispatch:
move.w (%a1,%d0.w),%d1
jmp (%a1,%d1.w)
frJump:
.word frC0-frJump, frC1-frJump, frC2-frJump, frC3-frJump
.word frC4-frJump, frC5-frJump, frC6-frJump, frC7-frJump
.word frC8-frJump, frC9-frJump, frC10-frJump, frC11-frJump
.word frC12-frJump, frC13-frJump, frC14-frJump, frC15-frJump
frC0: RECTBODY 0
frC1: RECTBODY 1
frC2: RECTBODY 2
frC3: RECTBODY 3
frC4: RECTBODY 4
frC5: RECTBODY 5
frC6: RECTBODY 6
frC7: RECTBODY 7
frC8: RECTBODY 8
frC9: RECTBODY 9
frC10: RECTBODY 10
frC11: RECTBODY 11
frC12: RECTBODY 12
frC13: RECTBODY 13
frC14: RECTBODY 14
frC15: RECTBODY 15
ftJump:
.word ftC0-ftJump, ftC1-ftJump, ftC2-ftJump, ftC3-ftJump
.word ftC4-ftJump, ftC5-ftJump, ftC6-ftJump, ftC7-ftJump
.word ftC8-ftJump, ftC9-ftJump, ftC10-ftJump, ftC11-ftJump
.word ftC12-ftJump, ftC13-ftJump, ftC14-ftJump, ftC15-ftJump
ftC0: THINBODY 0
ftC1: THINBODY 1
ftC2: THINBODY 2
ftC3: THINBODY 3
ftC4: THINBODY 4
ftC5: THINBODY 5
ftC6: THINBODY 6
ftC7: THINBODY 7
ftC8: THINBODY 8
ftC9: THINBODY 9
ftC10: THINBODY 10
ftC11: THINBODY 11
ftC12: THINBODY 12
ftC13: THINBODY 13
ftC14: THINBODY 14
ftC15: THINBODY 15
frDone:
movem.l %sp@+,%d2-%d7/%a2-%a6
rts
.data
.even
| Eight left-edge masks (bits at and to the right of x&7), then eight
| right-edge masks (bits at and to the left of it). The span body reaches the
| second half at 8(%a1,%dn.w), so the two must stay adjacent.
gFillMaskLut:
.byte 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01
.byte 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF