; joeyDraw.s - Apple IIgs ASM inner loops for graphics primitives. ; ; GAS syntax for the llvm-mos w65816 toolchain. Each function is the ; FAST INNER LOOP only; ; the C wrappers in src/core/draw.c and src/core/tile.c handle NULL / ; range validation, clipping, and dirty-rect marking. The inner loops ; trust their inputs (pre-clipped, byte-aligned, non-NULL). ; ; Calling convention (llvm-mos w65816 cdecl), as probed from clang -S: ; - arg0 in registers: A:X for a 32-bit pointer/long, A for a 16-bit ; word. (clang puts arg0 in registers, which is why the ; per-function stack offsets below are small.) ; - args 1.. on the stack, pushed right-to-left, first stack arg at ; 4,s (after the 3-byte JSL return), each word +2, each ptr/long +4. ; - After PHP every stack offset shifts by +1. ; - uint16_t return in A; 32-bit return in A:X. ; ; Memory model (llvm-mos small model): DBR = 0 for C code, and globals ; live in bank 0, so 16-bit absolute reaches both globals and bank-0 ; soft switches ($00Cxxx) -- no long prefix is needed on those. ; The SHR stage at $01:2000 is written with full 24-bit literals, which ; the assembler selects long addressing for automatically (value >64K). ; ; llvm-mc defaults to M=I=16; the explicit .a8/.a16/.i8/.i16 directives ; below pair with sep/rep so immediates are sized correctly. .text ; ---------------------------------------------------------------- ; void iigsInitRowLut(void) ; ; One-shot init for the y*160 lookup table at gRowOffsetLut. C calls ; this once from halInit; afterwards every primitive that needs y -> ; row byte offset can do `lda gRowOffsetLut,x` (X = y*2) in a few cyc ; instead of a shift-add chain. 200 entries x 2 bytes = 400 bytes. ; ---------------------------------------------------------------- .section .text.iigsInitRowLut,"ax" .globl iigsInitRowLut iigsInitRowLut: php rep #0x30 ; M=16, X=16 .a16 .i16 ldx #0 ; X = byte offset into LUT lda #0 ; A = current y*160 initLutLoop: sta gRowOffsetLut, x clc adc #160 inx inx cpx #400 bcc initLutLoop .a8 .i8 plp rtl ; ---------------------------------------------------------------- ; void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord) ; ; Fast constant fill of the 32000-byte stage at $01:$2000-$9CFF with ; fillWord (both bytes the same). Unrolled run of STA long-absolute ; indexed stores (`sta >$0120xx,x`, 6 cyc / 2 bytes). 40 stores cover ; 80 bytes; X steps by 80 over 400 rows. Long addressing targets bank ; $01 explicitly, so no DBR / AUXWRITE / shadow manipulation and no SEI. ; ; `pixels` (arg0) arrives in A:X and is ignored; the stage is hardcoded ; at $01:$2000 (the C wrapper guarantees s == jlStageGet()). fillWord ; (arg1) is at 5,s after PHP. ; ---------------------------------------------------------------- .section .text.iigsSurfaceClearInner,"ax" .globl iigsSurfaceClearInner iigsSurfaceClearInner: php rep #0x30 ; M=16, X=16 .a16 .i16 ldx #0 ; X = row byte base (0,80,...,31920) clrRowLoop: lda 5, s ; A = fillWord (S never moves; 5,s stays valid) sta 0x012000, x sta 0x012002, x sta 0x012004, x sta 0x012006, x sta 0x012008, x sta 0x01200a, x sta 0x01200c, x sta 0x01200e, x sta 0x012010, x sta 0x012012, x sta 0x012014, x sta 0x012016, x sta 0x012018, x sta 0x01201a, x sta 0x01201c, x sta 0x01201e, x sta 0x012020, x sta 0x012022, x sta 0x012024, x sta 0x012026, x sta 0x012028, x sta 0x01202a, x sta 0x01202c, x sta 0x01202e, x sta 0x012030, x sta 0x012032, x sta 0x012034, x sta 0x012036, x sta 0x012038, x sta 0x01203a, x sta 0x01203c, x sta 0x01203e, x sta 0x012040, x sta 0x012042, x sta 0x012044, x sta 0x012046, x sta 0x012048, x sta 0x01204a, x sta 0x01204c, x sta 0x01204e, x txa clc adc #80 tax cpx #32000 bcs clrExit ; X >= 32000 -> done brl clrRowLoop ; loop back is >127 bytes -> needs brl clrExit: .a8 .i8 plp rtl ; ---------------------------------------------------------------- ; void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord) ; ; Post-init fast clear: PHA stack-slam (4 cyc / 2 bytes) vs the STA ; long,X path (6 cyc / 2 bytes). SP is relocated to the top of the $01 ; stage region ($2000..$9CFF); AUXWRITE ($C005) redirects the bank-$00 ; stack writes to the $01 stage bank, and SHR shadow is already ; inhibited by halInit. ~1.5x faster. ; ; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI. ; SAFE ONLY POST-halInit. Soft switches use LONG addressing to bank $E0 ; ($E0C005), NOT `sta $C005`: under the GS/OS Loader DBR=$08, a DBR-rel ; `sta $C0xx` hits bank-$08 RAM and the switch silently never toggles, so ; AUXWRITE stays off and the slam corrupts bank 0 instead of the $01 ; stage. `pixels` (arg0) in A:X is ignored; fillWord (arg1) at 5,s. ; ---------------------------------------------------------------- .section .text.iigsSurfaceClearFastInner,"ax" .globl iigsSurfaceClearFastInner iigsSurfaceClearFastInner: php rep #0x30 .a16 .i16 lda 5, s ; A = fillWord (read before SP/AUXWRITE change) sta sclrFillCap sei tsc sta sclrSavedSp ; save original SP sep #0x20 .a8 sta 0xE0C005 ; AUXWRITE on: $00:0200-BFFF writes -> bank $01 rep #0x20 .a16 lda #0x9cff tcs ; SP = top of $01 stage region lda sclrFillCap ; A = fillWord = the pushed value ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes clrFastBlock: pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha pha dex bne clrFastBlock lda sclrSavedSp tcs ; restore original SP sep #0x20 .a8 sta 0xE0C004 ; AUXWRITE off rep #0x20 .a16 .a8 .i8 plp ; restores I (CLI) rtl ; ----- DRAWPRIMS data (uninitialized; bank 0 BSS) ----- .section .bss.gRowOffsetLut,"aw" .globl gRowOffsetLut gRowOffsetLut: .zero 400 .section .bss.sclrFillCap,"aw" .globl sclrFillCap sclrFillCap: .zero 2 .section .bss.sclrSavedSp,"aw" .globl sclrSavedSp sclrSavedSp: .zero 2 ; Flood-fill seed/match scratch (asm-only data; no C definition exists). ; iigsFloodWalkAndScansInner writes the seed match result and the left/ ; right span extents here for the C wrapper to read back. .section .bss.gFloodSeedMatch,"aw" .globl gFloodSeedMatch gFloodSeedMatch: .zero 2 .section .bss.gFloodLeftX,"aw" .globl gFloodLeftX gFloodLeftX: .zero 2 .section .bss.gFloodRightX,"aw" .globl gFloodRightX gFloodRightX: .zero 2 ; ===== ported: tile primitives ===== ; ==================================================================== ; frag_tiles.s - tile inner loops (llvm-mc w65816) ; ; Shared ABI fact (clang w65816 cdecl, per PORT_SPEC.md): ; - arg0 (pointer/long) arrives in A=low16, X=high16 (bank byte = ; low byte of X). It is NOT on the stack. ; - arg1, arg2, ... are pushed right-to-left and live on the stack; ; on JSL entry arg1 byte0 is at 4,s. A word arg is 2 bytes, a ; pointer/long arg is 4 bytes. ; - Each prologue push shifts every stack offset up: php=+1, phb=+1, ; phd=+2, pha/phx=+2. ; ==================================================================== ; -------------------------------------------------------------------- ; STAGING pointer block for the two-pointer tile routines (Copy / Paste ; / Snap / CopyMasked). The clang ABI hands us dst in A:X and src on the ; stack, but the copy loops assume a D-frame with dst at [D+0] and src ; at [D+4]. We assemble the 8 bytes here first, then COPY them onto the ; hardware stack and point D there -- D (the direct page) is ALWAYS bank ; 0, but this BSS block lives in the LOAD bank, so D must NOT point at it ; directly (that was the historical bug: [0],y/[4],y then read bank 0 ; while the stores wrote the load bank). The stack is bank 0, so the ; copied frame matches. ; tileScratchPtrs+0..2 = dst (low16 + bank) (staged, then -> [0],y) ; tileScratchPtrs+4..6 = src (low16 + bank) (staged, then -> [4],y) ; (bytes +3 / +7 are unused padding, mirroring the 4-byte ptr layout) ; -------------------------------------------------------------------- .section .bss.tileScratchPtrs,"aw" .globl tileScratchPtrs tileScratchPtrs: .zero 8 ; ==================================================================== ; iigsTileFillInner(uint8_t *dstRow0, uint16_t fillWord) ; ; Original: 16 STA abs,X stores at fixed offsets along a 160-byte ; stride (DBR set to the dst bank via PHB/PLB so abs,X rode the right ; bank). ~120 cyc per call vs the C version's ~300. ; ; ABI: arg0 dstRow0 in A:X (A = dst low16, X = dst high16 incl. bank). ; arg1 fillWord (word) on the stack: @4,s on JSL entry; after ; PHP+PHB+PHD(+4) it sits @8,s. Only fillWord is on the stack and ; dst comes in A:X. ; ; PORTING NOTE (addressing mode): the original used `sta |off,x` ; (16-bit absolute,X, DBR = dst bank). llvm-mc here unconditionally ; shrinks `sta off,x` with a small constant `off` to DIRECT-PAGE,X ; (opcode 0x95), which is bank-0 / D-relative and would target the ; wrong memory; and this build accepts none of the abs-forcing ; modifiers (`!`/`|`/`mos16()`). To stay bank-correct AND assemble, we ; keep the identical 16 store offsets and the identical fill word but ; reach the destination via DP-indirect-long `[dst],y` with Y = the ; offset -- the same mechanism the two-pointer tile routines use. We ; stash dst (A:X) in tileScratchPtrs and point D there ([0] = dst). ; D-frame: tileScratchPtrs+0..2 = dst (low16 + bank) -> [0],y ; ==================================================================== .section .text.iigsTileFillInner,"ax" .globl iigsTileFillInner iigsTileFillInner: php phb phd ; save caller D rep #0x30 .a16 .i16 ; Build the dst D-frame on the STACK (bank 0). A BSS scratch ; (tileScratchPtrs) lives in the LOAD bank, but D (the direct ; page) is ALWAYS bank 0, so the old `tcd #tileScratchPtrs` ; pointed [0],y at bank-0:offset while the stores wrote ; loadbank:offset -- the indirect read the wrong bank entirely. ; The hardware stack is bank 0, so a stack D-frame matches. ; A = dst low16, X = dst high16 (bank in low byte). phx ; dst high16 -> 3,s pha ; dst low16 -> 1,s tsc inc a ; A = SP+1 tcd ; D -> stacked dst; [0] = dst lda 12,s ; fillWord (8,s + 4 just pushed) ldy #0 sta [0], y ldy #2 sta [0], y ldy #160 sta [0], y ldy #162 sta [0], y ldy #320 sta [0], y ldy #322 sta [0], y ldy #480 sta [0], y ldy #482 sta [0], y ldy #640 sta [0], y ldy #642 sta [0], y ldy #800 sta [0], y ldy #802 sta [0], y ldy #960 sta [0], y ldy #962 sta [0], y ldy #1120 sta [0], y ldy #1122 sta [0], y .a8 .i8 rep #0x30 .a16 .i16 tsc clc adc #4 ; drop the dst D-frame (phx+pha) tcs pld ; restore caller D plb plp rtl ; ==================================================================== ; iigsTileCopyInner(uint8_t *dst, const uint8_t *src) ; ; Opaque copy of an 8x8 tile region (32 bytes) between two 4bpp ; surfaces with 160-byte row stride. Dst and src may be in any banks. ; ~340 cyc per tile vs the C version's ~900. ; ; ABI: arg0 dst in A:X (A=low16, X=high16). arg1 src (4-byte ptr) on ; the stack: @4,s on JSL entry; after PHP+PHB+PHD(+4) src low16 ; @8,s, src bank @10,s. We point D at tileScratchPtrs, store dst ; (A:X) at +0..2 and src (from the stack) at +4..6, then run the ; original [0],y (dst) / [4],y (src) loads unchanged. PLD restores ; the caller's D (saved by our PHD). ; ==================================================================== .section .text.iigsTileCopyInner,"ax" .globl iigsTileCopyInner iigsTileCopyInner: php phb phd ; save caller D rep #0x30 .a16 .i16 ; Build the dst/src D-frame in bank-0 scratch. sta tileScratchPtrs+0 ; dst low16 stx tileScratchPtrs+2 ; dst high16 (bank + pad) lda 8,s ; src low16 (post +4 push) sta tileScratchPtrs+4 lda 10,s ; src bank sta tileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via ; `tcd #tileScratchPtrs` indirected through the wrong bank. lda tileScratchPtrs+6 pha ; src bank -> 7,s lda tileScratchPtrs+4 pha ; src low16 -> 5,s lda tileScratchPtrs+2 pha ; dst high16 -> 3,s lda tileScratchPtrs+0 pha ; dst low16 -> 1,s tsc inc a ; A = SP+1 tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ldy #0 lda [4], y sta [0], y ldy #2 lda [4], y sta [0], y ldy #160 lda [4], y sta [0], y ldy #162 lda [4], y sta [0], y ldy #320 lda [4], y sta [0], y ldy #322 lda [4], y sta [0], y ldy #480 lda [4], y sta [0], y ldy #482 lda [4], y sta [0], y ldy #640 lda [4], y sta [0], y ldy #642 lda [4], y sta [0], y ldy #800 lda [4], y sta [0], y ldy #802 lda [4], y sta [0], y ldy #960 lda [4], y sta [0], y ldy #962 lda [4], y sta [0], y ldy #1120 lda [4], y sta [0], y ldy #1122 lda [4], y sta [0], y .a8 .i8 rep #0x30 .a16 .i16 tsc clc adc #8 ; drop the {dst,src} D-frame tcs pld ; restore caller D plb plp rtl ; ==================================================================== ; iigsTilePasteInner(uint8_t *dst, const uint8_t *src) ; ; Paste a packed jlTileT buffer (4 bytes/row * 8 rows = 32 bytes tight) ; onto a surface row at `dst`. Src stride 4, dst stride 160. Both args ; are 4-byte large-model pointers. ; ; ABI: identical to iigsTileCopyInner -- arg0 dst in A:X, arg1 src on ; the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD). D points ; at tileScratchPtrs; [0],y = dst, [4],y = src. ; ==================================================================== .section .text.iigsTilePasteInner,"ax" .globl iigsTilePasteInner iigsTilePasteInner: php phb phd ; save caller D rep #0x30 .a16 .i16 sta tileScratchPtrs+0 ; dst low16 stx tileScratchPtrs+2 ; dst high16 (bank + pad) lda 8,s ; src low16 sta tileScratchPtrs+4 lda 10,s ; src bank sta tileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via ; `tcd #tileScratchPtrs` indirected through the wrong bank. lda tileScratchPtrs+6 pha ; src bank -> 7,s lda tileScratchPtrs+4 pha ; src low16 -> 5,s lda tileScratchPtrs+2 pha ; dst high16 -> 3,s lda tileScratchPtrs+0 pha ; dst low16 -> 1,s tsc inc a ; A = SP+1 tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Row 0: src word 0 = 0, dst word 0 = 0 ldy #0 lda [4], y sta [0], y ldy #2 lda [4], y sta [0], y ; Row 1: src 4/6, dst 160/162 ldy #4 lda [4], y ldy #160 sta [0], y ldy #6 lda [4], y ldy #162 sta [0], y ; Row 2: src 8/10, dst 320/322 ldy #8 lda [4], y ldy #320 sta [0], y ldy #10 lda [4], y ldy #322 sta [0], y ; Row 3: src 12/14, dst 480/482 ldy #12 lda [4], y ldy #480 sta [0], y ldy #14 lda [4], y ldy #482 sta [0], y ; Row 4: src 16/18, dst 640/642 ldy #16 lda [4], y ldy #640 sta [0], y ldy #18 lda [4], y ldy #642 sta [0], y ; Row 5: src 20/22, dst 800/802 ldy #20 lda [4], y ldy #800 sta [0], y ldy #22 lda [4], y ldy #802 sta [0], y ; Row 6: src 24/26, dst 960/962 ldy #24 lda [4], y ldy #960 sta [0], y ldy #26 lda [4], y ldy #962 sta [0], y ; Row 7: src 28/30, dst 1120/1122 ldy #28 lda [4], y ldy #1120 sta [0], y ldy #30 lda [4], y ldy #1122 sta [0], y .a8 .i8 rep #0x30 .a16 .i16 tsc clc adc #8 ; drop the {dst,src} D-frame tcs pld ; restore caller D plb plp rtl ; ==================================================================== ; iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0) ; ; Snapshot an 8x8 region of a surface into a packed jlTileT buffer. ; Src stride 160, dst stride 4. Mirrors jlTilePaste with stride values ; swapped. ; ; ABI: arg0 dst (the packed tile buffer) in A:X, arg1 src (the surface ; row) on the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD). ; D points at tileScratchPtrs; [0],y = dst, [4],y = src. ; ==================================================================== .section .text.iigsTileSnapInner,"ax" .globl iigsTileSnapInner iigsTileSnapInner: php phb phd ; save caller D rep #0x30 .a16 .i16 sta tileScratchPtrs+0 ; dst low16 stx tileScratchPtrs+2 ; dst high16 (bank + pad) lda 8,s ; src low16 sta tileScratchPtrs+4 lda 10,s ; src bank sta tileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via ; `tcd #tileScratchPtrs` indirected through the wrong bank. lda tileScratchPtrs+6 pha ; src bank -> 7,s lda tileScratchPtrs+4 pha ; src low16 -> 5,s lda tileScratchPtrs+2 pha ; dst high16 -> 3,s lda tileScratchPtrs+0 pha ; dst low16 -> 1,s tsc inc a ; A = SP+1 tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Row 0: src 0/2, dst 0/2 ldy #0 lda [4], y sta [0], y ldy #2 lda [4], y sta [0], y ; Row 1: src 160/162, dst 4/6 ldy #160 lda [4], y ldy #4 sta [0], y ldy #162 lda [4], y ldy #6 sta [0], y ; Row 2: src 320/322, dst 8/10 ldy #320 lda [4], y ldy #8 sta [0], y ldy #322 lda [4], y ldy #10 sta [0], y ; Row 3: src 480/482, dst 12/14 ldy #480 lda [4], y ldy #12 sta [0], y ldy #482 lda [4], y ldy #14 sta [0], y ; Row 4: src 640/642, dst 16/18 ldy #640 lda [4], y ldy #16 sta [0], y ldy #642 lda [4], y ldy #18 sta [0], y ; Row 5: src 800/802, dst 20/22 ldy #800 lda [4], y ldy #20 sta [0], y ldy #802 lda [4], y ldy #22 sta [0], y ; Row 6: src 960/962, dst 24/26 ldy #960 lda [4], y ldy #24 sta [0], y ldy #962 lda [4], y ldy #26 sta [0], y ; Row 7: src 1120/1122, dst 28/30 ldy #1120 lda [4], y ldy #28 sta [0], y ldy #1122 lda [4], y ldy #30 sta [0], y .a8 .i8 rep #0x30 .a16 .i16 tsc clc adc #8 ; drop the {dst,src} D-frame tcs pld ; restore caller D plb plp rtl ; ==================================================================== ; iigsTileCopyMaskedInner(uint8_t *dst, const uint8_t *src, ; uint16_t transparent) ; ; Same shape as jlTileCopy (160/160 strides, 32 bytes), but source ; nibbles equal to `transparent` are skipped. Per-byte fast path: if ; both src nibbles == transparent the whole byte is skipped; otherwise ; build the output by mixing src/dst nibbles. ; ; ABI: arg0 dst in A:X. arg1 src (4-byte ptr) and arg2 transparent ; (word) on the stack. JSL entry: src @4,s, transparent @8,s. ; After PHP+PHB+PHD(+4): src low16 @8,s, src bank @10,s, ; transparent @12,s. The original read `trans` via the D-frame ; ([D+8]); here D points at tileScratchPtrs instead, so we read ; transparent's low byte from the stack (@12,s in M=8) during ; setup. [0],y = dst, [4],y = src in the per-byte subroutine. ; ==================================================================== .section .text.iigsTileCopyMaskedInner,"ax" .globl iigsTileCopyMaskedInner iigsTileCopyMaskedInner: php phb phd ; save caller D rep #0x30 .a16 .i16 ; Build the dst/src D-frame in bank-0 scratch. sta tileScratchPtrs+0 ; dst low16 stx tileScratchPtrs+2 ; dst high16 (bank + pad) lda 8,s ; src low16 sta tileScratchPtrs+4 lda 10,s ; src bank sta tileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via ; `tcd #tileScratchPtrs` indirected through the wrong bank. lda tileScratchPtrs+6 pha ; src bank -> 7,s lda tileScratchPtrs+4 pha ; src low16 -> 5,s lda tileScratchPtrs+2 pha ; dst high16 -> 3,s lda tileScratchPtrs+0 pha ; dst low16 -> 1,s tsc inc a ; A = SP+1 tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Pre-compute scratch values from `transparent`. ; transparent sits @12,s after the prologue; the 4-word stack ; D-frame pushed just above adds 8 more, so it is now @20,s. sep #0x20 ; M=8 .a8 lda 20,s ; transparent (low byte) and #0x0F sta tmaskTLo ; tLo = T asl a asl a asl a asl a sta tmaskTHi ; tHi = T<<4 ora tmaskTLo sta tmaskDoubled ; doubled = (T<<4)|T ; Walk 8 rows of 4 bytes. Y holds the byte offset; after ; the 3 in-row INYs Y is at row*160+3, so +157 lands at ; next row's start (row*160+160). ldx #8 ; row counter ldy #0 ; byte offset tmaskRowLoop: jsr tmaskByte ; byte 0 iny jsr tmaskByte ; byte 1 iny jsr tmaskByte ; byte 2 iny jsr tmaskByte ; byte 3 rep #0x20 ; M=16 for arithmetic on Y .a16 tya clc adc #157 ; +156 stride, +1 (we're at row+3) tay sep #0x20 ; back to M=8 .a8 dex bne tmaskRowLoop rep #0x20 ; M=16 before epilogue .a16 .i16 tsc clc adc #8 ; drop the {dst,src} D-frame tcs pld ; restore caller D plb plp rtl ; tmaskByte: in M=8 X=16, Y holds byte offset. ; - load src byte ; - if both nibbles transparent: skip ; - else: assemble output from src/dst nibbles per nibble's ; non-transparency, store tmaskByte: ; Entered (via JSR from the M=8 row loop) with M=8, X=16. ; Tell the assembler so the immediates below size as 1 ; byte. RTS returns to the still-M=8 row loop, so no ; width restore is needed here. .a8 .i16 lda [4], y ; A = src byte cmp tmaskDoubled beq tmaskSkip ; both nibbles == transparent sta tmaskSrc ; Decide hi nibble. and #0xF0 cmp tmaskTHi beq tmaskUseDstHi sta tmaskOutHi ; src hi (kept in $F0 form) bra tmaskHiDone tmaskUseDstHi: lda [0], y and #0xF0 sta tmaskOutHi tmaskHiDone: ; Decide lo nibble. lda tmaskSrc and #0x0F cmp tmaskTLo beq tmaskUseDstLo sta tmaskOutLo ; src lo bra tmaskLoDone tmaskUseDstLo: lda [0], y and #0x0F sta tmaskOutLo tmaskLoDone: lda tmaskOutHi ora tmaskOutLo sta [0], y tmaskSkip: rts ; -------------------------------------------------------------------- ; Scratch for the masked-tile inner loop. Lives outside the ; primitive's stack frame because the per-byte subroutine call would ; otherwise need to thread state through registers. (bank-0 globals) ; -------------------------------------------------------------------- .section .bss.tmaskTLo,"aw" .globl tmaskTLo tmaskTLo: .zero 1 .section .bss.tmaskTHi,"aw" .globl tmaskTHi tmaskTHi: .zero 1 .section .bss.tmaskDoubled,"aw" .globl tmaskDoubled tmaskDoubled: .zero 1 .section .bss.tmaskSrc,"aw" .globl tmaskSrc tmaskSrc: .zero 1 .section .bss.tmaskOutHi,"aw" .globl tmaskOutHi tmaskOutHi: .zero 1 .section .bss.tmaskOutLo,"aw" .globl tmaskOutLo tmaskOutLo: .zero 1 ; ===== ported: fillrect / pixel / line ===== ; ==================================================================== ; JoeyLib IIgs draw primitives (llvm-mc GAS, w65816) ; Functions: iigsFillRectInner, iigsDrawPixelInner, iigsDrawLineInner ; ==================================================================== ; ==================================================================== ; iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y, ; uint16_t w, uint16_t h, uint16_t nibble) ; ; Fill an axis-aligned rectangle of 4bpp pixels into the IIgs SHR ; stage at bank $01. FillRect hard-codes bank 1 and derives the row ; address from y via gRowOffsetLut, so the `pixels` arg (A:X) is unused. ; ; ABI: clang cdecl. pixels arrives in A:X (UNUSED). x,y,w,h,nibble are ; on the stack (right-to-left). On JSL entry: x@4,s y@6,s w@8,s h@10,s ; nib@12,s. After php+phb+phd (+4) and tsc+adc#8+tcd (D = SP+8), the ; equates are: ; fri_x=0 fri_y=2 fri_w=4 fri_h=6 fri_nib=8 ; Prologue (php/phb/phd, tsc+adc#8+tcd) otherwise unchanged. ; ==================================================================== .section .text.iigsFillRectInner,"ax" .globl iigsFillRectInner iigsFillRectInner: fri_x = 0 fri_y = 2 fri_w = 4 fri_h = 6 fri_nib = 8 php phb phd rep #0x30 .a16 .i16 tsc clc adc #8 tcd ; pxStart = x; pxEnd = x + w. lda fri_x sta friPxStart clc adc fri_w sta friPxEnd ; leadingByte = x >> 1; hasLeading = x & 1. lda fri_x lsr a sta friLeadingByte lda fri_x and #1 sta friHasLeading beq friStartReady lda friPxStart inc a sta friPxStart friStartReady: ; midStart = pxStart >> 1. lda friPxStart lsr a sta friMidStart ; spanLen = pxEnd - pxStart; hasTrailing = spanLen & 1; midBytes = spanLen >> 1. lda friPxEnd sec sbc friPxStart sta friMidBytes ; temporary hold spanLen and #1 sta friHasTrailing lda friMidBytes lsr a sta friMidBytes ; trailingByte = midStart + midBytes. lda friMidStart clc adc friMidBytes sta friTrailingByte ; nibble decoding: friNibLo = nib (low half), friNibHi = nib<<4 ; (high half), friDoubled = (nib<<4)|nib (full byte fill). sep #0x20 .a8 lda fri_nib and #0x0F sta friNibLo asl a asl a asl a asl a sta friNibHi ora friNibLo sta friDoubled rep #0x20 .a16 ; curRow = $2000 + y*160 (LUT-driven). lda fri_y asl a tax lda gRowOffsetLut,x clc adc #0x2000 sta friCurRow lda fri_h sta friRowsLeft ; friFillWord = friDoubled replicated into both bytes (16-bit pattern). lda friDoubled and #0x00FF sta friFillWord xba ; A = byte in high half ora friFillWord sta friFillWord friRowLoop: lda friRowsLeft bne friDoRow brl friExit friDoRow: ; Leading partial-byte RMW. lda friHasLeading beq friNoLead lda friCurRow clc adc friLeadingByte tax sep #0x20 .a8 lda 0x010000,x and #0xF0 ora friNibLo sta 0x010000,x rep #0x20 .a16 friNoLead: ; Middle bytes: if midBytes == 0, skip; if midBytes == 1, just seed ; one byte; if midBytes >= 2, seed first byte then MVN propagates the ; seed across the rest of the row. lda friMidBytes bne friMidNonZero brl friNoMid friMidNonZero: ; Computed-jump slam for ANY middle width. nWords = midBytes>>1 words are ; slammed through the unrolled STA long,X table by entering it at store ; #(80-nWords), with X biased so those stores cover ; [curRow+midStart .. +2*nWords-1] at 3 cyc/byte. Replaces the old ; friFullWidth-only slam plus seed+MVN-for-partials (7 cyc/byte). The entry ; is reached with an RTS computed-goto: it stays in the program bank and ; uses the normal stack, so there is no bank-0 indirect-vector hazard. ; nWords==0 (a 1-byte middle) lands just past the table on the odd-byte ; writer. An odd trailing middle byte is written after the table. lda friMidBytes lsr a sta friSlamWords ; nWords = midBytes >> 1 ; Push (entry - 1); entry = friMidSlamTbl + (80 - nWords) * 4 (each store is 4 bytes). lda #80 sec sbc friSlamWords asl a asl a clc adc #friMidSlamTbl-1 pha ; X = curRow + midStart + 2*nWords - 160, so store #(80-nWords) hits curRow+midStart. lda friSlamWords asl a clc adc friCurRow clc adc friMidStart sec sbc #160 tax lda friFillWord rts ; computed-goto into friMidSlamTbl friMidSlamTbl: sta 0x010000,x sta 0x010002,x sta 0x010004,x sta 0x010006,x sta 0x010008,x sta 0x01000A,x sta 0x01000C,x sta 0x01000E,x sta 0x010010,x sta 0x010012,x sta 0x010014,x sta 0x010016,x sta 0x010018,x sta 0x01001A,x sta 0x01001C,x sta 0x01001E,x sta 0x010020,x sta 0x010022,x sta 0x010024,x sta 0x010026,x sta 0x010028,x sta 0x01002A,x sta 0x01002C,x sta 0x01002E,x sta 0x010030,x sta 0x010032,x sta 0x010034,x sta 0x010036,x sta 0x010038,x sta 0x01003A,x sta 0x01003C,x sta 0x01003E,x sta 0x010040,x sta 0x010042,x sta 0x010044,x sta 0x010046,x sta 0x010048,x sta 0x01004A,x sta 0x01004C,x sta 0x01004E,x sta 0x010050,x sta 0x010052,x sta 0x010054,x sta 0x010056,x sta 0x010058,x sta 0x01005A,x sta 0x01005C,x sta 0x01005E,x sta 0x010060,x sta 0x010062,x sta 0x010064,x sta 0x010066,x sta 0x010068,x sta 0x01006A,x sta 0x01006C,x sta 0x01006E,x sta 0x010070,x sta 0x010072,x sta 0x010074,x sta 0x010076,x sta 0x010078,x sta 0x01007A,x sta 0x01007C,x sta 0x01007E,x sta 0x010080,x sta 0x010082,x sta 0x010084,x sta 0x010086,x sta 0x010088,x sta 0x01008A,x sta 0x01008C,x sta 0x01008E,x sta 0x010090,x sta 0x010092,x sta 0x010094,x sta 0x010096,x sta 0x010098,x sta 0x01009A,x sta 0x01009C,x sta 0x01009E,x ; Odd trailing middle byte: the slam wrote 2*nWords bytes; if midBytes is ; odd, write the last middle byte at curRow + midStart + 2*nWords. lda friMidBytes and #1 beq friMidNoOdd lda friSlamWords asl a clc adc friCurRow clc adc friMidStart tax sep #0x20 .a8 lda friDoubled sta 0x010000,x rep #0x20 .a16 friMidNoOdd: friNoMid: ; Trailing partial-byte RMW. lda friHasTrailing beq friNoTrail lda friCurRow clc adc friTrailingByte tax sep #0x20 .a8 lda 0x010000,x and #0x0F ora friNibHi sta 0x010000,x rep #0x20 .a16 friNoTrail: ; Advance to next row. lda friCurRow clc adc #160 sta friCurRow lda friRowsLeft dec a sta friRowsLeft brl friRowLoop friExit: .a8 .i8 pld plb plp rtl .section .bss.friPxStart,"aw" .globl friPxStart friPxStart: .zero 2 .section .bss.friPxEnd,"aw" .globl friPxEnd friPxEnd: .zero 2 .section .bss.friLeadingByte,"aw" .globl friLeadingByte friLeadingByte: .zero 2 .section .bss.friHasLeading,"aw" .globl friHasLeading friHasLeading: .zero 2 .section .bss.friMidStart,"aw" .globl friMidStart friMidStart: .zero 2 .section .bss.friMidBytes,"aw" .globl friMidBytes friMidBytes: .zero 2 .section .bss.friHasTrailing,"aw" .globl friHasTrailing friHasTrailing: .zero 2 .section .bss.friTrailingByte,"aw" .globl friTrailingByte friTrailingByte: .zero 2 .section .bss.friNibLo,"aw" .globl friNibLo friNibLo: .zero 2 .section .bss.friNibHi,"aw" .globl friNibHi friNibHi: .zero 2 .section .bss.friDoubled,"aw" .globl friDoubled friDoubled: .zero 2 .section .bss.friCurRow,"aw" .globl friCurRow friCurRow: .zero 2 .section .bss.friRowsLeft,"aw" .globl friRowsLeft friRowsLeft: .zero 2 .section .bss.friFillWord,"aw" .globl friFillWord friFillWord: .zero 2 .section .bss.friSlamWords,"aw" .globl friSlamWords friSlamWords: .zero 2 ; ==================================================================== ; iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, ; uint16_t nibble) ; ; Plot one 4bpp pixel at (x,y) into a 320x200 surface. Caller has ; already validated bounds and stripped the high nibbles of `nibble` ; (only low 4 bits used). pixels is a 4-byte large-model pointer that ; IS dereferenced via DP-indirect-long. ; ; Offset math: byte = y*160 + (x>>1). Nibble half: x&1 == 0 picks high ; nibble (left pixel), else low. ; ; ABI: clang cdecl. pixels arrives in A:X; x,y,nibble are on the stack. ; llvm-mc only encodes DP-indirect-long ; ([dp],y) for an operand that fits the direct page, so we cannot use a ; 16-bit bss symbol as the pointer slot. Instead, faithful to the spec's ; "pointer arg0 that IS used" recipe, we repoint D at a bank-0 scratch ; struct (dpxlScratch): store pixels (A:X) at dpxlScratch+0..2 and use ; [pix],y with pix=0. Because D no longer overlays the stack frame, the ; x,y,nibble args are read stack-relative. ; Prologue pushes: php(+1)+phb(+1)+phd(+2)=+4. On JSL entry x@4,s y@6,s ; nib@8,s; after the 4 pushes: x@8,s y@10,s nib@12,s. ; pix=0 (D-relative, = dpxlScratch+0) ; xx=8,s yy=10,s nib=12,s ; pix is a 3-byte far pointer (A low16 + X bank byte); we store A then X ; as words -> a harmless 4th filler byte in the 4-byte scratch. ; ==================================================================== .section .text.iigsDrawPixelInner,"ax" .globl iigsDrawPixelInner iigsDrawPixelInner: pix = 0 ; pixels far ptr (D+0..2 = dpxlScratch) php phb phd rep #0x30 .a16 .i16 ; Stash the pixels far pointer into the DP scratch via D-RELATIVE stores ; (AFTER tcd): a plain `sta dpxlScratch` writes the LOAD bank (DBR) while ; [pix],y reads through D, which is ALWAYS bank 0 -- the two disagreed and the ; plot pointer was garbage. phx/pha preserve pix (A:X survive php/phb/phd) ; across the `lda #dpxlScratch` that clobbers A; pulled back so the N,s arg ; offsets below keep their values. phx ; save pix bank pha ; save pix offset lda #dpxlScratch tcd ; D = dpxlScratch (bank 0); [pix],y == [0],y pla ; pix offset sta pix+0 ; D-relative -> bank0:dpxlScratch+0 pla ; pix bank sta pix+2 ; D-relative -> bank0:dpxlScratch+2 ; Compute byte offset = y*160 + (x>>1) into A. Use the LUT. lda 10,s ; y asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut,x ; A = y*160 sta dpxlTmp lda 8,s ; x lsr a ; A = x >> 1 clc adc dpxlTmp ; A = byte offset within surface tay sep #0x20 ; M=8 .a8 lda 8,s ; A = x low byte (parity in LSB) and #1 bne dpxlOdd ; Even x -> high nibble. lda 12,s ; nibble asl a asl a asl a asl a sta dpxlNibPart lda [pix],y and #0x0F ora dpxlNibPart sta [pix],y bra dpxlDone dpxlOdd: ; Odd x -> low nibble. lda 12,s ; nibble and #0x0F sta dpxlNibPart lda [pix],y and #0xF0 ora dpxlNibPart sta [pix],y dpxlDone: rep #0x20 ; back to M=16 for epilogue .a16 .a8 .i8 pld plb plp rtl .section .bss.dpxlTmp,"aw" .globl dpxlTmp dpxlTmp: .zero 2 .section .bss.dpxlNibPart,"aw" .globl dpxlNibPart dpxlNibPart: .zero 1 .section .bss.dpxlScratch,"aw" .globl dpxlScratch dpxlScratch: .zero 4 ; ==================================================================== ; iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, ; uint16_t x1, uint16_t y1, uint16_t nibble) ; ; Bresenham line plot. Caller has clipped both endpoints into ; [0..319] x [0..199], so no per-pixel bounds check. ; ; Walks (dx, dy, sx, sy, err) and plots inline (no per-pixel ; function call). Each plot does the same parity-aware nibble RMW as ; iigsDrawPixel. State stored in DRAWPRIMS scratch because registers ; are too few for the full Bresenham state plus current pixel address. ; ; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [pix],y); ; x0,y0,x1,y1,nibble on the stack. Same hybrid as DrawPixel: llvm-mc ; cannot encode [dp],y from a 16-bit bss symbol, so we repoint D at a ; bank-0 scratch struct (dlnScratch), stash pixels (A:X) at ; dlnScratch+0..2, and use [pix],y with pix=0 in the loop. The five ; word args are copied into DRAWPRIMS scratch up front (stack-relative), ; so the loop never touches the stack frame again. ; Prologue pushes: php(+1)+phb(+1)+phd(+2)=+4. On JSL entry x0@4,s ; y0@6,s x1@8,s y1@10,s nib@12,s; after the 4 pushes: ; x0@8,s y0@10,s x1@12,s y1@14,s nib@16,s ; pix=0 (D-relative, = dlnScratch+0) ; ==================================================================== .section .text.iigsDrawLineInner,"ax" .globl iigsDrawLineInner iigsDrawLineInner: pix = 0 ; pixels far ptr (D+0..2 = dlnScratch) ; Stash pixels far pointer (A:X) into bank-0 scratch before any push. rep #0x30 .a16 .i16 sta dlnScratch ; low 16 bits of pixels stx dlnScratch+2 ; bank byte (+ filler) php phb phd rep #0x30 .a16 .i16 ; Repoint D at the scratch struct so [pix],y (pix=0) is DP-indirect-long. lda #dlnScratch tcd ; dx = |x1 - x0|, sx = sign(x1 - x0) lda 12,s ; x1 sec sbc 8,s ; x0 bpl dlnDxPos eor #0xFFFF clc adc #1 ; A = x0 - x1 (positive |dx|) sta dlnDx lda #0xFFFF sta dlnSx ; sx = -1 bra dlnDxDone dlnDxPos: sta dlnDx lda #1 sta dlnSx dlnDxDone: ; dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy. lda 14,s ; y1 sec sbc 10,s ; y0 bpl dlnDyPos sta dlnDy ; A is negative; that's dy with the sign already lda #0xFFFF sta dlnSy ; sy = -1 bra dlnDyDone dlnDyPos: ; y1 >= y0 -> raw dy positive; negate for Bresenham (-|y1-y0|). eor #0xFFFF clc adc #1 sta dlnDy lda #1 sta dlnSy dlnDyDone: ; err = dx + dy lda dlnDx clc adc dlnDy sta dlnErr ; Copy x0, y0 to running state slots. lda 8,s ; x0 sta dlnX lda 10,s ; y0 sta dlnY ; Cache x1, y1 so the loop's compare is fast. lda 12,s ; x1 sta dlnX1 lda 14,s ; y1 sta dlnY1 ; Precompute both 16-bit nibble OR-values ONCE (stays in M=16 the ; whole loop -- no per-pixel SEP/REP flip). The nibble RMW reads the ; target byte plus its adjacent high byte (16-bit), masks so only the ; target nibble changes, and writes both bytes back; the high byte is ; preserved by the mask + zero OR-high-byte, so the result is identical ; to the old 8-bit single-byte RMW. dlnNibHi/dlnNibLo have low byte = ; (nib<<4) / nib and high byte = 0. lda 16,s ; nibble and #0x000F sta dlnNibLo ; low byte = nib, high byte = 0 asl a asl a asl a asl a sta dlnNibHi ; low byte = nib<<4, high byte = 0 dlnLoop: ; --- Plot pixel at (dlnX, dlnY) --- LUT path. Stays in M=16. lda dlnY asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut,x ; A = y*160 sta dlnTmp lda dlnX lsr a ; x >> 1 clc adc dlnTmp ; byte offset tay lda dlnX ; x parity and #1 bne dlnPlotOdd ; Even x -> high nibble. Mask keeps low nibble of target byte and the ; whole adjacent high byte; OR sets nib<<4 into the high nibble. lda [pix],y and #0xFF0F ora dlnNibHi sta [pix],y bra dlnPlotDone dlnPlotOdd: ; Odd x -> low nibble. Mask keeps high nibble of target byte and the ; whole adjacent high byte; OR sets nib into the low nibble. lda [pix],y and #0xF0FF ora dlnNibLo sta [pix],y dlnPlotDone: ; --- Loop test: if X==X1 and Y==Y1, done --- lda dlnX cmp dlnX1 bne dlnStep lda dlnY cmp dlnY1 bne dlnStep brl dlnExit dlnStep: ; e2 = err << 1 (signed). Compare e2 against dy and dx. lda dlnErr asl a ; e2 = err * 2 sta dlnE2 ; if (e2 >= dy) { err += dy; X += sx; } ; dy is negative, so signed-compare with BMI/BPL needed. ; Test e2 - dy >= 0 via SEC; SBC dy; BPL. sec sbc dlnDy bmi dlnSkipX ; e2 < dy lda dlnErr clc adc dlnDy sta dlnErr lda dlnX clc adc dlnSx sta dlnX dlnSkipX: ; if (e2 <= dx) { err += dx; Y += sy; } ; Test dx - e2 >= 0 via dx - e2 (BPL means e2 <= dx). lda dlnDx sec sbc dlnE2 bmi dlnSkipY ; dx < e2 lda dlnErr clc adc dlnDx sta dlnErr lda dlnY clc adc dlnSy sta dlnY dlnSkipY: brl dlnLoop dlnExit: .a8 .i8 pld plb plp rtl .section .bss.dlnDx,"aw" .globl dlnDx dlnDx: .zero 2 .section .bss.dlnDy,"aw" .globl dlnDy dlnDy: .zero 2 .section .bss.dlnSx,"aw" .globl dlnSx dlnSx: .zero 2 .section .bss.dlnSy,"aw" .globl dlnSy dlnSy: .zero 2 .section .bss.dlnErr,"aw" .globl dlnErr dlnErr: .zero 2 .section .bss.dlnE2,"aw" .globl dlnE2 dlnE2: .zero 2 .section .bss.dlnX,"aw" .globl dlnX dlnX: .zero 2 .section .bss.dlnY,"aw" .globl dlnY dlnY: .zero 2 .section .bss.dlnX1,"aw" .globl dlnX1 dlnX1: .zero 2 .section .bss.dlnY1,"aw" .globl dlnY1 dlnY1: .zero 2 .section .bss.dlnNibLo,"aw" .globl dlnNibLo dlnNibLo: .zero 2 .section .bss.dlnNibHi,"aw" .globl dlnNibHi dlnNibHi: .zero 2 .section .bss.dlnTmp,"aw" .globl dlnTmp dlnTmp: .zero 2 .section .bss.dlnScratch,"aw" .globl dlnScratch dlnScratch: .zero 4 ; ===== ported: circles ===== ; ================================================================ ; iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, ; uint16_t r, uint16_t nibble) ; ; Bresenham midpoint circle outline. Caller has verified that the ; whole bounding box (cx-r..cx+r, cy-r..cy+r) fits inside the ; surface, so the inner loop plots all 8 octants without per-pixel ; clip checks. Each iteration computes 4 row-base byte offsets ; (cy +/- y)*160, (cy +/- x)*160 then plots 8 pixels inline. ; ; ABI (llvm-mos w65816 cdecl): ; pix -> arg0 in A:X (A=offset low16, X low byte=bank). USED via ; [pix],y, so it must live in the direct page. We stash it ; into bank-0 scratch dcPixPtr (offset@+0, bank@+2) BEFORE ; setting D, then point D at dcPixPtr so the original ; `[pix],y` (pix equ 0 -> [D+0],y) loads are byte-identical. ; php/phb/phd do not touch A/X, so A:X still hold pix when ; we capture them after the pushes. ; cx,cy,r,nibble -> on the stack. On JSL entry cx@4,s cy@6,s r@8,s ; nib@10,s. After php(+1)+phb(+1)+phd(+2)=+4: cx@8,s ; cy@10,s r@12,s nib@14,s. S never moves in the body ; (no pushes in the loop), so these N,s reads stay valid. ; They are stack reads because D hosts the pix pointer. ; ================================================================ .section .text.iigsDrawCircleInner,"ax" .globl iigsDrawCircleInner pix = 0 ; D-relative: D points at dcPixPtr dcArgCx = 8 ; cx @ 8,s after +4 prologue dcArgCy = 10 ; cy @ 10,s dcArgR = 12 ; r @ 12,s dcArgNib = 14 ; nibble @ 14,s iigsDrawCircleInner: php phb phd rep #0x30 .a16 .i16 ; Stash the pix far pointer (still in A:X) into bank-0 scratch and ; point D at it so [pix],y works unchanged. ; Store pix into the DP scratch via D-RELATIVE stores (AFTER tcd): a ; plain `sta dcPixPtr` writes the LOAD bank (DBR) while [pix],y reads ; through D, which is ALWAYS bank 0 -- the two disagreed, so the plot ; pointer was garbage and nothing drew. phx/pha preserve pix across the ; `lda #dcPixPtr` that clobbers A, then are pulled back so the N,s arg ; offsets below do not shift. phx ; save pix bank pha ; save pix offset lda #dcPixPtr tcd ; D = &dcPixPtr (bank 0); [pix],y == [0],y pla ; pix offset sta pix+0 ; D-relative -> bank0:dcPixPtr+0 pla ; pix bank sta pix+2 ; D-relative -> bank0:dcPixPtr+2 ; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4. lda dcArgNib, s and #0x000F sta dcNibLo ; low byte = nib, high byte = 0 asl a asl a asl a asl a sta dcNibHi ; low byte = nib<<4 ; x = r, y = 0, err = 1 - x. lda dcArgR, s sta dcX lda #0 sta dcY lda #1 sec sbc dcArgR, s sta dcErr ; Initialize the 4 octant row bases once, then maintain them ; incrementally in the Bresenham update (instead of 4 LUT lookups per ; iteration). At entry x=r, y=0: RowYP=RowYN=acy*160, ; RowXP=(acy+r)*160, RowXN=(acy-r)*160. lda dcArgCy, s asl a tax lda gRowOffsetLut, x sta dcRowYP sta dcRowYN lda dcArgCy, s clc adc dcArgR, s asl a tax lda gRowOffsetLut, x sta dcRowXP lda dcArgCy, s sec sbc dcArgR, s asl a tax lda gRowOffsetLut, x sta dcRowXN dcLoop: ; Loop guard: if x < y we are done. lda dcX cmp dcY bcs dcLoopBody ; x >= y -> continue brl dcExit dcLoopBody: ; Row bases (dcRowYP/YN/XP/XN) are maintained incrementally in the ; Bresenham update -- no per-iteration LUT lookups here. ; ; 8 octant plots, fully inlined. Each plot: ; 1. col = (acx +/- dcX|dcY) -> A ; 2. stash col in X (free across the plots); the parity test reads it ; back with txa instead of a memory round-trip. ; 3. byteIdx = col >> 1; byte addr = byteIdx + rowBase -> Y ; 4. test col & 1 (via txa); do high or low nibble RMW ; ; All 8 octant plots stay in M=16 -- no per-plot SEP/REP flip. The ; nibble RMW reads the target byte plus its adjacent high byte (16-bit ; load), masks so only the target nibble changes ($FF0F keeps low ; nibble + whole high byte for even/high-nibble x, $F0FF keeps high ; nibble + whole high byte for odd/low-nibble x), then writes both ; bytes back. ; Octants 1-4: y-row pair (cx +/- x, cy +/- y). ; Plot 1: (cx+x, cy+y) lda dcArgCx, s clc adc dcX tax lsr a clc adc dcRowYP tay txa and #1 bne dcOdd1 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone1 dcOdd1: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone1: ; Plot 2: (cx-x, cy+y) lda dcArgCx, s sec sbc dcX tax lsr a clc adc dcRowYP tay txa and #1 bne dcOdd2 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone2 dcOdd2: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone2: ; Plot 3: (cx+x, cy-y) lda dcArgCx, s clc adc dcX tax lsr a clc adc dcRowYN tay txa and #1 bne dcOdd3 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone3 dcOdd3: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone3: ; Plot 4: (cx-x, cy-y) lda dcArgCx, s sec sbc dcX tax lsr a clc adc dcRowYN tay txa and #1 bne dcOdd4 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone4 dcOdd4: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone4: ; Octants 5-8: x-row pair (cx +/- y, cy +/- x). ; Plot 5: (cx+y, cy+x) lda dcArgCx, s clc adc dcY tax lsr a clc adc dcRowXP tay txa and #1 bne dcOdd5 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone5 dcOdd5: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone5: ; Plot 6: (cx-y, cy+x) lda dcArgCx, s sec sbc dcY tax lsr a clc adc dcRowXP tay txa and #1 bne dcOdd6 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone6 dcOdd6: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone6: ; Plot 7: (cx+y, cy-x) lda dcArgCx, s clc adc dcY tax lsr a clc adc dcRowXN tay txa and #1 bne dcOdd7 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone7 dcOdd7: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone7: ; Plot 8: (cx-y, cy-x) lda dcArgCx, s sec sbc dcY tax lsr a clc adc dcRowXN tay txa and #1 bne dcOdd8 lda [pix], y and #0xFF0F ora dcNibHi sta [pix], y bra dcDone8 dcOdd8: lda [pix], y and #0xF0FF ora dcNibLo sta [pix], y dcDone8: ; Update Bresenham: y++; if err<=0: err+=2y+1; else x--; err+=2(y-x)+1. lda dcY inc a sta dcY ; y now = old y + 1 ; y increased by 1: RowYP moves down a row (+160), RowYN moves up (-160). lda dcRowYP clc adc #160 sta dcRowYP lda dcRowYN sec sbc #160 sta dcRowYN lda dcErr bmi dcErrLE ; err < 0 -> err <= 0 bne dcErrGT ; err > 0 ; err == 0: take the LE branch (err += 2y+1). dcErrLE: lda dcY asl a clc adc #1 clc adc dcErr sta dcErr brl dcLoop dcErrGT: lda dcX dec a sta dcX ; x decreased by 1: RowXP moves up a row (-160), RowXN moves down (+160). lda dcRowXP sec sbc #160 sta dcRowXP lda dcRowXN clc adc #160 sta dcRowXN ; err += 2*(y-x) + 1 lda dcY sec sbc dcX asl a clc adc #1 clc adc dcErr sta dcErr brl dcLoop dcExit: .a8 .i8 pld plb plp rtl ; ================================================================ ; iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, ; uint16_t r, uint16_t fillWord) ; ; Filled circle with horizontal-span scanline output. Caller has ; verified the bbox fits inside the surface, so the inner loop fills ; every span unconditionally. Maintains xx=x*x and yy=y*y ; incrementally so the hot path uses only 16-bit add/sub/cmp -- no ; 65816 multiply at all (other than one r*r at setup). ; ; fillWord is the doubled byte (low byte) replicated, e.g. nibble 3 ; -> $3333 (we only use the low byte for nibble RMW). ; ; ABI (llvm-mos w65816 cdecl): ; pix -> arg0 in A:X. USED via [fpix],y, so stashed into bank-0 ; scratch fcPixPtr (offset@+0, bank@+2) and D pointed at ; it; fpix equ 0 -> [D+0],y. ; cx,cy,r,fillWord -> on the stack. JSL entry cx@4,s cy@6,s r@8,s ; fill@10,s. After php+phb+phd (+4): cx@8,s cy@10,s ; r@12,s fill@14,s. S is constant in the outer body; the ; jsr-called helpers (fcMul16/fcDoSpan) only touch ; scratch globals + [fpix],y, never N,s, so the inner ; jsr-pushed return PC never disturbs an arg read. ; ================================================================ .section .text.iigsFillCircleInner,"ax" .globl iigsFillCircleInner fpix = 0 ; D-relative: D points at fcPixPtr fcArgCx = 8 ; cx @ 8,s after +4 prologue fcArgCy = 10 ; cy @ 10,s fcArgR = 12 ; r @ 12,s fcArgFill = 14 ; fillWord @ 14,s iigsFillCircleInner: php phb phd rep #0x30 .a16 .i16 ; Stash the pix far pointer (A:X) into bank-0 scratch and point D at it. sta fcPixPtr ; offset low 16 bits txa sta fcPixPtr+2 ; low byte = bank lda #fcPixPtr tcd ; D = &fcPixPtr ; [fpix],y == [0],y ; Cache fill nibble bytes. ffill low byte = doubled byte (e.g., $33). ; fcFillByte = doubled low byte (full byte fills + low nibble). ; fcFillHi = (nibble << 4) only (high nibble RMW). ; fcFillLo = nibble only (low nibble RMW). lda fcArgFill, s and #0x00FF sta fcFillByte ; doubled byte e.g. $33 lsr a lsr a lsr a lsr a ; A = nibble sta fcFillLo asl a asl a asl a asl a ; A = nibble << 4 sta fcFillHi ; Compute r*r via 16-bit shift-and-add. Only done once. lda fcArgR, s sta fcMulA lda fcArgR, s sta fcMulB jsr fcMul16 sta fcR2 sta fcXX ; xx = r*r initially ; yy = 0, x = r, y = 0. lda #0 sta fcYY sta fcY lda fcArgR, s sta fcX fcOuterLoop: ; Inner: while (xx + yy) > r2 then xx -= 2x - 1; x--. fcInnerLoop: lda fcXX clc adc fcYY cmp fcR2 bcc fcInnerDone ; xx+yy < r2 beq fcInnerDone ; xx+yy == r2 (acceptable) ; xx + yy > r2 -> decrement lda fcX asl a ; A = 2x sec sbc #1 ; A = 2x - 1 sta fcDec lda fcXX sec sbc fcDec sta fcXX lda fcX dec a sta fcX bra fcInnerLoop fcInnerDone: ; leftCol = cx - x, rightCol = cx + x for both lower and (if y>0) upper. lda fcArgCx, s sec sbc fcX sta fcLeftCol lda fcArgCx, s clc adc fcX sta fcRightCol ; Lower row: rowBase = (cy + y) * 160 lda fcArgCy, s clc adc fcY asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut, x ; A = y*160 sta fcSpanRowBase jsr fcDoSpan ; If y > 0: upper row at (cy - y). lda fcY beq fcSkipUpper lda fcArgCy, s sec sbc fcY asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut, x ; A = y*160 sta fcSpanRowBase jsr fcDoSpan fcSkipUpper: ; yy += 2*y + 1 lda fcY asl a clc adc #1 clc adc fcYY sta fcYY ; y++ lda fcY inc a sta fcY ; if y > r: exit, else loop cmp fcArgR, s bcc fcContinue beq fcContinue brl fcExit fcContinue: brl fcOuterLoop fcExit: .a8 .i8 pld plb plp rtl ; ---------------------------------------------------------------- ; fcMul16: A = fcMulA * fcMulB (16-bit unsigned, low 16 of result). ; Standard shift-and-add. 16 iterations. Caller has M=16, X=16. ; ---------------------------------------------------------------- fcMul16: .a16 .i16 lda #0 sta fcMulRes ldx #16 fcMulLoop: ; result <<= 1 lda fcMulRes asl a sta fcMulRes ; multiplier <<= 1, MSB -> C lda fcMulA asl a sta fcMulA bcc fcMulSkip lda fcMulRes clc adc fcMulB sta fcMulRes fcMulSkip: dex bne fcMulLoop lda fcMulRes rts ; ---------------------------------------------------------------- ; fcDoSpan: fill horizontal span from fcLeftCol to fcRightCol on row ; whose byte base is in fcSpanRowBase. Byte-aligned fill for the ; middle bytes and per-pixel RMW for the partial-nibble leading / ; trailing bytes. ; ; Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values). ; Trashes A, X, Y, P. Preserves D and B (relies on D = &fcPixPtr). ; ---------------------------------------------------------------- fcDoSpan: .a16 .i16 ; leftByte = leftCol >> 1, leftPartial = leftCol & 1 lda fcLeftCol lsr a sta fcLeftByte lda fcLeftCol and #1 sta fcLeftPartial ; rightByte = rightCol >> 1, rightPartial = !(rightCol & 1) lda fcRightCol lsr a sta fcRightByte lda fcRightCol and #1 eor #1 sta fcRightPartial ; If leftByte == rightByte: single-byte case lda fcLeftByte cmp fcRightByte bne fcSpanMulti brl fcSpanSingle fcSpanMulti: ; Multi-byte case. ; Leading partial (low nibble of leftByte) if leftPartial. lda fcLeftPartial beq fcSkipLP lda fcSpanRowBase clc adc fcLeftByte tay sep #0x20 .a8 lda [fpix], y and #0xF0 ora fcFillLo sta [fpix], y rep #0x20 .a16 fcSkipLP: ; Trailing partial (high nibble of rightByte) if rightPartial. lda fcRightPartial beq fcSkipRP lda fcSpanRowBase clc adc fcRightByte tay sep #0x20 .a8 lda [fpix], y and #0x0F ora fcFillHi sta [fpix], y rep #0x20 .a16 fcSkipRP: ; Mid bytes: (leftByte+leftPartial) .. (rightByte-rightPartial) incl. lda fcLeftByte clc adc fcLeftPartial sta fcMidStart lda fcRightByte sec sbc fcRightPartial sta fcMidEnd cmp fcMidStart bcs fcMidGo rts ; midEnd < midStart fcMidGo: lda fcMidEnd sec sbc fcMidStart inc a sta fcMidCount lda fcSpanRowBase clc adc fcMidStart tay ; Y = byte offset lda fcMidCount tax ; X = byte counter sep #0x20 .a8 lda fcFillByte ; doubled byte fcFillLoop: sta [fpix], y iny dex bne fcFillLoop rep #0x20 .a16 rts fcSpanSingle: ; Single-byte case: leftByte == rightByte. Three sub-cases: ; leftPartial=1 -> low nibble only ; rightPartial=1 -> high nibble only ; neither -> full byte (two adjacent pixels) lda fcSpanRowBase clc adc fcLeftByte tay sep #0x20 .a8 lda fcLeftPartial beq fcSbCheckRP lda [fpix], y and #0xF0 ora fcFillLo sta [fpix], y rep #0x20 .a16 rts fcSbCheckRP: .a8 ; reached via BEQ from M=8 path lda fcRightPartial beq fcSbFull lda [fpix], y and #0x0F ora fcFillHi sta [fpix], y rep #0x20 .a16 rts fcSbFull: .a8 ; reached via BEQ from M=8 path lda fcFillByte sta [fpix], y rep #0x20 .a16 rts ; ================================================================ ; Scratch slots (bank-0 BSS). dcSavedCol is kept for byte-faithful ; parity with the original DRAWPRIMS layout even though the inlined ; plots no longer round-trip through it. dcPixPtr / fcPixPtr hold the ; stashed arg0 far pointer for the [pix],y / [fpix],y direct-page ; indirect-long loads. ; ================================================================ .section .bss.dcPixPtr,"aw" .globl dcPixPtr dcPixPtr: .zero 4 .section .bss.dcX,"aw" .globl dcX dcX: .zero 2 .section .bss.dcY,"aw" .globl dcY dcY: .zero 2 .section .bss.dcErr,"aw" .globl dcErr dcErr: .zero 2 .section .bss.dcNibLo,"aw" .globl dcNibLo dcNibLo: .zero 2 .section .bss.dcNibHi,"aw" .globl dcNibHi dcNibHi: .zero 2 .section .bss.dcRowYP,"aw" .globl dcRowYP dcRowYP: .zero 2 .section .bss.dcRowYN,"aw" .globl dcRowYN dcRowYN: .zero 2 .section .bss.dcRowXP,"aw" .globl dcRowXP dcRowXP: .zero 2 .section .bss.dcRowXN,"aw" .globl dcRowXN dcRowXN: .zero 2 .section .bss.dcSavedCol,"aw" .globl dcSavedCol dcSavedCol: .zero 2 .section .bss.fcPixPtr,"aw" .globl fcPixPtr fcPixPtr: .zero 4 .section .bss.fcX,"aw" .globl fcX fcX: .zero 2 .section .bss.fcY,"aw" .globl fcY fcY: .zero 2 .section .bss.fcXX,"aw" .globl fcXX fcXX: .zero 2 .section .bss.fcYY,"aw" .globl fcYY fcYY: .zero 2 .section .bss.fcR2,"aw" .globl fcR2 fcR2: .zero 2 .section .bss.fcDec,"aw" .globl fcDec fcDec: .zero 2 .section .bss.fcMulA,"aw" .globl fcMulA fcMulA: .zero 2 .section .bss.fcMulB,"aw" .globl fcMulB fcMulB: .zero 2 .section .bss.fcMulRes,"aw" .globl fcMulRes fcMulRes: .zero 2 .section .bss.fcLeftCol,"aw" .globl fcLeftCol fcLeftCol: .zero 2 .section .bss.fcRightCol,"aw" .globl fcRightCol fcRightCol: .zero 2 .section .bss.fcSpanRowBase,"aw" .globl fcSpanRowBase fcSpanRowBase: .zero 2 .section .bss.fcLeftByte,"aw" .globl fcLeftByte fcLeftByte: .zero 2 .section .bss.fcRightByte,"aw" .globl fcRightByte fcRightByte: .zero 2 .section .bss.fcLeftPartial,"aw" .globl fcLeftPartial fcLeftPartial: .zero 2 .section .bss.fcRightPartial,"aw" .globl fcRightPartial fcRightPartial: .zero 2 .section .bss.fcMidStart,"aw" .globl fcMidStart fcMidStart: .zero 2 .section .bss.fcMidEnd,"aw" .globl fcMidEnd fcMidEnd: .zero 2 .section .bss.fcMidCount,"aw" .globl fcMidCount fcMidCount: .zero 2 .section .bss.fcFillByte,"aw" .globl fcFillByte fcFillByte: .zero 2 .section .bss.fcFillLo,"aw" .globl fcFillLo fcFillLo: .zero 2 .section .bss.fcFillHi,"aw" .globl fcFillHi fcFillHi: .zero 2 ; ===== ported: blit / dirty / input / flood ===== ; ==================================================================== ; frag_blitflood.s - llvm-mc GAS (w65816): ; iigsBlitStageToShr, iigsMarkDirtyRowsInner, iigsPollJoystickInner, ; iigsInputSnapshot, iigsFloodWalkAndScansInner ; ; Shared ABI facts (clang w65816 cdecl, per PORT_SPEC.md): ; - arg0 (pointer/long) arrives in A=low16, X=high16; word arg0 in A. ; It is NOT on the stack. ; - arg1, arg2, ... are pushed right-to-left; on JSL entry arg1 byte0 ; is at 4,s. A word arg is 2 bytes, a pointer/long arg is 4 bytes. ; - Each prologue push shifts every stack offset up: php=+1, phb=+1, ; phd=+2, pha/phx=+2. ; - Memory model: DBR=0 for C-called code; all globals + bank-0 soft ; switches reachable via 16-bit absolute (no long prefix needed; ; `$00Cxxx` operands are plain 16-bit abs). ; ==================================================================== ; ==================================================================== ; iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, ; uint16_t uploadFlags) ; ; SHR upload: optional SCB (200 bytes) and palette (512 bytes) uploads ; via direct-$E1 MVN, then a per-row dirty-skipped, chunked-SEI PEI ; slam of the 32000-byte pixel buffer from the bank-$01 back buffer to ; the $E1 SHR frame buffer. WIDE dirty rows take the unrolled 80-PEI ; full-row slam; NARROW dirty rows take an exact-extent MVN. ; ; Original args (D-frame, D=SP+8, every arg on stack): ; scbPtr D+0..3 (long ptr) ; palettePtr D+4..7 (long ptr) ; uploadFlags D+8..9 (uint16_t bitmask) ; ; ABI: under clang scbPtr (arg0, ptr) arrives in A:X and is NOT on the ; stack; palettePtr (arg1, ptr) and uploadFlags (arg2, word) are on the ; stack. JSL entry: palettePtr @4,s, uploadFlags @8,s; after ; php+phb+phd (+4): palettePtr @8,s, uploadFlags @12,s. ; ; The original read scbPtr/palettePtr/uploadFlags via a D-frame ; (bscb=D+0, bpal=D+4, bflags=D+8). We rebuild that exact frame in a ; private bank-0 scratch block (bsFrame) -- D+0..3 must be writable ; scratch and cannot overlap the saved registers / return address the ; way the original's stack args did -- copy scbPtr from A:X to +0..3, ; palettePtr from the stack to +4..7, uploadFlags to +8, then point D ; at bsFrame so all `bscb`/`bpal`/`bflags` reads work unchanged. Phase ; 3 (the slam) clobbers D freely via TCD to row bases (exactly as the ; original); the final PLD restores the caller's D (saved by our PHD) ; AND therefore restores DBR=0 expectations -- DBR is restored by PLB. ; ==================================================================== .section .text.iigsBlitStageToShr,"ax" .globl iigsBlitStageToShr iigsBlitStageToShr: bscb = 0 bpal = 4 bflags = 8 ; A dirty row with (maxWord-minWord) < PEI_NARROW_MAX uses an exact MVN ; instead of the full-row PEI slam. 32 keeps a safe margin below the ; ~34-word MVN/PEI break-even. PEI_NARROW_MAX = 32 php phb phd rep #0x30 .a16 .i16 ; DBR must be the CALLER's data bank (= the bank our globals / ; D-frame live in). crt0 set it and a JSL preserves it across ; the cross-seg call, so at entry DBR already equals that bank. ; We must NOT force DBR := PBR (`phk`): under MULTI-SEGMENT this ; asm can land in a different bank (seg2) than the globals ; (seg1), so PBR != globals bank and phk would point DBR at the ; wrong bank -> garbage dirty-band / gPei* reads -> corrupted ; present. (Two earlier GS/OS traps this path also fixes: args ; were read back via a DP frame under DBR=$08 -> wrong bank, now ; read via the SAME absolute addressing; and each MVN leaves ; DBR=$E1, undone after every MVN -- see below.) ; ; Stash args into absolute scratch (DBR = caller/globals bank). ; scbPtr in A:X; palettePtr/flags on stack (post php+phb+phd ; +4: @8,s @12,s). sta bsFrame+0 ; scbPtr.lo (A) stx bsFrame+2 ; scbPtr.hi (X, bank+pad) lda 8,s ; palettePtr.lo sta bsFrame+4 lda 10,s ; palettePtr.hi (bank+pad) sta bsFrame+6 lda 12,s ; uploadFlags sta bsFrame+8 ; Capture the caller's DBR (= globals bank, saved by phb at ; 3,s) into a FIXED bank-$E1 byte. Each MVN below leaves ; DBR=$E1; we reload this byte (long-addressed, so it is both ; DBR- and SP-independent -- the PEI slam relocates SP) to put ; DBR back at the globals bank. $E1:9DFE is in the unused SCB ; tail ($9DC8-$9DFF), never touched by the SCB/palette/pixel ; MVNs. sep #0x20 .a8 lda 3,s ; caller DBR (globals bank) sta 0xE19DFE ; SP/DBR-independent stash rep #0x20 .a16 ; 1. SCB upload (200 bytes) via MVN, only when the SCB dirty bit is ; set. Source bank is runtime-patched into the MVN instruction ; (encoding: $54 dst src, so byte +2 is src). lda bsFrame+8 ; uploadFlags (absolute, DBR=PBR) and #1 beq bsSkipScb sep #0x20 .a8 lda bsFrame+2 ; scbPtr bank byte (DBR = globals) ; Self-mod the MVN src-bank byte. The store must reach the MVN ; instruction in THIS code's bank (PBR), not DBR (globals): ; under multi-seg this asm can land in seg2 while DBR=seg1, so a ; DBR-relative `sta mvnScbInst+2` would patch the wrong bank and ; leave the MVN src=$00 -> garbage SCB upload. phk/plb makes ; DBR:=PBR for the store, then we restore DBR := globals. phk plb sta mvnScbInst+2 lda 0xE19DFE ; globals bank (long: DBR/SP-indep) pha plb rep #0x20 .a16 lda bsFrame+0 ; scbPtr offset tax ldy #0x9D00 lda #199 mvnScbInst: mvn 0xE1, 0x00 ; mvn dst,src (src bank runtime-patched) ; MVN left DBR=$E1; restore DBR := globals bank (caller's, ; stashed at $E1:9DFE). NOT phk -- under multi-seg PBR is this ; asm's bank, not the globals bank. A is dead here. sep #0x20 .a8 lda 0xE19DFE pha plb rep #0x20 .a16 bsSkipScb: ; 2. Palette upload (512 bytes) via MVN, only when the palette dirty ; bit is set. Same trick. lda bsFrame+8 ; uploadFlags (absolute, DBR=PBR) and #2 beq bsSkipPal sep #0x20 .a8 lda bsFrame+6 ; palettePtr bank byte (DBR = globals) ; Self-mod the MVN src-bank byte via DBR:=PBR (see the SCB MVN ; above) so the patch reaches the instruction in THIS code's ; bank, then restore DBR := globals. phk plb sta mvnPalInst+2 lda 0xE19DFE ; globals bank (long: DBR/SP-indep) pha plb rep #0x20 .a16 lda bsFrame+4 ; palettePtr offset tax ldy #0x9E00 lda #511 mvnPalInst: mvn 0xE1, 0x00 ; MVN left DBR=$E1; restore DBR := globals bank (see SCB MVN). sep #0x20 .a8 lda 0xE19DFE pha plb rep #0x20 .a16 bsSkipPal: ; 3. Pixel blit via PEI-slam, with per-row dirty skip and chunked SEI. tsc sta gPeiOrigSp sep #0x20 .a8 ; Soft switches are accessed via LONG addressing to bank $E0 ; ($E0Cxxx), NOT `sta $C035`. Under the GS/OS Loader DBR=$08, ; a DBR-relative `lda/sta $C0xx` hits bank-$08 RAM, not the ; I/O soft switches -- so shadow/RAMRD/RAMWRT silently never ; toggle and the slam writes nothing to $E1. Bank $E0 always ; holds the real I/O. (Technique from the prior joeylib's ; asmSlam: >$E0C035 for shadow, >$E0C0xx for RAMRD/RAMWRT.) lda 0xE0C035 sta gPeiOrigShadow rep #0x20 .a16 ldx #0 ; X = absolute row counter peiChunkBegin: ldy #0 ; Y = in-chunk row counter sei ; Shadow/AUXWRITE/RAMRD are scoped to each PEI burst below ; (peiSlamRow), NOT the whole chunk. The per-row control ; logic reads/writes bank-0 globals (dirty bands, row LUT, ; gPei* scratch, gPeiOrigSp); RAMRD/AUXWRITE on would ; misdirect those to aux bank $01 -- reading garbage dirty ; decisions and a garbage saved SP that ran RTL off to ; $FFFE. MVN rows need no switches (explicit src/dst banks). peiRowLoop: cpx #200 bcc peiNotAllDone ; X < 200 -> still rows to do brl peiChunkEnd peiNotAllDone: cpy #40 bcc peiCheckDirty ; Y < 40 -> chunk has room brl peiChunkEnd peiCheckDirty: sep #0x20 .a8 lda gStageMinWord,x cmp gStageMaxWord,x rep #0x20 .a16 bcc peiRowDirty ; min < max -> dirty beq peiRowDirty ; min == max -> dirty (1 word) inx ; clean row, skip iny brl peiRowLoop ; Dirty row: narrow spans take the exact-extent MVN; wide spans take ; the full-row PEI slam ($01 stack-push, shadowed $01->$E1). The slam is ; the GS fast path (the whole reason the stage lives at $01/2000 and the ; screen at $E1/2000); re-enabled now that the DBR/D-frame bugs that were ; corrupting its dirty-band reads are fixed, plus the shadow mask clears ; bit 4 (aux-bank shadow inhibit) so $01->$E1 shadowing is actually live. peiRowDirty: sep #0x20 .a8 lda gStageMaxWord,x sec sbc gStageMinWord,x ; maxWord - minWord (>= 0) rep #0x20 .a16 and #0x00FF cmp #PEI_NARROW_MAX bcs peiSlamRow ; wide span -> full-row PEI slam brl peiMvnRow ; narrow span -> exact-extent MVN peiSlamRow: ; Save X and Y into long-mode scratch (stack is hijacked into $E1). txa sta gPeiCurRow tya sta gPeiChunkRow lda gPeiCurRow asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut,x ; A = y*160 clc adc #0x2000 ; A = row_start tcd ; D = row_start (PEI dp base) clc adc #159 tcs ; SP = row_start + 159 ; Enable shadow+AUXWRITE+RAMRD for the PEI burst ONLY. ; gPeiOrigShadow is read here while RAMRD is still off, so ; it comes from bank 0 correctly. sep #0x20 .a8 lda gPeiOrigShadow and #0xF7 ; clear bit 3 -> SHR shadow ON. SHR ; shadowing mirrors the $2000-9FFF ; region for BOTH banks ($00->$E0 and ; $01->$E1); the burst pushes into ; aux $01 (RAMWRT) so it lands at $E1. sta 0xE0C035 lda #0 sta 0xE0C005 ; RAMWRT on: $00:0200-BFFF writes -> $01 sta 0xE0C003 ; RAMRD on: PEI reads aux $01 stage rep #0x20 .a16 ; 80 PEIs from DP+$9E down to DP+$00. pei 0x9E pei 0x9C pei 0x9A pei 0x98 pei 0x96 pei 0x94 pei 0x92 pei 0x90 pei 0x8E pei 0x8C pei 0x8A pei 0x88 pei 0x86 pei 0x84 pei 0x82 pei 0x80 pei 0x7E pei 0x7C pei 0x7A pei 0x78 pei 0x76 pei 0x74 pei 0x72 pei 0x70 pei 0x6E pei 0x6C pei 0x6A pei 0x68 pei 0x66 pei 0x64 pei 0x62 pei 0x60 pei 0x5E pei 0x5C pei 0x5A pei 0x58 pei 0x56 pei 0x54 pei 0x52 pei 0x50 pei 0x4E pei 0x4C pei 0x4A pei 0x48 pei 0x46 pei 0x44 pei 0x42 pei 0x40 pei 0x3E pei 0x3C pei 0x3A pei 0x38 pei 0x36 pei 0x34 pei 0x32 pei 0x30 pei 0x2E pei 0x2C pei 0x2A pei 0x28 pei 0x26 pei 0x24 pei 0x22 pei 0x20 pei 0x1E pei 0x1C pei 0x1A pei 0x18 pei 0x16 pei 0x14 pei 0x12 pei 0x10 pei 0x0E pei 0x0C pei 0x0A pei 0x08 pei 0x06 pei 0x04 pei 0x02 pei 0x00 ; Restore: AUXWRITE/RAMRD off FIRST, then read gPeiOrigShadow ; from bank 0 to restore the shadow register. After this the ; bank-0 control reads below (and at peiChunkEnd) hit main. sep #0x20 .a8 lda #0 sta 0xE0C004 ; RAMWRT off sta 0xE0C002 ; RAMRD off lda gPeiOrigShadow sta 0xE0C035 ; shadow restored rep #0x20 .a16 lda gPeiCurRow tax inx lda gPeiChunkRow tay iny brl peiRowLoop ; Narrow dirty span: exact-extent MVN of words [minWord..maxWord] from ; bank-$01 back buffer to $E1 SHR frame buffer. src $01 -> dst $E1; ; GAS spells MVN dst,src so `mvn 0xE1, 0x01`. peiMvnRow: txa sta gPeiCurRow tya sta gPeiChunkRow ; rowStart = $2000 + y*160 lda gPeiCurRow asl a tax lda gRowOffsetLut,x clc adc #0x2000 sta gPeiMvnSrc ; src/dst offset = rowStart + minWord*2 lda gPeiCurRow tax sep #0x20 .a8 lda gStageMinWord,x rep #0x20 .a16 and #0x00FF asl a clc adc gPeiMvnSrc sta gPeiMvnSrc ; count = (maxWord - minWord)*2 + 1 sep #0x20 .a8 lda gStageMaxWord,x sec sbc gStageMinWord,x rep #0x20 .a16 and #0x00FF asl a inc a sta gPeiMvnCount ; Block move: X=src off, Y=dst off (same), A=count-1; src $01, dst $E1. lda gPeiMvnSrc tax tay lda gPeiMvnCount mvn 0xE1, 0x01 ; MVN left DBR=$E1; restore DBR := globals bank (caller's, ; stashed at $E1:9DFE) before the absolute gPei*/dirty reads ; below and on the next pass. A is dead (reloaded just below). sep #0x20 .a8 lda 0xE19DFE pha plb rep #0x20 .a16 ; Restore row counters and advance. lda gPeiCurRow tax inx lda gPeiChunkRow tay iny brl peiRowLoop peiChunkEnd: ; Per-chunk teardown: restore SP, shadow, AUXWRITE/RAMRD, then CLI. lda gPeiOrigSp tcs sep #0x20 .a8 lda gPeiOrigShadow sta 0xE0C035 lda #0 sta 0xE0C004 ; RAMWRT off sta 0xE0C002 ; RAMRD off rep #0x20 .a16 cli cpx #200 bcs peiAllDone brl peiChunkBegin peiAllDone: .a8 .i8 pld plb plp ; restores I (pre-SEI value) rtl ; ==================================================================== ; iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, ; uint16_t minWord, uint16_t maxWord) ; ; For each row in [yStart, yEnd) widen gStageMinWord[row] DOWN to ; minWord (if smaller) and gStageMaxWord[row] UP to maxWord (if larger). ; Caller clips yStart/yEnd to [0,200); no internal bounds check. ; ; Original args (D-frame, D=SP+8, all on stack): ; yStart D+0..1, yEnd D+2..3, minWord D+4..5, maxWord D+6..7 ; ; ABI: ALL FOUR ARGS ARE WORDS. arg0 yStart arrives in A; arg1 yEnd, ; arg2 minWord, arg3 maxWord are on the stack. JSL entry: yEnd @4,s, ; minWord @6,s, maxWord @8,s; after php+phb+phd (+4): yEnd @8,s. ; The original D-frame had yStart at D+0; under clang yStart is in A ; and is only ever loaded ONCE into X (the loop never re-reads it), so ; we just TAX it -- no D+0 slot is needed. The three stack args slid ; DOWN by 2 (one word), so set D = SP+6 (adc #6 not #8): then ; yEnd@8,s = D+2, minWord = D+4, maxWord = D+6 (original equates hold). ; D+0..1 would overlap the return address, but mdrYStart is never ; touched in the frame, so that is harmless. ; ==================================================================== .section .text.iigsMarkDirtyRowsInner,"ax" .globl iigsMarkDirtyRowsInner iigsMarkDirtyRowsInner: mdrYEnd = 2 mdrMinWord = 4 mdrMaxWord = 6 php phb phd rep #0x30 .a16 .i16 tax ; X = yStart (arg0 in A); never re-read tsc clc adc #6 tcd ; D = SP+6 (yEnd@D+2, min@D+4, max@D+6) cpx mdrYEnd bcs mdrExit ; empty range -> nothing to do sep #0x20 .a8 mdrLoop: lda mdrMinWord cmp gStageMinWord,x bcs mdrNoMin ; A >= existing -> already <= minWord sta gStageMinWord,x ; widen down mdrNoMin: lda mdrMaxWord cmp gStageMaxWord,x bcc mdrNoMax ; A < existing -> already >= maxWord beq mdrNoMax sta gStageMaxWord,x ; widen up mdrNoMax: inx cpx mdrYEnd bcc mdrLoop rep #0x20 .a16 mdrExit: .a8 .i8 pld plb plp rtl ; ==================================================================== ; iigsPollJoystickInner(void) ; ; Reads the IIgs paddle ports (PDL0/PDL1) at 1 MHz. No args; returns the ; packed result in registers (see ABI below) -- no output globals. ; ; ABI: no args. Returns a uint32_t in A:X (A=low16, X=high16): ; A = (JoyX << 8) | resolved , X = JoyY ; i.e. resolved = ret & 0xFF (3 = stick present, 0 = none), ; px = (ret >> 8) & 0xFF (JoyX 0..255), ; py = (ret >> 16) & 0xFF (JoyY 0..255). ; Register return is used instead of output globals because an asm WRITE ; of a global does not reach the C-read address in this toolchain. ; ; Faithful port of John Brooks' 1 MHz single-pass GetJoyXY. KEY trick: ; DP is set to $C000 so the paddle/speed I/O is read DP-relative ; (`lda $64` -> $00C064). DP is ALWAYS bank 0, so this is DBR-INDEPENDENT ; -- no DBR forcing, so DBR stays at the caller's bank ($08 under GS/OS) ; and the gJoy* globals (absolute, DBR-relative) are written in the right ; bank with no bank juggling. The dual/solo counter reads JoyX ($C064) and ; JoyY ($C065) together (`lda $64 / and $65`) every ~11 cyc; whichever ; axis trips first, it drops to a solo loop for the other. A settle-wait ; (with timeout) up front avoids a mid-discharge trigger and avoids ; hanging when no stick is attached. ; ==================================================================== .section .text.iigsPollJoystickInner,"ax" .globl iigsPollJoystickInner iigsPollJoystickInner: php sep #0x34 ; sei + 8-bit M/X .a8 .i8 phd pea 0xC000 pld ; DP = $C000 (I/O via DP, bank 0) ; Wait for both paddles to settle (bit7=0), with a timeout so a missing ; stick can't hang the poll. ldy #0 gjWaitBad: lda 0x64 ; $C064 (JoyX) ora 0x65 ; | $C065 (JoyY) bpl gjSettled ; both bit7=0 -> settled iny bne gjWaitBad ; Y<256 -> keep waiting brl gjNoJoy ; settle timeout -> no stick gjSettled: bit 0x70 ; PTRIG: start X,Y timers lsr 0x36 ; force 1 MHz (clear bit 7) ldx #1 ; dual X,Y counter init xba xba gjDualXY0: lda 0x64 and 0x65 bpl gjToSolo inx gjDualXY1: lda 0x64 and 0x65 bpl gjToSolo inx gjDualXY2: lda 0x64 and 0x65 bpl gjToSolo inx bne gjDualXY0 gjDualXY3: lda 0x64 and 0x65 bmi gjSameXY dex gjSameXY: dex txy bra gjGotXY gjSoloX: bit 0x64 bmi gjSoloXOk dey bra gjGotXY gjSoloXOk: inx bne gjSoloX gjSoloY: bit 0x65 bmi gjSoloYOk dex bra gjGotXY gjToSolo: txy bit 0x64 bmi gjSoloXOk bit 0x65 bpl gjSameXY gjSoloYOk: iny bne gjSoloY dey gjGotXY: rol 0x36 ; restore CPU speed (8-bit mode) ; X = JoyX (px), Y = JoyY (py), both 0..255. Hand the result back in ; REGISTERS as the C ABI's uint32_t (A = low16, X = high16) -- NOT via ; globals: an asm WRITE of a global does not reach the C-read address in ; this toolchain (DBR/bank mismatch), whereas register return is proven ; (same path as iigsGetTickWord / iigsReadHzParam). Packing: ; A (low16) = (px << 8) | resolved X (high16) = py ; -> C sees: resolved = r&0xFF, px = (r>>8)&0xFF, py = (r>>16)&0xFF. pld ; restore caller DP plp ; back to caller M=I=16; X=px, Y=py .a16 .i16 phx ; save px (16-bit) tya tax ; X = py -> high word of result pla ; A = px xba ; A = px << 8 (XBA is 16-bit reg-wide) ora #0x0003 ; A = (px << 8) | resolved(=3) rtl ; uint32_t in A:X gjNoJoy: ; Settle timed out (speed unchanged, so no `rol`). Report no stick ; (resolved=0) with centred axes (px=py=128). pld ; restore caller DP plp ; back to caller M=I=16 .a16 .i16 ldx #128 ; py = 128 (high word) lda #0x8000 ; (px=128 << 8) | resolved(=0) rtl ; uint32_t in A:X ; ==================================================================== ; iigsInputSnapshot(void) ; ; Snapshot gKeyState->gKeyPrev (KEY_COUNT=60 bytes) plus ; gMouseButtonState/gJoyButtonState (4 bytes each). No args. ; ; ABI: void; no register/stack args. DBR stays 0; all globals via ; 16-bit abs. ; ==================================================================== .section .text.iigsInputSnapshot,"ax" .globl iigsInputSnapshot iigsInputSnapshot: php rep #0x30 .a16 .i16 sep #0x20 .a8 ; Snapshot gKeyState -> gKeyPrev (60 bytes), long-mode loop. ldx #59 isnKeyLoop: lda gKeyState,x sta gKeyPrev,x dex bpl isnKeyLoop ; Snapshot gMouseButtonState -> gMouseButtonPrev (4 bytes inline). lda gMouseButtonState sta gMouseButtonPrev lda gMouseButtonState+1 sta gMouseButtonPrev+1 lda gMouseButtonState+2 sta gMouseButtonPrev+2 lda gMouseButtonState+3 sta gMouseButtonPrev+3 ; Snapshot gJoyButtonState -> gJoyButtonPrev (4 bytes inline). lda gJoyButtonState sta gJoyButtonPrev lda gJoyButtonState+1 sta gJoyButtonPrev+1 lda gJoyButtonState+2 sta gJoyButtonPrev+2 lda gJoyButtonState+3 sta gJoyButtonPrev+3 rep #0x30 .a16 .i16 plp rtl ; ==================================================================== ; iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, ; uint16_t matchColor, uint16_t newColor, ; uint16_t matchEqual, ; int16_t *stackX, int16_t *stackY, ; uint16_t *spInOut, uint16_t maxSp) ; ; Combined per-popped-seed flood work: seed test + walk-left + walk- ; right + span fill + scan-above + scan-below (each scan pushes new ; seeds onto the explicit stackX/stackY scan stack via *spInOut/maxSp). ; Outputs gFloodSeedMatch / gFloodLeftX / gFloodRightX. ; ; Original args (D-frame, D=SP+8, all on stack): ; pixels D+0..3, x D+4..5, y D+6..7, matchColor D+8..9, ; newColor D+10..11, matchEqual D+12..13, stackX D+14..17, ; stackY D+18..21, spInOut D+22..25, maxSp D+26..27 ; ; ABI: pixels (arg0, ptr) arrives in A:X and is NOT on the stack; the ; other 9 args are on the stack. The body needs ALL of D+0..D+27 to be ; writable DP scratch -- it overwrites the pixels slot (wsRow) with the ; computed row pointer and uses `[wsRow],y`, `[wsScanRow],y`, ; `[wsStackX],y`, `[wsStackY],y`, `[wsSpInOut],y` DP-indirect-long. ; The clang stack offers no such writable 28-byte window above the ; saved regs, so we point D at a private bank-0 scratch frame (wsFrame) ; and copy EVERY arg into it at the ORIGINAL D-offsets, leaving all the ; ws* equates and indirect loads unchanged. ; ; Stack source offsets (after php+phb+phd, +4): ; x@8,s y@10,s matchColor@12,s newColor@14,s matchEqual@16,s ; stackX@18,s(4) stackY@22,s(4) spInOut@26,s(4) maxSp@30,s ; Copied to: wsFrame+4 / +6 / +8 / +10 / +12 / +14..17 / +18..21 / ; +22..25 / +26 ; pixels(A:X) -> wsFrame+0..3. PLD restores caller D. ; ==================================================================== .section .text.iigsFloodWalkAndScansInner,"ax" .globl iigsFloodWalkAndScansInner iigsFloodWalkAndScansInner: ; wsRow occupies D+0..3 (was pixels arg): row = pixels + y*160 is stored ; over the pixels slot so `lda [wsRow],y` reads the row directly. ; wsScanRow overlaps wsMatch+wsNew (D+8..11): those args are cached to ; wsMatchByte/wsNewByte first, then the slot is reused. wsRow = 0 ; D+0..3 (was wsPixels arg) wsX = 4 wsY = 6 wsScanRow = 8 ; D+8..11 (was wsMatch+wsNew args) wsMatch = 8 ; alias of wsScanRow during initial cache wsNew = 10 ; alias of wsScanRow+2 during initial cache wsEq = 12 wsStackX = 14 wsStackY = 18 wsSpInOut = 22 wsMaxSp = 26 wsPixels = 0 ; alias of wsRow for the initial row-addr compute wsMidEnd = 4 ; alias of wsX, used by inline fill (post-walk) ; DP scratch for the rewritten scan inner loop (DP-relative reads). wsScanCurX = 4 ; alias wsX/wsMidEnd, 16-bit wsScanByte = 12 ; alias wsEq.lo, 8-bit wsScanPrevHit = 26 ; alias wsMaxSp.lo, 8-bit wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit php phb phd rep #0x30 .a16 .i16 ; Rebuild the full D-frame in bank-0 scratch. ; pixels in A:X; the 9 stack args at +8/+10/.../+30,s. sta wsFrame+0 ; pixels.lo (A) stx wsFrame+2 ; pixels.hi (X) lda 8,s ; x sta wsFrame+4 lda 10,s ; y sta wsFrame+6 lda 12,s ; matchColor sta wsFrame+8 lda 14,s ; newColor sta wsFrame+10 lda 16,s ; matchEqual sta wsFrame+12 lda 18,s ; stackX.lo sta wsFrame+14 lda 20,s ; stackX.hi sta wsFrame+16 lda 22,s ; stackY.lo sta wsFrame+18 lda 24,s ; stackY.hi sta wsFrame+20 lda 26,s ; spInOut.lo sta wsFrame+22 lda 28,s ; spInOut.hi sta wsFrame+24 lda 30,s ; maxSp sta wsFrame+26 ; Copy the staged 32-byte frame into a STACK (bank-0) D-frame: ; wsFrame is a BSS global in the LOAD bank, but D (the direct ; page) is ALWAYS bank 0, so `tcd #wsFrame` aimed every ; D-relative read and [wsRow],y / [wsSpInOut],y indirect at ; bank-0:offset while the stores above wrote loadbank:offset. ; The hardware stack is bank 0, so a stack D-frame matches. lda wsFrame+30 pha lda wsFrame+28 pha lda wsFrame+26 pha lda wsFrame+24 pha lda wsFrame+22 pha lda wsFrame+20 pha lda wsFrame+18 pha lda wsFrame+16 pha lda wsFrame+14 pha lda wsFrame+12 pha lda wsFrame+10 pha lda wsFrame+8 pha lda wsFrame+6 pha lda wsFrame+4 pha lda wsFrame+2 pha lda wsFrame+0 pha tsc inc a ; A = SP+1 tcd ; D -> stacked frame (bank 0) ; Cache 8-bit constants used across walk + scan + fill. sep #0x20 .a8 lda wsMatch and #0x0F sta wsMatchByte lda wsNew and #0x0F sta wsNewByte ora #0 pha ; save newByte for doubled compute asl a asl a asl a asl a ; A = newByte << 4 sta wsNewHigh ora 1,s ; A = (newByte<<4) | newByte sta wsDoubledByte pla ; clean stack lda wsEq sta wsEqByte rep #0x20 .a16 ; Compute rowAddr (long ptr) = pixels + lut[y*2]; store over wsRow. lda wsY asl a ; A = y*2 (LUT byte offset) tax lda gRowOffsetLut,x ; A = y*160 clc adc wsPixels ; A = pixels.lo + y*160 = row.lo tax ; X = row.lo (carry preserved) lda wsPixels+2 ; A = pixels.hi adc #0 ; A = row.hi (carry from prev add) sta wsRow+2 ; D+2..3 = row.hi txa sta wsRow ; D+0..1 = row.lo ; === SEED TEST + WALK LEFT + WALK RIGHT === sep #0x20 .a8 lda wsEqByte rep #0x20 .a16 bne wsWalkEqEntry brl wsWalkBndEntry ; ***** EQUAL MODE WALK ***** wsWalkEqEntry: ; --- SEED TEST EQ (inline) --- lda wsX lsr a tay sep #0x20 .a8 bcs wsSeedEqOdd lda [wsRow],y lsr a lsr a lsr a lsr a bra wsSeedEqHave wsSeedEqOdd: lda [wsRow],y and #0x0F wsSeedEqHave: cmp wsMatchByte rep #0x20 .a16 bne wsSeedEqMiss lda #1 sta gFloodSeedMatch bra wsWalkEqSeedOk wsSeedEqMiss: lda #0 sta gFloodSeedMatch brl wsExit wsWalkEqSeedOk: ; --- WALK LEFT EQ (byte-cached) --- lda wsX sta wsScanCurX lsr a tay ; Y = byteIdx bcc wsLEqEvenEntry sep #0x20 .a8 lda [wsRow],y sta wsScanByte rep #0x20 .a16 bra wsLEqOddEntry wsLEqEvenEntry: lda wsScanCurX beq wsLeftEqDone dey sep #0x20 .a8 lda [wsRow],y sta wsScanByte and #0x0F cmp wsMatchByte rep #0x20 .a16 bne wsLeftEqDone dec wsScanCurX wsLEqOddEntry: lda wsScanCurX beq wsLeftEqDone sep #0x20 .a8 lda wsScanByte lsr a lsr a lsr a lsr a cmp wsMatchByte rep #0x20 .a16 bne wsLeftEqDone dec wsScanCurX bra wsLEqEvenEntry wsLeftEqDone: lda wsScanCurX sta gFloodLeftX ; --- WALK RIGHT EQ (byte-cached) --- lda wsX sta wsScanCurX lsr a tay bcs wsREqOddEntry sep #0x20 .a8 lda [wsRow],y sta wsScanByte rep #0x20 .a16 bra wsREqEvenEntry wsREqEvenEntry: lda wsScanCurX cmp #319 bcs wsRightEqDone sep #0x20 .a8 lda wsScanByte and #0x0F cmp wsMatchByte rep #0x20 .a16 bne wsRightEqDone inc wsScanCurX wsREqOddEntry: lda wsScanCurX cmp #319 bcs wsRightEqDone iny sep #0x20 .a8 lda [wsRow],y sta wsScanByte lsr a lsr a lsr a lsr a cmp wsMatchByte rep #0x20 .a16 bne wsRightEqDone inc wsScanCurX bra wsREqEvenEntry wsRightEqDone: lda wsScanCurX sta gFloodRightX brl wsAfterWalk ; ***** BOUNDARY MODE WALK ***** wsWalkBndEntry: ; --- SEED TEST BND (inline) --- lda wsX lsr a tay sep #0x20 .a8 bcs wsSeedBndOdd lda [wsRow],y lsr a lsr a lsr a lsr a bra wsSeedBndHave wsSeedBndOdd: lda [wsRow],y and #0x0F wsSeedBndHave: cmp wsMatchByte beq wsSeedBndMiss cmp wsNewByte beq wsSeedBndMiss rep #0x20 .a16 lda #1 sta gFloodSeedMatch bra wsWalkBndSeedOk wsSeedBndMiss: rep #0x20 .a16 lda #0 sta gFloodSeedMatch brl wsExit wsWalkBndSeedOk: ; --- WALK LEFT BND (byte-cached) --- lda wsX sta wsScanCurX lsr a tay bcc wsLBndEvenEntry sep #0x20 .a8 lda [wsRow],y sta wsScanByte rep #0x20 .a16 bra wsLBndOddEntry wsLBndEvenEntry: lda wsScanCurX beq wsLeftBndDone dey sep #0x20 .a8 lda [wsRow],y sta wsScanByte and #0x0F cmp wsMatchByte beq wsLBndStop cmp wsNewByte beq wsLBndStop rep #0x20 .a16 dec wsScanCurX bra wsLBndOddEntry2 wsLBndStop: rep #0x20 .a16 bra wsLeftBndDone wsLBndOddEntry2: wsLBndOddEntry: lda wsScanCurX beq wsLeftBndDone sep #0x20 .a8 lda wsScanByte lsr a lsr a lsr a lsr a cmp wsMatchByte beq wsLBndStop2 cmp wsNewByte beq wsLBndStop2 rep #0x20 .a16 dec wsScanCurX bra wsLBndEvenEntry wsLBndStop2: rep #0x20 .a16 wsLeftBndDone: lda wsScanCurX sta gFloodLeftX ; --- WALK RIGHT BND (byte-cached) --- lda wsX sta wsScanCurX lsr a tay bcs wsRBndOddEntry sep #0x20 .a8 lda [wsRow],y sta wsScanByte rep #0x20 .a16 bra wsRBndEvenEntry wsRBndEvenEntry: lda wsScanCurX cmp #319 bcs wsRightBndDone sep #0x20 .a8 lda wsScanByte and #0x0F cmp wsMatchByte beq wsRBndStop cmp wsNewByte beq wsRBndStop rep #0x20 .a16 inc wsScanCurX bra wsRBndOddEntry2 wsRBndStop: rep #0x20 .a16 bra wsRightBndDone wsRBndOddEntry2: wsRBndOddEntry: lda wsScanCurX cmp #319 bcs wsRightBndDone iny sep #0x20 .a8 lda [wsRow],y sta wsScanByte lsr a lsr a lsr a lsr a cmp wsMatchByte beq wsRBndStop2 cmp wsNewByte beq wsRBndStop2 rep #0x20 .a16 inc wsScanCurX bra wsRBndEvenEntry wsRBndStop2: rep #0x20 .a16 wsRightBndDone: lda wsScanCurX sta gFloodRightX wsAfterWalk: ; Cache leftX/rightX/spanLen for the scans. lda gFloodLeftX sta wsLeftX lda gFloodRightX sta wsRightX sec sbc wsLeftX inc a sta wsSpanLen ; === FILL THE SPAN === lda wsLeftX lsr a ; A = leadingByte; carry = leftX & 1 tay ; Y = leadingByte bcc wsFillNoLead ; Leading partial byte: low nibble of byte[Y] = newNibble. sep #0x20 .a8 lda [wsRow],y and #0xF0 ora wsNewByte sta [wsRow],y rep #0x20 .a16 iny ; midStart = leadingByte + 1 wsFillNoLead: ; midEnd = (rightX + 1) >> 1; trailing partial only if rightX even. lda wsRightX inc a ; A = pxEnd = rightX + 1 lsr a ; A = midEnd; carry = pxEnd & 1 sta wsMidEnd ; DP store (so DP-direct CPY works) bcs wsFillSetTrail lda #0 sta wsHasTrail bra wsFillMidLoop wsFillSetTrail: lda #1 sta wsHasTrail wsFillMidLoop: sep #0x20 .a8 lda wsDoubledByte wsFillMidIter: cpy wsMidEnd ; DP-direct CPY (X=16-bit reads 2 bytes) bcs wsFillMidDone sta [wsRow],y iny bra wsFillMidIter wsFillMidDone: rep #0x20 .a16 lda wsHasTrail beq wsFillDone ; Trailing partial byte: high nibble of byte[wsMidEnd] = newNibble. ldy wsMidEnd ; Y = trailing byte index sep #0x20 .a8 lda [wsRow],y and #0x0F ora wsNewHigh sta [wsRow],y rep #0x20 .a16 wsFillDone: ; Load sp from *spInOut, cache maxSp. ldy #0 lda [wsSpInOut],y sta wsSp lda wsMaxSp sta wsMaxSpCache ; === SCAN ABOVE (if y > 0) === lda wsY bne wsHasAbove brl wsSkipAbove wsHasAbove: lda wsRow sec sbc #160 sta wsScanRow lda wsRow+2 sbc #0 sta wsScanRow+2 lda wsY dec a sta wsScanY jsr wsScanAndPush wsSkipAbove: ; === SCAN BELOW (if y < 199) === lda wsY cmp #199 bcc wsHasBelow brl wsSkipBelow wsHasBelow: lda wsRow clc adc #160 sta wsScanRow lda wsRow+2 adc #0 sta wsScanRow+2 lda wsY inc a sta wsScanY jsr wsScanAndPush wsSkipBelow: ; Store updated sp back to *spInOut. ldy #0 lda wsSp sta [wsSpInOut],y wsExit: rep #0x30 ; M=16/X=16 for the SP arithmetic .a16 .i16 tsc clc adc #32 ; drop the 32-byte stack D-frame tcs pld plb plp rtl ; wsScanAndPush: walk wsScanRow[wsLeftX..wsRightX] for run-edge ; transitions. Pushes (curX-1, wsScanY) on falling edges and ; (rightX, wsScanY) at end of trailing run. 2 pixels per byte read. wsScanAndPush: sep #0x20 .a8 lda #0 sta wsScanPrevHit lda wsEqByte rep #0x20 .a16 bne wsEqEntry2 brl wsBoundEntry2 ; ***** EQUAL MODE ***** wsEqEntry2: lda wsLeftX sta wsScanCurX ; DP write 16-bit lsr a ; A = byteIdx; carry = leftX & 1 tay ; Y = byteIdx bcc wsEqAfterLead2 ; LEADING: low nibble of byte at Y (curX = leftX, odd). sep #0x20 .a8 lda [wsScanRow],y and #0x0F cmp wsMatchByte beq wsEqLeadHit2 lda #0 bra wsEqLeadStore2 wsEqLeadHit2: lda #1 wsEqLeadStore2: sta wsScanCurHit bne wsEqLeadNoFall2 lda wsScanPrevHit beq wsEqLeadNoFall2 rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsEqLeadNoFall2: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX iny wsEqAfterLead2: ; Pair loop: while curX+1 <= rightX, process high+low of one byte. wsEqPairLoop2: lda wsScanCurX inc a cmp wsRightX bcc wsEqDoPair2 beq wsEqDoPair2 brl wsEqTrailing2 wsEqDoPair2: sep #0x20 .a8 lda [wsScanRow],y sta wsScanByte lsr a lsr a lsr a lsr a ; A = high nibble (pixel at curX) cmp wsMatchByte beq wsEqHi2Hit lda #0 bra wsEqHi2Store wsEqHi2Hit: lda #1 wsEqHi2Store: sta wsScanCurHit bne wsEqHi2NoFall lda wsScanPrevHit beq wsEqHi2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsEqHi2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX sep #0x20 .a8 lda wsScanByte and #0x0F ; A = low nibble (pixel at curX after inc) cmp wsMatchByte beq wsEqLo2Hit lda #0 bra wsEqLo2Store wsEqLo2Hit: lda #1 wsEqLo2Store: sta wsScanCurHit bne wsEqLo2NoFall lda wsScanPrevHit beq wsEqLo2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsEqLo2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX iny brl wsEqPairLoop2 wsEqTrailing2: lda wsScanCurX cmp wsRightX bcc wsEqDoTrail2 beq wsEqDoTrail2 brl wsScanDone wsEqDoTrail2: sep #0x20 .a8 lda [wsScanRow],y lsr a lsr a lsr a lsr a ; A = high nibble cmp wsMatchByte beq wsEqTr2Hit lda #0 bra wsEqTr2Store wsEqTr2Hit: lda #1 wsEqTr2Store: sta wsScanCurHit bne wsEqTr2NoFall lda wsScanPrevHit beq wsEqTr2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsEqTr2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 brl wsScanDone ; ***** BOUNDARY MODE ***** wsBoundEntry2: lda wsLeftX sta wsScanCurX lsr a tay bcc wsBndAfterLead2 ; LEADING: low nibble of byte at Y, boundary mode. sep #0x20 .a8 lda [wsScanRow],y and #0x0F cmp wsMatchByte beq wsBndLeadMiss2 cmp wsNewByte beq wsBndLeadMiss2 lda #1 bra wsBndLeadStore2 wsBndLeadMiss2: lda #0 wsBndLeadStore2: sta wsScanCurHit bne wsBndLeadNoFall2 lda wsScanPrevHit beq wsBndLeadNoFall2 rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsBndLeadNoFall2: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX iny wsBndAfterLead2: wsBndPairLoop2: lda wsScanCurX inc a cmp wsRightX bcc wsBndDoPair2 beq wsBndDoPair2 brl wsBndTrailing2 wsBndDoPair2: sep #0x20 .a8 lda [wsScanRow],y sta wsScanByte lsr a lsr a lsr a lsr a ; A = high nibble cmp wsMatchByte beq wsBndHi2Miss cmp wsNewByte beq wsBndHi2Miss lda #1 bra wsBndHi2Store wsBndHi2Miss: lda #0 wsBndHi2Store: sta wsScanCurHit bne wsBndHi2NoFall lda wsScanPrevHit beq wsBndHi2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsBndHi2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX sep #0x20 .a8 lda wsScanByte and #0x0F ; A = low nibble cmp wsMatchByte beq wsBndLo2Miss cmp wsNewByte beq wsBndLo2Miss lda #1 bra wsBndLo2Store wsBndLo2Miss: lda #0 wsBndLo2Store: sta wsScanCurHit bne wsBndLo2NoFall lda wsScanPrevHit beq wsBndLo2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsBndLo2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 inc wsScanCurX iny brl wsBndPairLoop2 wsBndTrailing2: lda wsScanCurX cmp wsRightX bcc wsBndDoTrail2 beq wsBndDoTrail2 brl wsScanDone wsBndDoTrail2: sep #0x20 .a8 lda [wsScanRow],y lsr a lsr a lsr a lsr a ; A = high nibble cmp wsMatchByte beq wsBndTr2Miss cmp wsNewByte beq wsBndTr2Miss lda #1 bra wsBndTr2Store wsBndTr2Miss: lda #0 wsBndTr2Store: sta wsScanCurHit bne wsBndTr2NoFall lda wsScanPrevHit beq wsBndTr2NoFall rep #0x20 .a16 lda wsScanCurX dec a sta wsPushX jsr wsPushXY sep #0x20 .a8 wsBndTr2NoFall: lda wsScanCurHit sta wsScanPrevHit rep #0x20 .a16 brl wsScanDone wsScanDone: ; Trailing run: if prevHit, push (rightX, scanY). sep #0x20 .a8 lda wsScanPrevHit rep #0x20 .a16 beq wsScanReturn lda wsRightX sta wsPushX jsr wsPushXY wsScanReturn: rts ; wsPushXY: push (wsPushX, wsScanY) at sp if sp