; 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 jlpInit; 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.
;
; Also builds tpmNibPairIdx (256 bytes) for iigsTilePasteMonoInner:
; tpmNibPairIdx[b] = ((b & 0xF0) ? 2 : 0) | ((b & 0x0F) ? 1 : 0), the
; 2-bit "which nibbles are opaque" index into the per-call fg/bg combo
; bytes (PERF-AUDIT #3).
; ----------------------------------------------------------------
	.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

; tpmNibPairIdx build: byte stores, so M=8 (X stays 16 for the index).
	sep #0x20
	.a8
	ldx #0                      ; X = b
nibLutLoop:
	txa                         ; A = b (low 8 bits of X)
	and #0xF0
	beq nibLutHiZero
	lda #2
	bra nibLutHiDone
nibLutHiZero:
	lda #0
nibLutHiDone:
	sta tpmNibPairIdx, x
	txa
	and #0x0F
	beq nibLutNext
	lda tpmNibPairIdx, x
	ora #1
	sta tpmNibPairIdx, x
nibLutNext:
	inx
	cpx #256
	bcc nibLutLoop
	rep #0x20
	.a16

	.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 jlpInit. ~1.5x faster.
;
; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI.
; SAFE ONLY POST-jlpInit. 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 nibble)
;
; 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 nibble (word, pre-masked to 0x0..0xF by the jlpTileFill
;      macro) on the stack: @4,s on JSL entry; after PHP+PHB+PHD(+4) it
;      sits @8,s. Only the nibble is on the stack and dst comes in A:X.
;      The $NNNN fill word is derived HERE from the nibble (shift/ORA
;      chain, ~26 cyc) so C never pays the * 0x1111 software-multiply
;      helper for a runtime color (PERF-AUDIT #41).
;
; 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

; Derive fillWord = nibble * 0x1111 in asm (#41). tileScratchPtrs+4 is
; free scratch here: tileFill stages no src pointer.
                lda     12,s                 ; nibble (8,s + 4 just pushed)
                asl     a
                asl     a
                asl     a
                asl     a                    ; A = nibble << 4
                ora     12,s                 ; A = nibble * 0x11 (doubled byte)
                sta     tileScratchPtrs+4
                xba                          ; A = doubled byte << 8
                ora     tileScratchPtrs+4    ; A = nibble * 0x1111 = fillWord

                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


; ====================================================================
; iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile,
;                        uint16_t fgColor, uint16_t bgColor)
;
; Fused mono colorize + paste (PERF-AUDIT #3): expands a 32-byte mono
; tile (4 bytes/row * 8 rows, tight) straight into surface rows at
; `dstRow0` (stride 160) -- no intermediate jlTileT colorize pass and
; no second jlpTilePaste call. Contract is jlpGenericTilePasteMono
; (src/generic/genericTile.c): per source byte, a NON-ZERO high nibble
; selects fgColor for the left pixel (else bgColor), a NON-ZERO low
; nibble selects fgColor for the right pixel (else bgColor). fgColor/
; bgColor arrive PRE-MASKED to 4 bits (the public jlTilePasteMono
; wrapper owns the & 0x0F, PERF-AUDIT #69) -- no re-mask here.
;
; Design: each source byte maps to one of only FOUR output bytes
; (bg/bg, bg/fg, fg/bg, fg/fg). A static 256-byte table
; (tpmNibPairIdx, built once in iigsInitRowLut) collapses the byte to
; its 2-bit opacity-pair index; the four combo bytes are built per
; call (4 bytes, ~30 cyc) into the bank-0 stack D-frame. The body is
; then branch-free: load src byte -> index the static table -> index
; the combo bytes -> store. ~33 cyc/byte. This beats both a per-call
; 16-word LUT (word-at-a-time needs a ~300-cyc per-call build AND two
; table hops per word anyway) and inline branch selection on the
; nibble tests (~58 cyc/byte, data-dependent).
;
; ABI: arg0 dstRow0 in A:X. arg1 monoTile (4-byte ptr), arg2 fgColor
;      (word), arg3 bgColor (word) on the stack. JSL entry: mono.lo
;      @4,s, mono.bank @6,s, fg @8,s, bg @10,s. No PHB (DBR is never
;      written), so after PHP+PHD(+3): mono.lo @7,s, mono.bank @9,s,
;      fg @11,s, bg @13,s; after the comboHi push (+2): fg @13,s,
;      bg @15,s. Same bank-0 stack D-frame as iigsTilePasteInner, two
;      words wider:
;        D+0..3  dst far ptr   -> [tpmDst],y
;        D+4..7  src far ptr   -> [tpmSrc],y
;        D+8..11 combo bytes   -> lda tpmCombo,x (dp,x; idx 0..3)
;      combo[idx]: bit1 of idx = hi nibble opaque -> fg in high output
;      nibble; bit0 = lo nibble opaque -> fg in low output nibble:
;        D+8 = bg:bg   D+9 = bg:fg   D+10 = fg:bg   D+11 = fg:fg
; ====================================================================
.section .text.iigsTilePasteMonoInner,"ax"
.globl iigsTilePasteMonoInner
iigsTilePasteMonoInner:
tpmDst   = 0
tpmSrc   = 4
tpmCombo = 8

                php
                phd                          ; save caller D
                rep     #0x30
                .a16
                .i16

                ; Stage dst (A:X) and src (stack) exactly like
                ; iigsTilePasteInner: tileScratchPtrs is LOAD-bank BSS, so it
                ; is only a staging area -- the D-frame the indirects read
                ; through is rebuilt on the bank-0 stack below.
                sta     tileScratchPtrs+0    ; dst low16
                stx     tileScratchPtrs+2    ; dst high16 (bank + pad)
                lda     7,s                  ; src low16
                sta     tileScratchPtrs+4
                lda     9,s                  ; src bank
                sta     tileScratchPtrs+6

                ; Build the four combo bytes. fg/bg are pre-masked words
                ; (high byte 0), so every intermediate below has a clean
                ; high byte for the XBA merges.
                lda     11,s                 ; fg
                asl     a
                asl     a
                asl     a
                asl     a
                sta     tpmFgHi              ; fg << 4
                lda     13,s                 ; bg
                asl     a
                asl     a
                asl     a
                asl     a
                sta     tpmBgHi              ; bg << 4
                ; comboHi word: high byte fg:fg (idx 3), low byte fg:bg (idx 2)
                lda     tpmFgHi
                ora     11,s                 ; A = fg:fg
                xba                          ; A = fg:fg << 8
                ora     tpmFgHi
                ora     13,s                 ; low byte = fg:bg
                pha                          ; -> D+10..11 (args now +2: fg @13,s bg @15,s)
                ; comboLo word: high byte bg:fg (idx 1), low byte bg:bg (idx 0)
                lda     tpmBgHi
                ora     13,s                 ; A = bg:fg
                xba                          ; A = bg:fg << 8
                ora     tpmBgHi
                ora     15,s                 ; low byte = bg:bg
                pha                          ; -> D+8..9

                ; Stack D-frame: {dst, src} under the combo words.
                lda     tileScratchPtrs+6
                pha                          ; src bank   -> D+6
                lda     tileScratchPtrs+4
                pha                          ; src low16  -> D+4
                lda     tileScratchPtrs+2
                pha                          ; dst high16 -> D+2
                lda     tileScratchPtrs+0
                pha                          ; dst low16  -> D+0
                tsc
                inc     a                    ; A = SP+1
                tcd                          ; D -> stacked {dst,src,combos}

                lda     #0                   ; B := 0 so M=8 TAX gives a clean 16-bit index
                sep     #0x20                ; M=8, X/Y stay 16
                .a8

; 32 unrolled byte expansions: src offset = row*4 + col (packed),
; dst offset = row*160 + col. M=8 loads never touch B, so every TAX
; below transfers 0:byte.
                ; Row 0: src 0..3 == dst 0..3, so one Y serves both.
                ldy     #0
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                sta     [tpmDst], y
                ldy     #1
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                sta     [tpmDst], y
                ldy     #2
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                sta     [tpmDst], y
                ldy     #3
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                sta     [tpmDst], y
                ; Row 1: src 4..7, dst 160..163
                ldy     #4
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #160
                sta     [tpmDst], y
                ldy     #5
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #161
                sta     [tpmDst], y
                ldy     #6
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #162
                sta     [tpmDst], y
                ldy     #7
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #163
                sta     [tpmDst], y
                ; Row 2: src 8..11, dst 320..323
                ldy     #8
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #320
                sta     [tpmDst], y
                ldy     #9
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #321
                sta     [tpmDst], y
                ldy     #10
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #322
                sta     [tpmDst], y
                ldy     #11
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #323
                sta     [tpmDst], y
                ; Row 3: src 12..15, dst 480..483
                ldy     #12
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #480
                sta     [tpmDst], y
                ldy     #13
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #481
                sta     [tpmDst], y
                ldy     #14
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #482
                sta     [tpmDst], y
                ldy     #15
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #483
                sta     [tpmDst], y
                ; Row 4: src 16..19, dst 640..643
                ldy     #16
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #640
                sta     [tpmDst], y
                ldy     #17
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #641
                sta     [tpmDst], y
                ldy     #18
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #642
                sta     [tpmDst], y
                ldy     #19
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #643
                sta     [tpmDst], y
                ; Row 5: src 20..23, dst 800..803
                ldy     #20
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #800
                sta     [tpmDst], y
                ldy     #21
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #801
                sta     [tpmDst], y
                ldy     #22
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #802
                sta     [tpmDst], y
                ldy     #23
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #803
                sta     [tpmDst], y
                ; Row 6: src 24..27, dst 960..963
                ldy     #24
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #960
                sta     [tpmDst], y
                ldy     #25
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #961
                sta     [tpmDst], y
                ldy     #26
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #962
                sta     [tpmDst], y
                ldy     #27
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #963
                sta     [tpmDst], y
                ; Row 7: src 28..31, dst 1120..1123
                ldy     #28
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #1120
                sta     [tpmDst], y
                ldy     #29
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #1121
                sta     [tpmDst], y
                ldy     #30
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #1122
                sta     [tpmDst], y
                ldy     #31
                lda     [tpmSrc], y
                tax
                lda     tpmNibPairIdx, x
                tax
                lda     tpmCombo, x
                ldy     #1123
                sta     [tpmDst], y

                rep     #0x30                ; M=16 before the epilogue math
                .a16
                .i16
                tsc
                clc
                adc     #12                  ; drop the {dst,src,combos} D-frame
                tcs
                .a8
                .i8
                pld
                plp
                rtl


; iigsTilePasteMonoInner per-call scratch (fg<<4 / bg<<4 staging).
.section .bss.tpmFgHi,"aw"
.globl tpmFgHi
tpmFgHi:
.zero 2

.section .bss.tpmBgHi,"aw"
.globl tpmBgHi
tpmBgHi:
.zero 2

; Static mono-byte -> 2-bit opacity-pair index table (built once in
; iigsInitRowLut): tpmNibPairIdx[b] = ((b&0xF0)?2:0) | ((b&0x0F)?1:0).
.section .bss.tpmNibPairIdx,"aw"
.globl tpmNibPairIdx
tpmNibPairIdx:
.zero 256


; ====================================================================
; 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.
; ====================================================================
; TMASK_BYTE: one masked byte, expanded inline (NATIVE-PERF Phase 2
; R6). Body identical to the old tmaskByte subroutine; expanding at
; each of the 32 sites deletes the jsr/rts pair (12 cyc x 32 = 384
; cyc/call, ~9% of the op). GAS numeric labels stay local per
; expansion. In: M=8, X=16, Y = byte offset, [0]=dst / [4]=src frame.
.macro TMASK_BYTE
                lda     [4], y               ; A = src byte
                cmp     tmaskDoubled
                beq     9f                   ; both nibbles == transparent
                sta     tmaskSrc
                and     #0xF0
                cmp     tmaskTHi
                beq     7f
                sta     tmaskOutHi           ; src hi (kept in $F0 form)
                bra     8f
7:
                lda     [0], y
                and     #0xF0
                sta     tmaskOutHi
8:
                lda     tmaskSrc
                and     #0x0F
                cmp     tmaskTLo
                beq     5f
                sta     tmaskOutLo           ; src lo
                bra     6f
5:
                lda     [0], y
                and     #0x0F
                sta     tmaskOutLo
6:
                lda     tmaskOutHi
                ora     tmaskOutLo
                sta     [0], y
9:
.endm

.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:
                TMASK_BYTE                   ; byte 0
                iny
                TMASK_BYTE                   ; byte 1
                iny
                TMASK_BYTE                   ; byte 2
                iny
                TMASK_BYTE                   ; 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
                beq     tmaskRowsDone
                brl     tmaskRowLoop         ; loop body is 4 inlined
                                             ; TMASK_BYTE expansions --
                                             ; past 8-bit branch range
tmaskRowsDone:

                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



; ====================================================================
; NATIVE-PERF Phase 2 Step D (R2): FUSED stage-tile entries.
;
; The C tile path used to pay pointer math (far LUT read + s->pixels
; double-deref) plus TWO JSLs per stage tile op (op inner + dirty
; marker). These entries do all three in ONE JSL: derive the stage
; destination from the pinned stage base ($01:2000, IIGS_STAGE_PIXELS
; in hal.c) + gRowOffsetLut[by*8] + bx*4, run the op via
; absolute-long,X stores with the stage literal folded into each
; offset, then widen the dirty band via stageTileMark below. The
; jlpTile* macros in port.h gate on s == gStage; non-stage surfaces
; keep taking the pointer-based inners above.
; ====================================================================

; stageTileMark: shared fused dirty-mark tail.
; In:  A = bx (word), Y = by (word); M=16, X=16.
; Out: M=16, X=16. Clobbers A, X, Y.
; Widens gStageMinWord/gStageMaxWord rows by*8..by*8+7 to the tile
; band [bx*2, bx*2+1] -- byte compares in M=8 through the same
; DBR-inherited abs,x write path as iigsMarkDirtyRowsInner (the
; proven asm-writes-C-global pattern; see the trailer note).
; ONE shared section for the helper + all three fused entries: the
; multi-seg OMF packer places sections atomically, so the 16-bit jsr
; from each entry to stageTileMark stays same-bank by construction
; (same reason tmaskByte lives inside the copyMasked section; 16-bit
; self-references only resolve intra-segment -- see clang-build.sh).
.section .text.iigsFusedTile,"ax"
.globl stageTileMark
stageTileMark:
                asl     a                    ; A = bx*2 = minWord
                pha                          ; park minWord
                tya
                asl     a
                asl     a
                asl     a                    ; A = by*8 = first row
                tax
                pla                          ; A = minWord (high byte 0)
                sep     #0x20
                .a8
                ldy     #8                   ; row counter (X stays 16-bit)
stmLoop:
                cmp     gStageMinWord,x
                bcs     stmNoMin             ; A >= existing -> already covered
                sta     gStageMinWord,x      ; widen down
stmNoMin:
                inc     a                    ; A = maxWord = minWord + 1
                cmp     gStageMaxWord,x
                bcc     stmNoMax             ; A < existing -> already covered
                beq     stmNoMax
                sta     gStageMaxWord,x      ; widen up
stmNoMax:
                dec     a                    ; back to minWord
                inx
                dey
                bne     stmLoop
                rep     #0x20
                .a16
                rts

; ====================================================================
; iigsTileFillMark(uint16_t nibble, uint16_t bx, uint16_t by)
;
; ABI: arg0 nibble (word, pre-masked to 0x0..0xF by the jlpTileFill
;      macro) in A; bx @4,s, by @6,s on JSL entry -> @8,s / @10,s
;      after php+phb+phd. No D-frame: the destination is reached via
;      $012000+off,x (absolute long indexed), X = tile byte offset.
;      fillWord = nibble * $1111 derived in asm (PERF-AUDIT #41);
;      tileScratchPtrs is free scratch (no src pointer here).
; ====================================================================
.globl iigsTileFillMark
iigsTileFillMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                sta     tileScratchPtrs+0    ; nibble
                asl     a
                asl     a
                asl     a
                asl     a                    ; nibble << 4
                ora     tileScratchPtrs+0    ; nibble * 0x11
                sta     tileScratchPtrs+2
                xba                          ; doubled byte << 8
                ora     tileScratchPtrs+2    ; nibble * 0x1111 = fillWord
                sta     tileScratchPtrs+4    ; park fillWord
                ; dst offset = gRowOffsetLut[by*8] + bx*4
                lda     10,s                 ; by
                asl     a
                asl     a
                asl     a
                asl     a                    ; by*16 = (by*8) * 2 = LUT byte index
                tax
                lda     8,s                  ; bx
                asl     a
                asl     a                    ; bx*4
                clc
                adc     gRowOffsetLut,x
                tax                          ; X = stage byte offset of tile row 0
                lda     tileScratchPtrs+4    ; fillWord
                sta     0x012000+0,x
                sta     0x012000+2,x
                sta     0x012000+160,x
                sta     0x012000+162,x
                sta     0x012000+320,x
                sta     0x012000+322,x
                sta     0x012000+480,x
                sta     0x012000+482,x
                sta     0x012000+640,x
                sta     0x012000+642,x
                sta     0x012000+800,x
                sta     0x012000+802,x
                sta     0x012000+960,x
                sta     0x012000+962,x
                sta     0x012000+1120,x
                sta     0x012000+1122,x
                ; fused dirty mark
                lda     10,s
                tay                          ; Y = by
                lda     8,s                  ; A = bx
                jsr     stageTileMark
                pld
                plb
                plp
                rtl

; ====================================================================
; iigsTilePasteMark(const uint8_t *srcTilePixels, uint16_t bx, uint16_t by)
;
; ABI: arg0 src in A:X (packed 32-byte tile, stride 4); bx @4,s,
;      by @6,s on JSL entry -> @8,s / @10,s after php+phb+phd ->
;      @12,s / @14,s after the 4-byte src D-frame (phx+pha). The src
;      D-frame comes straight from the registers -- no tileScratchPtrs
;      round-trip (the fill-style single-pointer frame). Dst is
;      $012000+off,x like iigsTileFillMark.
; ====================================================================
.globl iigsTilePasteMark
iigsTilePasteMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; src high16 -> 3,s
                pha                          ; src low16  -> 1,s
                tsc
                inc     a
                tcd                          ; [0] = srcTilePixels
                ; dst offset = gRowOffsetLut[by*8] + bx*4
                lda     14,s                 ; by
                asl     a
                asl     a
                asl     a
                asl     a
                tax
                lda     12,s                 ; bx
                asl     a
                asl     a
                clc
                adc     gRowOffsetLut,x
                tax                          ; X = stage byte offset of tile row 0
                ; 8 rows: src stride 4 via [0],y; dst stride 160 folded
                ; into the long,x literals.
                ldy     #0
                lda     [0], y
                sta     0x012000+0,x
                ldy     #2
                lda     [0], y
                sta     0x012000+2,x
                ldy     #4
                lda     [0], y
                sta     0x012000+160,x
                ldy     #6
                lda     [0], y
                sta     0x012000+162,x
                ldy     #8
                lda     [0], y
                sta     0x012000+320,x
                ldy     #10
                lda     [0], y
                sta     0x012000+322,x
                ldy     #12
                lda     [0], y
                sta     0x012000+480,x
                ldy     #14
                lda     [0], y
                sta     0x012000+482,x
                ldy     #16
                lda     [0], y
                sta     0x012000+640,x
                ldy     #18
                lda     [0], y
                sta     0x012000+642,x
                ldy     #20
                lda     [0], y
                sta     0x012000+800,x
                ldy     #22
                lda     [0], y
                sta     0x012000+802,x
                ldy     #24
                lda     [0], y
                sta     0x012000+960,x
                ldy     #26
                lda     [0], y
                sta     0x012000+962,x
                ldy     #28
                lda     [0], y
                sta     0x012000+1120,x
                ldy     #30
                lda     [0], y
                sta     0x012000+1122,x
                ; fused dirty mark
                lda     14,s
                tay                          ; Y = by
                lda     12,s                 ; A = bx
                jsr     stageTileMark
                ; drop the src D-frame, restore
                tsc
                clc
                adc     #4
                tcs
                pld
                plb
                plp
                rtl

; ====================================================================
; iigsTileCopyMark(const uint8_t *srcRow0, uint16_t dstBx, uint16_t dstBy)
;
; Same shape as iigsTilePasteMark but the source is a SURFACE row
; (stride 160, any bank -- the jlpTileCopy macro derives it in C
; because src may be any surface), so the [0],y offsets mirror the
; dst deltas. Dst is the stage, derived in asm.
; ====================================================================
.globl iigsTileCopyMark
iigsTileCopyMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; src high16 -> 3,s
                pha                          ; src low16  -> 1,s
                tsc
                inc     a
                tcd                          ; [0] = srcRow0
                ; dst offset = gRowOffsetLut[dstBy*8] + dstBx*4
                lda     14,s                 ; dstBy
                asl     a
                asl     a
                asl     a
                asl     a
                tax
                lda     12,s                 ; dstBx
                asl     a
                asl     a
                clc
                adc     gRowOffsetLut,x
                tax                          ; X = stage byte offset of tile row 0
                ; 8 rows: src stride 160 via [0],y; dst stride 160 in
                ; the long,x literals (parallel offsets = block copy).
                ldy     #0
                lda     [0], y
                sta     0x012000+0,x
                ldy     #2
                lda     [0], y
                sta     0x012000+2,x
                ldy     #160
                lda     [0], y
                sta     0x012000+160,x
                ldy     #162
                lda     [0], y
                sta     0x012000+162,x
                ldy     #320
                lda     [0], y
                sta     0x012000+320,x
                ldy     #322
                lda     [0], y
                sta     0x012000+322,x
                ldy     #480
                lda     [0], y
                sta     0x012000+480,x
                ldy     #482
                lda     [0], y
                sta     0x012000+482,x
                ldy     #640
                lda     [0], y
                sta     0x012000+640,x
                ldy     #642
                lda     [0], y
                sta     0x012000+642,x
                ldy     #800
                lda     [0], y
                sta     0x012000+800,x
                ldy     #802
                lda     [0], y
                sta     0x012000+802,x
                ldy     #960
                lda     [0], y
                sta     0x012000+960,x
                ldy     #962
                lda     [0], y
                sta     0x012000+962,x
                ldy     #1120
                lda     [0], y
                sta     0x012000+1120,x
                ldy     #1122
                lda     [0], y
                sta     0x012000+1122,x
                ; fused dirty mark
                lda     14,s
                tay                          ; Y = dstBy
                lda     12,s                 ; A = dstBx
                jsr     stageTileMark
                ; drop the src D-frame, restore
                tsc
                clc
                adc     #4
                tcs
                pld
                plb
                plp
                rtl

; ====================================================================
; iigsTileMapPasteMark(uint16_t bx, uint16_t by, uint16_t wTiles,
;                      uint16_t hTiles, const jlTileT *tiles,
;                      const uint8_t *map)
;
; NATIVE-PERF Phase 4: whole-map tile walker. ONE JSL walks the
; wTiles x hTiles row-major uint8_t index map and pastes every
; non-0xFF tile to the stage, then widens the dirty band once for the
; whole rectangle -- amortizing the per-tile JSL, arg staging, derive,
; and 8-row mark that iigsTilePasteMark pays per tile. The dst offset
; advances incrementally (+4 per tile column, +1280 per tile row =
; 8 SHR rows) from ONE gRowOffsetLut read at entry.
;
; ABI: bx in A; by @4,s, wTiles @6,s, hTiles @8,s, tiles @10,s/12,s,
;      map @14,s/16,s on JSL entry. Frame: php+phb+phd (+4), parked
;      bx (+2), then a 22-byte reserved D-frame (+22) = +28 total; D
;      = SP+1 so frame slots are D-relative and args sit at D+31..43.
;      X survives the 16-pair copy body (sta long,x never writes X),
;      so it carries the walking dst offset; Y is clobbered per cell
;      and the map index lives in the frame instead. The idx << 5
;      tileset add carries into the bank byte (tmwSrc+2), so a
;      tileset crossing a bank boundary still resolves.
;
; No internal jsr (the band union is inline), so the section is
; self-contained -- no cross-section 16-bit-jsr hazard. The three
; wrapping branches (skip-cell forward, column back, row back) span
; the ~150-byte copy body and use the bne-over-brl idiom.
; ====================================================================
.section .text.iigsTileMapPasteMark,"ax"
.globl iigsTileMapPasteMark
iigsTileMapPasteMark:
tmwSrc      = 0                              ; current tile src far ptr ([0],y)
tmwMap      = 4                              ; map far ptr ([4],y)
tmwRowBase  = 8                              ; dst offset of current tile row
tmwTilesLo  = 10                             ; tileset base low16
tmwTilesHi  = 12                             ; tileset base high16 (bank)
tmwMapIdx   = 14                             ; running map index (reused: yEnd for the mark)
tmwTx       = 16                             ; columns left this row (reused: minWord)
tmwTy       = 18                             ; rows left (reused: maxWord)
tmwWt       = 20                             ; wTiles reload value
tmwBx       = 22                             ; parked bx
tmwByArg    = 31                             ; by      (orig 4,s  + 28 pushed, D-relative)
tmwWtArg    = 33                             ; wTiles  (orig 6,s)
tmwHtArg    = 35                             ; hTiles  (orig 8,s)
tmwTLoArg   = 37                             ; tiles low16  (orig 10,s)
tmwTHiArg   = 39                             ; tiles high16 (orig 12,s)
tmwMLoArg   = 41                             ; map low16    (orig 14,s)
tmwMHiArg   = 43                             ; map high16   (orig 16,s)

                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                pha                          ; park bx -> D+22
                tsc
                sec
                sbc     #22
                tcs                          ; reserve the 22-byte frame
                inc     a
                tcd                          ; D = frame base (bank-0 stack)

                lda     tmwMLoArg
                sta     tmwMap+0
                lda     tmwMHiArg
                sta     tmwMap+2
                lda     tmwTLoArg
                sta     tmwTilesLo
                lda     tmwTHiArg
                sta     tmwTilesHi
                lda     tmwWtArg
                sta     tmwWt
                lda     tmwHtArg
                sta     tmwTy
                stz     tmwMapIdx
                ; row-0 dst offset = gRowOffsetLut[by*8] + bx*4 (ONCE)
                lda     tmwByArg
                asl     a
                asl     a
                asl     a
                asl     a
                tax
                lda     tmwBx
                asl     a
                asl     a
                clc
                adc     gRowOffsetLut,x
                sta     tmwRowBase

tmwRowTop:
                lda     tmwWt
                sta     tmwTx
                lda     tmwRowBase
                tax                          ; X = dst offset, walks +4/column
tmwColTop:
                ldy     tmwMapIdx
                sep     #0x20
                .a8
                lda     [tmwMap], y          ; map byte
                cmp     #0xFF
                bne     tmwDoTile
                rep     #0x20
                .a16
                brl     tmwCellNext          ; skip: past the copy body's range
tmwDoTile:
                rep     #0x20
                .a16
                and     #0x00FF              ; idx (high byte was stale)
                asl     a
                asl     a
                asl     a
                asl     a
                asl     a                    ; idx * 32 (sizeof proof in tile.c)
                clc
                adc     tmwTilesLo
                sta     tmwSrc+0
                lda     tmwTilesHi
                adc     #0                   ; bank carry
                sta     tmwSrc+2
                ; 8 rows: src stride 4 via [0],y; dst stride 160 folded
                ; into the long,x literals (the iigsTilePasteMark body).
                ldy     #0
                lda     [tmwSrc], y
                sta     0x012000+0,x
                ldy     #2
                lda     [tmwSrc], y
                sta     0x012000+2,x
                ldy     #4
                lda     [tmwSrc], y
                sta     0x012000+160,x
                ldy     #6
                lda     [tmwSrc], y
                sta     0x012000+162,x
                ldy     #8
                lda     [tmwSrc], y
                sta     0x012000+320,x
                ldy     #10
                lda     [tmwSrc], y
                sta     0x012000+322,x
                ldy     #12
                lda     [tmwSrc], y
                sta     0x012000+480,x
                ldy     #14
                lda     [tmwSrc], y
                sta     0x012000+482,x
                ldy     #16
                lda     [tmwSrc], y
                sta     0x012000+640,x
                ldy     #18
                lda     [tmwSrc], y
                sta     0x012000+642,x
                ldy     #20
                lda     [tmwSrc], y
                sta     0x012000+800,x
                ldy     #22
                lda     [tmwSrc], y
                sta     0x012000+802,x
                ldy     #24
                lda     [tmwSrc], y
                sta     0x012000+960,x
                ldy     #26
                lda     [tmwSrc], y
                sta     0x012000+962,x
                ldy     #28
                lda     [tmwSrc], y
                sta     0x012000+1120,x
                ldy     #30
                lda     [tmwSrc], y
                sta     0x012000+1122,x
tmwCellNext:
                inc     tmwMapIdx            ; 16-bit (M=16 on both arrival paths)
                inx
                inx
                inx
                inx                          ; dst offset += 4 (one tile column)
                dec     tmwTx
                beq     tmwRowDone
                brl     tmwColTop
tmwRowDone:
                lda     tmwRowBase
                clc
                adc     #1280                ; 8 SHR rows x 160 = one tile row
                sta     tmwRowBase
                dec     tmwTy
                beq     tmwMark
                brl     tmwRowTop

tmwMark:
                ; One band union for the whole rectangle (over-marks
                ; TILE_MAP_SKIP cells -- safe, present just re-uploads
                ; clean pixels). Frame slots are dead now; reuse them:
                ; tmwMapIdx = yEnd, tmwTx = minWord, tmwTy = maxWord.
                lda     tmwByArg
                clc
                adc     tmwHtArg
                asl     a
                asl     a
                asl     a
                sta     tmwMapIdx            ; yEnd = (by + hTiles) * 8
                lda     tmwBx
                asl     a
                sta     tmwTx                ; minWord = bx * 2
                lda     tmwBx
                clc
                adc     tmwWtArg
                asl     a
                dec     a
                sta     tmwTy                ; maxWord = (bx + wTiles) * 2 - 1
                lda     tmwByArg
                asl     a
                asl     a
                asl     a
                tax                          ; X = first row = by * 8
                sep     #0x20
                .a8
tmwMarkLoop:
                lda     tmwTx
                cmp     gStageMinWord,x
                bcs     tmwNoMin
                sta     gStageMinWord,x      ; widen down
tmwNoMin:
                lda     tmwTy
                cmp     gStageMaxWord,x
                bcc     tmwNoMax
                beq     tmwNoMax
                sta     gStageMaxWord,x      ; widen up
tmwNoMax:
                inx
                cpx     tmwMapIdx
                bcc     tmwMarkLoop
                rep     #0x30
                .a16
                .i16
                tsc
                clc
                adc     #24                  ; drop frame (22) + parked bx (2)
                tcs
                pld
                plb
                plp
                rtl




; --------------------------------------------------------------------
; 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: iigsFillRectMark, iigsFillRectNoMark, iigsDrawPixelInner,
;            iigsDrawLineInner
; ====================================================================


; ====================================================================
; NATIVE-PERF Phase 6 W1: fused stage fillRect entries + shared mark.
;
; stageRectMark: shared fused dirty-mark tail for rect-shaped stage
; ops (fillRect now; the W1 fused sprite entries reopen this section
; and jsr here too -- a 16-bit jsr must NEVER cross .section blocks,
; see the fused-tile trailer note at .text.iigsFusedTile).
; Generalizes stageTileMark to N rows and a computed word band.
;
; In:  A = (maxWord << 8) | minWord (each 0..79), X = yStart (word),
;      Y = rowCount (word, >= 1). M=16, X=16.
; Out: M=16, X=16. Clobbers A, X, Y.
; Widens gStageMinWord/gStageMaxWord rows yStart..yStart+rowCount-1
; to cover [minWord, maxWord]. Byte compares in M=8 through the same
; DBR-inherited abs,x write path as stageTileMark /
; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; callers
; must NOT phk/plb before jsr-ing here).
; ====================================================================
.section .text.iigsFusedRect,"ax"
.globl stageRectMark
stageRectMark:
                sep     #0x20
                .a8
strmLoop:
                cmp     gStageMinWord,x      ; A (low byte) = minWord
                bcs     strmNoMin            ; A >= existing -> already covered
                sta     gStageMinWord,x      ; widen down
strmNoMin:
                xba                          ; A = maxWord
                cmp     gStageMaxWord,x
                bcc     strmNoMax            ; A < existing -> already covered
                beq     strmNoMax
                sta     gStageMaxWord,x      ; widen up
strmNoMax:
                xba                          ; back to minWord for the next row
                inx
                dey
                bne     strmLoop
                rep     #0x20
                .a16
                rts


; ====================================================================
; iigsFillRectMark  (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble)
; iigsFillRectNoMark(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 (pinned base + gRowOffsetLut row derive), then --
; Mark flavor only -- widen the dirty band rows y..y+h-1 to
; [x>>2, (x+w-1)>>2] via stageRectMark, replacing the old fill JSL +
; C-side surfaceMarkDirtyRect -> iigsMarkDirtyRowsInner JSL pair with
; ONE JSL. The NoMark twin (same body, flag skips the mark tail)
; serves callers that batch their own mark (jlDrawRect outline,
; jlFillCircle spans). Stage-gated by the C dispatch (port.h).
;
; ABI: clang cdecl. x arrives in A (parked in friX); y,w,h,nibble are
; on the stack (right-to-left). On JSL entry: y@4,s w@6,s h@8,s
; nib@10,s. After php+phb+phd (+4) and tsc+adc#8+tcd (D = SP+8), the
; equates are:
;   frm_y=0  frm_w=2  frm_h=4  frm_nib=6
; ====================================================================
.globl iigsFillRectNoMark
iigsFillRectNoMark:
frm_y   = 0
frm_w   = 2
frm_h   = 4
frm_nib = 6

                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                sta     friX                 ; park arg0 (x)
                stz     friDoMark            ; unfused arm (batched-mark callers)
                bra     frmBody

.globl iigsFillRectMark
iigsFillRectMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                sta     friX                 ; park arg0 (x)
                lda     #1
                sta     friDoMark
frmBody:
                tsc
                clc
                adc     #8
                tcd

; pxStart = x; pxEnd = x + w.
                lda     friX
                sta     friPxStart
                clc
                adc     frm_w
                sta     friPxEnd

; leadingByte = x >> 1; hasLeading = x & 1.
                lda     friX
                lsr     a
                sta     friLeadingByte
                lda     friX
                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     frm_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     frm_y
                asl     a
                tax
                lda     gRowOffsetLut,x
                clc
                adc     #0x2000
                sta     friCurRow

                lda     frm_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

; Row-invariant hoists (Phase 6 W1): the slam entry address, the slam
; X bias, the odd-middle-byte column, and its flag never change across
; rows -- computed once here instead of the old ~64-cyc-per-row
; recompute inside friMidNonZero. Harmless when midBytes == 0 (unused).
                lda     friMidBytes
                lsr     a
                sta     friSlamWords         ; nWords = midBytes >> 1
                lda     #80
                sec
                sbc     friSlamWords
                asl     a
                asl     a
                clc
                adc     #friMidSlamTbl-1
                sta     friSlamEntry         ; rts computed-goto target - 1
                lda     friSlamWords
                asl     a
                clc
                adc     friMidStart
                sta     friOddOff            ; midStart + 2*nWords (odd mid byte column)
                sec
                sbc     #160
                sta     friSlamBias          ; per row: X = friCurRow + friSlamBias
                lda     friMidBytes
                and     #1
                sta     friHasOddMid

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. The entry address and
; the X bias are row-invariant and hoisted above the row loop (Phase 6
; W1); per row this is push + add + rts (~30 cyc vs the old ~64). 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     friSlamEntry
                pha
                lda     friCurRow
                clc
                adc     friSlamBias
                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
; (flag + column hoisted above the row loop).
                lda     friHasOddMid
                beq     friMidNoOdd
                lda     friCurRow
                clc
                adc     friOddOff
                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:
; ----- fused dirty mark: rows y..y+h-1, band [x>>2, (x+w-1)>>2] -----
; friPxEnd = x + w with the ORIGINAL x/w (only friPxStart was nudged
; for the leading nibble), so the band covers the partial-nibble edges
; exactly like SURFACE_WORD_INDEX in the retired C-side mark.
                lda     friDoMark
                beq     frmSkipMark
                lda     friPxEnd
                dec     a
                lsr     a
                lsr     a                    ; maxWord = (x + w - 1) >> 2
                xba                          ; maxWord -> high byte (low byte 0)
                sta     friMarkBand
                lda     friX
                lsr     a
                lsr     a                    ; minWord = x >> 2
                ora     friMarkBand          ; A = (maxWord << 8) | minWord
                sta     friMarkBand          ; park the packed band
                lda     frm_y
                tax                          ; X = first row
                lda     frm_h
                tay                          ; Y = row count (>= 1 by contract)
                lda     friMarkBand
                jsr     stageRectMark
frmSkipMark:
                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
.section .bss.friX,"aw"
.globl friX
friX:
                .zero 2
.section .bss.friDoMark,"aw"
.globl friDoMark
friDoMark:
                .zero 2
.section .bss.friMarkBand,"aw"
.globl friMarkBand
friMarkBand:
                .zero 2
.section .bss.friSlamEntry,"aw"
.globl friSlamEntry
friSlamEntry:
                .zero 2
.section .bss.friSlamBias,"aw"
.globl friSlamBias
friSlamBias:
                .zero 2
.section .bss.friOddOff,"aw"
.globl friOddOff
friOddOff:
                .zero 2
.section .bss.friHasOddMid,"aw"
.globl friHasOddMid
friHasOddMid:
                .zero 2


; ====================================================================
; NATIVE-PERF Phase 6 W1: FUSED stage-sprite entries.
;
; jlSpriteDraw / jlSpriteRestoreUnder used to pay a C-side dirty mark
; (surfaceMarkDirtyRect -> iigsMarkDirtyRowsInner JSL, ~870 cyc for a
; 16-row sprite) plus an indirect C call into the codegen arena. Each
; entry here does the whole tail in ONE JSL: derive the stage
; destination from the pinned base ($01:2000 + gRowOffsetLut[y]),
; long-call the compiled arena routine (push target-1 + rtl -- the
; 65816 has no indirect JSL and self-modifying stubs stay banned,
; #19), then widen the dirty band via stageRectMark. C resolves
; fnAddr (resolved-fnAddr cache, spriteDispatch.h) and passes it as
; arg0; the entries never read ArenaSlotT.
; REOPENS .text.iigsFusedRect so the 16-bit jsr/pea self-references
; stay same-bank by construction (the fused-tile trailer rule; GAS
; same-name sections concatenate within the TU). Stage-gated by the C
; dispatch (s == gStage); callable ONLY from C (DBR must be the C
; data bank for the gStage* / gRowOffsetLut abs,x reads and the
; friMarkBand scratch). The compiled callees preserve D/DBR and
; return M=16/X=16 (spriteEmitIigs.c contract), so the dp-parked
; JSL args stay readable for the post-call band widen. No SEI.
; ====================================================================
.section .text.iigsFusedRect,"ax"

; ====================================================================
; iigsSpriteDrawMark(uint32_t fnAddr, uint16_t x, uint16_t y,
;                    uint16_t widthPx, uint16_t heightPx)
;
; ABI: arg0 fnAddr in A:X (A = arena routine low16, X = bank word).
;      x@4,s y@6,s w@8,s h@10,s on JSL entry. After php+phb+phd (+4)
;      and phx+pha parking fnAddr (+4), D = SP+1 and the equates below
;      hold. Long-calls the compiled DRAW routine (C ABI: A = dstRow0
;      low16, X = dstRow0 bank), then widens rows y..y+h-1 to
;      [x>>2, (x+w-1)>>2]. h is arbitrary (>16-row sprites fine);
;      caller guarantees fully-on-surface.
; ====================================================================
sdmFnLo = 0
sdmFnHi = 2
sdmX    = 11
sdmY    = 13
sdmW    = 15
sdmH    = 17
.globl iigsSpriteDrawMark
iigsSpriteDrawMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; fn bank word -> dp 2
                pha                          ; fn low16     -> dp 0
                tsc
                inc     a
                tcd                          ; D = SP + 1
                ; dstRow0 low16 = $2000 + gRowOffsetLut[y] + (x >> 1)
                lda     sdmY
                asl     a
                tax
                lda     sdmX
                lsr     a
                clc
                adc     gRowOffsetLut,x
                clc
                adc     #0x2000
                tay                          ; park dstRow0 low16 in Y
                ; long-call the compiled DRAW routine (push target-1 + rtl)
                phk                          ; return bank
                pea     sdmRet-1             ; return PC - 1
                sep     #0x20
                .a8
                lda     sdmFnHi              ; arena bank byte
                pha
                rep     #0x20
                .a16
                lda     sdmFnLo
                dec     a
                pha                          ; target - 1
                tya                          ; A = dstRow0 low16 (arg0 lo)
                ldx     #0x0001              ; X = stage bank $01 (arg0 hi)
                rtl                          ; jumps into the arena routine
sdmRet:
                ; callee returned M=16/X=16, D/DBR preserved, dp args intact
                lda     sdmX
                clc
                adc     sdmW
                dec     a
                lsr     a
                lsr     a                    ; maxWord = (x + w - 1) >> 2
                xba                          ; -> high byte
                sta     friMarkBand
                lda     sdmX
                lsr     a
                lsr     a                    ; minWord = x >> 2
                ora     friMarkBand          ; A = (maxWord << 8) | minWord
                ldx     sdmY                 ; X = first row
                ldy     sdmH                 ; Y = row count
                jsr     stageRectMark
                tsc
                clc
                adc     #4                   ; drop the parked fnAddr
                tcs
                pld
                plb
                plp
                rtl

; ====================================================================
; iigsSpriteRestoreMark(uint32_t fnAddr, uint16_t backupLo, uint16_t bx,
;                       uint16_t by, uint16_t bw, uint16_t bh)
;
; ABI: arg0 fnAddr in A:X; backupLo@4,s bx@6,s by@8,s bw@10,s bh@12,s
;      on JSL entry -> the equates below (same frame shape as draw).
; The compiled RESTORE routine is void copy(uint32_t packed) with
; packed = srcOff | dstOff<<16 -> A = srcOff (backup), X = dstOff
; (screen); its MVN banks are pre-patched by the C caller
; (spriteCompiledRestoreMark) and it restores DBR itself (MVN sets
; DBR = dst bank; the emitted phb/plb pair covers it). Screen offset
; derived here from the pinned stage base; then widens rows
; by..by+bh-1 to [bx>>2, (bx+bw-1)>>2]. bx/bw arrive even (validated
; by the caller); bh arbitrary.
; ====================================================================
isrmFnLo = 0
isrmFnHi = 2
isrmBkLo = 11
isrmBx   = 13
isrmBy   = 15
isrmBw   = 17
isrmBh   = 19
.globl iigsSpriteRestoreMark
iigsSpriteRestoreMark:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; fn bank word -> dp 2
                pha                          ; fn low16     -> dp 0
                tsc
                inc     a
                tcd                          ; D = SP + 1
                ; screen dstOff low16 = $2000 + gRowOffsetLut[by] + (bx >> 1)
                lda     isrmBy
                asl     a
                tax
                lda     isrmBx
                lsr     a
                clc
                adc     gRowOffsetLut,x
                clc
                adc     #0x2000
                tay                          ; park screen offset in Y
                ; long-call the compiled RESTORE routine
                phk
                pea     isrmRet-1
                sep     #0x20
                .a8
                lda     isrmFnHi
                pha
                rep     #0x20
                .a16
                lda     isrmFnLo
                dec     a
                pha
                tyx                          ; X = dstOff (screen)
                lda     isrmBkLo             ; A = srcOff (backup)
                rtl
isrmRet:
                lda     isrmBx
                clc
                adc     isrmBw
                dec     a
                lsr     a
                lsr     a                    ; maxWord = (bx + bw - 1) >> 2
                xba
                sta     friMarkBand
                lda     isrmBx
                lsr     a
                lsr     a                    ; minWord = bx >> 2
                ora     friMarkBand          ; A = (maxWord << 8) | minWord
                ldx     isrmBy               ; X = first row
                ldy     isrmBh               ; Y = row count
                jsr     stageRectMark
                tsc
                clc
                adc     #4                   ; drop the parked fnAddr
                tcs
                pld
                plb
                plp
                rtl


; ====================================================================
; iigsSpriteSaveStage(uint32_t fnAddr, uint16_t backupLo, uint16_t x,
;                     uint16_t y)
;
; Fused stage SAVE (perf-hunt-2 rank 10): the SAVE twin of
; iigsSpriteRestoreMark minus the mark tail (save is a read -- no band
; widen). Derives the SCREEN offset from the pinned stage base
; asm-side, replacing the C glue's row-offset pointer math +
; SPLIT_POINTER + indirect-fn-pointer call with this one JSL. The
; compiled SAVE routine is void copy(uint32_t packed) with
; packed = srcOff | dstOff<<16 -> A = srcOff (SCREEN), X = dstOff
; (backup); its MVN banks are pre-patched by the C caller and it
; restores DBR itself. x arrives CLIPPED EVEN (x & ~1, the C caller
; owns that), y in 0..199.
;
; P7-2 STAGE 2 WAS BUILT AND REVERTED HERE (2026-07-19): a 10-arg
; variant that also filled the backup metadata through one [dp] frame
; benched jlSpriteSaveUnder 722 -> 602 -- the llvm-mos C-side
; marshalling of 9 stack args + three SPLIT_POINTERs cost MORE than
; the far-pointer field stores it deleted (same compiler wall as the
; fnAddr-cache autopsy). Do not resurrect without a compiler-level
; change to argument lowering.
;
; ABI: arg0 fnAddr in A:X; backupLo@4,s x@6,s y@8,s on JSL entry ->
; the equates below (same frame shape as iigsSpriteRestoreMark:
; php+phb+phd then phx+pha, D = SP + 1, parked fn at dp 0..3).
; ====================================================================
isvsFnLo = 0
isvsFnHi = 2
isvsBkLo = 11
isvsX    = 13
isvsY    = 15
.globl iigsSpriteSaveStage
iigsSpriteSaveStage:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; fn bank word -> dp 2
                pha                          ; fn low16     -> dp 0
                tsc
                inc     a
                tcd                          ; D = SP + 1
                ; screen srcOff low16 = $2000 + gRowOffsetLut[y] + (x >> 1)
                lda     isvsY
                asl     a
                tax
                lda     isvsX
                lsr     a
                clc
                adc     gRowOffsetLut,x
                clc
                adc     #0x2000
                tay                          ; park screen offset in Y
                ; long-call the compiled SAVE routine
                phk
                pea     isvsRet-1
                sep     #0x20
                .a8
                lda     isvsFnHi
                pha
                rep     #0x20
                .a16
                lda     isvsFnLo
                dec     a
                pha
                ldx     isvsBkLo             ; X = dstOff (backup)
                tya                          ; A = srcOff (screen)
                rtl
isvsRet:
                tsc
                clc
                adc     #4                   ; drop the parked fnAddr
                tcs
                pld
                plb
                plp
                rtl

; ====================================================================
; 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.
; No PHB/PLB: DBR is never written here (PERF-AUDIT #53), so the frame
; is php(+1)+phd(+2)=+3. On JSL entry x@4,s y@6,s nib@8,s; after the 3
; push bytes: x@7,s y@9,s nib@11,s.
;   pix=0 (D-relative, = dpxlScratch+0)
;   xx=7,s  yy=9,s  nib=11,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
                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/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. Fold the LUT row directly
; into the add (adc gRowOffsetLut,x) -- no scratch round-trip (#53).
                lda     9,s                  ; y
                asl     a                    ; A = y*2 (LUT byte offset)
                tax
                lda     7,s                  ; x
                lsr     a                    ; A = x >> 1
                clc
                adc     gRowOffsetLut,x      ; A = x>>1 + y*160
                tay

                sep     #0x20                ; M=8
                .a8

                lda     7,s                  ; A = x low byte (parity in LSB)
                and     #1
                bne     dpxlOdd

; Even x -> high nibble.
                lda     11,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     11,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
                plp
                rtl


; ====================================================================
; iigsDrawPixelMark(uint16_t x, uint16_t y, uint16_t nibble)
;
; FUSED stage-only jlDrawPixel (NATIVE-PERF Phase 2 R10): one JSL does
; the single-row dirty widen (word x>>2, row y -- same abs,x
; DBR-inherited write path as iigsMarkDirtyRowsInner) and the nibble
; RMW plot against the pinned stage base ($01:2000 + gRowOffsetLut[y]
; + (x>>1), long,x with the literal folded in). Caller (jlDrawPixel)
; has clipped and owns the s == gStage gate; the batching plotters
; (plotPixelNoMark for lines/circles) stay on iigsDrawPixelInner.
;
; ABI: x in A; y @4,s, nibble @6,s on JSL entry. Frame is php (+1)
; plus the parked x (+2): x @1,s, y @7,s, nibble @9,s. No PHB (DBR
; never written, PERF-AUDIT #53 precedent), no D use. Exits M=16/X=16.
; No intra-asm jsr, so the section is self-contained (cross-section
; 16-bit jsr is a wild jump -- see the fused-tile block note).
; ====================================================================
.section .text.iigsDrawPixelMark,"ax"
.globl iigsDrawPixelMark
iigsDrawPixelMark:
                php
                rep     #0x30
                .a16
                .i16
                pha                          ; park x; y -> 7,s nib -> 9,s
                ; --- single-row widen: A still holds x ---
                lsr     a
                lsr     a
                tay                          ; Y = wordIdx = x>>2 (<= 79)
                lda     7,s                  ; y
                tax                          ; X = row
                tya                          ; A = wordIdx
                sep     #0x20
                .a8
                cmp     gStageMinWord,x
                bcs     dpxmNoMin
                sta     gStageMinWord,x      ; widen down
dpxmNoMin:
                cmp     gStageMaxWord,x
                bcc     dpxmNoMax
                beq     dpxmNoMax
                sta     gStageMaxWord,x      ; widen up
dpxmNoMax:
                rep     #0x20
                .a16
                ; --- plot: byte offset = gRowOffsetLut[y] + (x>>1) ---
                lda     7,s                  ; y
                asl     a
                tax
                lda     1,s                  ; x
                lsr     a
                clc
                adc     gRowOffsetLut,x
                tax                          ; X = stage byte offset
                sep     #0x20
                .a8
                lda     1,s                  ; x low byte (parity in LSB)
                and     #1
                bne     dpxmOdd
                lda     9,s                  ; nibble -> high half (left pixel)
                asl     a
                asl     a
                asl     a
                asl     a
                sta     dpxlNibPart
                lda     0x012000,x
                and     #0x0F
                ora     dpxlNibPart
                sta     0x012000,x
                bra     dpxmDone
dpxmOdd:
                lda     9,s                  ; nibble -> low half (right pixel)
                and     #0x0F
                sta     dpxlNibPart
                lda     0x012000,x
                and     #0xF0
                ora     dpxlNibPart
                sta     0x012000,x
dpxmDone:
                rep     #0x30
                .a16
                .i16
                pla                          ; drop parked x
                plp
                rtl


.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 = bank0 scratch)

                php
                phb
                phd
                rep     #0x30
                .a16
                .i16

; Stash the pixels far pointer via D-RELATIVE stores (AFTER tcd): a plain
; `sta dlnScratch` writes the LOAD bank (DBR) while [pix],y reads through
; D, which is ALWAYS bank 0 -- the two disagreed, so every plot went
; through a garbage bank-0 pointer (PERF-AUDIT.md #83: wrong pixels
; always; #86: a layout shift let the plots stomp the dln* walk state,
; hanging the Bresenham). Identical fix to iigsDrawPixelInner /
; iigsDrawCircleInner; phx/pha keep the N,s arg offsets unchanged.
                phx                          ; save pix bank
                pha                          ; save pix offset
                lda     #dlnScratch
                tcd                          ; D = dlnScratch (bank 0); [pix],y == [0],y
                pla
                sta     pix+0                ; D-relative -> bank0 scratch+0
                pla
                sta     pix+2                ; D-relative -> bank0 scratch+2

; 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. 65816 is little-endian: byte[Y] is the LOW byte
; of A, so $FFF0 clears the target's low nibble and preserves the whole
; adjacent byte at Y+1; OR sets nib into the low nibble. (The old $F0FF
; had the bytes swapped: it never cleared the target nibble and erased
; the neighbor's low nibble -- PERF-AUDIT.md #87.)
                lda     [pix],y
                and     #0xFFF0
                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 byte at Y+1
; (16-bit load; the 65816 is little-endian so byte[Y] is the LOW byte
; of A), masks so only the target nibble changes ($FF0F clears the
; target's high nibble for even x, $FFF0 clears the target's low
; nibble for odd x -- both preserve the whole adjacent byte), then
; writes both bytes back. (The old odd mask $F0FF had its bytes
; swapped and corrupted the neighbor -- PERF-AUDIT.md #87.)

; 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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 #0xFFF0
	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 nibble)
;
; 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).
;
; nibble is the raw fill color (pre-masked to 0x0..0xF by the
; jlpFillCircle macro); the doubled fill byte and the two RMW nibble
; bytes are derived HERE (shift/ORA, cheaper than it was to unpack the
; old pre-multiplied fillWord arg) so C never pays the * 0x1111
; software-multiply helper for a runtime color (PERF-AUDIT #41).
;
; 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,nibble -> on the stack. JSL entry cx@4,s cy@6,s r@8,s
;               nibble@10,s. After php+phb+phd (+4): cx@8,s cy@10,s
;               r@12,s nibble@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                       ; nibble   @ 14,s
iigsFillCircleInner:
	php
	phb
	phd
	rep #0x30
	.a16
	.i16

; Stash the pix far pointer via D-RELATIVE stores (AFTER tcd) -- a plain
; `sta fcPixPtr` writes the LOAD bank (DBR) while [fpix],y reads through
; D = bank 0 (the #83/#86 garbage-plot-pointer bug; same fix as
; iigsDrawPixelInner). phx/pha keep the N,s arg offsets unchanged.
	phx                          ; save pix bank
	pha                          ; save pix offset
	lda #fcPixPtr
	tcd                          ; D = fcPixPtr (bank 0); [fpix],y == [0],y
	pla
	sta 0                        ; D-relative -> bank0 scratch+0 (fpix lo16)
	pla
	sta 2                        ; D-relative -> bank0 scratch+2 (bank byte)

; Derive the fill bytes from the raw nibble arg (#41).
; fcFillByte = doubled byte (full byte fills + low nibble), e.g. $33.
; fcFillHi = (nibble << 4) only (high nibble RMW).
; fcFillLo = nibble only (low nibble RMW).
	lda fcArgFill, s             ; A = nibble (pre-masked 0x0..0xF)
	sta fcFillLo
	asl a
	asl a
	asl a
	asl a                        ; A = nibble << 4
	sta fcFillHi
	ora fcFillLo
	sta fcFillByte               ; doubled byte e.g. $33

; 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(void)
;
; SHR pixel upload: 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.
;
; SCB/palette upload does NOT live here: uploadScbAndPaletteIfNeeded
; (hal.c) owns it via dirty-flag-gated C memcpy before this is called.
; The old scbPtr/palettePtr/uploadFlags args and their flag-gated MVN
; upload blocks were dead (uploadFlags was hard-coded 0 at the only
; call site) and were deleted along with the per-present argument
; marshalling they required (PERF-AUDIT #52).
;
; ABI: no args. Phase 3 (the slam) clobbers D freely via TCD to row
; bases; the final PLD restores the caller's D (saved by our PHD) and
; PLB restores DBR (the narrow-row MVNs leave DBR=$E1).
; ====================================================================
.section .text.iigsBlitStageToShr,"ax"
.globl iigsBlitStageToShr
iigsBlitStageToShr:
; Slam-run eligibility floor (NATIVE-PERF W2): a dirty row narrower
; than PEI_RUN_MIN words always takes the exact-extent MVN. With the
; run hoist in place (one softswitch set per RUN of consecutive
; eligible rows instead of 6 slow I/O per row), the old ">= 33 words"
; promotion rule is replaced by the two-term predicate
;     W >= PEI_RUN_MIN  &&  (max+1) <= W + W/2
; i.e. promote when the words the slam pushes cost no more than ~1.5x
; the words MVN would move (the conservative envelope of the fitted-
; MAME and real-HW write-cost slopes; derivation in NATIVE-PERF-
; PLAN.md W2). The old rule both UNDER-promoted left-anchored
; mid-width bands and OVER-promoted right-anchored ones.
PEI_RUN_MIN     = 8

                php
                phb
                phd
                rep     #0x30
                .a16
                .i16

                ; DBR must be the CALLER's data bank (= the bank our globals
                ; 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.
                ; (The old $E1:9DFE DBR stash is gone: the narrow-row MVN
                ; now reloads DBR from the phb copy at 3,s. $E1:9DFE is
                ; reused as the slam run's SP-offset park -- the ONLY datum
                ; the switches-on run loop must load, and long addressing
                ; (effective bank $E1) is immune to the RAMRD/RAMWRT
                ; redirects. It sits in the unused SCB tail $9DC8-$9DFF,
                ; above the last pixel byte $9CFF; UBER's mailbox uses
                ; $9DC8-$9DCE -- no overlap.)

; 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

; ---------------------------------------------------------------
; Chunked-SEI scan. Contract UNCHANGED: sei per chunk, <= 40 rows
; consumed per chunk, cli at chunk end so the latched VBL IRQ catches
; up (this is what keeps the GetTick bench clock honest). Runs are
; clipped at the chunk budget, so no SEI hold got longer -- every row
; got cheaper, so holds strictly shrink.
; The whole scan runs in 8-bit M: bands are bytes, and the old
; per-row sep/rep pairs (3 per dirty row) are gone. Shadow/AUXWRITE/
; RAMRD stay scoped to the slam RUNS below -- the scan reads bank-0
; globals, which the switches would misdirect into the stage pixels.
; ---------------------------------------------------------------
peiChunkBegin:
                ldy     #0                   ; Y = in-chunk row counter
                sei
                sep     #0x20
                .a8
peiRowLoop:
; TOUCHED test (surfaceInternal.h STAGE_DIRTY_ROW_TOUCHED): a row is
; CLEAN iff gStageMinWord[y] == 0xFF. Valid under the clipped-marks
; invariant -- every real mark writes min <= 79 (STAGE_DIRTY_FULL_MAX),
; and stageDirtyClearAll is the only writer of 0xFF. Sentinel pin
; (finding #47) updated: this walk now relies on min==0xFF <=> clean;
; do not change STAGE_DIRTY_CLEAN_MIN without updating this asm.
                lda     gStageMinWord,x
                inc     a
                beq     peiRowClean          ; 0xFF -> 0x00 -> clean
                ; dirty: A = W-1 = max - min
                lda     gStageMaxWord,x
                sec
                sbc     gStageMinWord,x
                cmp     #PEI_RUN_MIN - 1
                bcc     peiMvnRowBr          ; W < PEI_RUN_MIN -> MVN
                ; slam-eligibility: (max+1) <= W + W/2
                inc     a                    ; A = W
                sta     gPeiWidthTmp
                lsr     a
                clc
                adc     gPeiWidthTmp         ; A = W + W/2  (<= 120, fits)
                cmp     gStageMaxWord,x
                beq     peiMvnRowBr          ; 1.5W == max -> max+1 > 1.5W
                bcs     peiRunScan           ; 1.5W >  max -> slam run
peiMvnRowBr:
                brl     peiMvnRow
peiRowClean:
                inx
                iny
peiRowBounds:
                cpx     #200
                bcs     peiChunkDone
                cpy     #40
                bcc     peiRowLoop
peiChunkDone:
; Per-chunk teardown is now JUST the CLI: runs restore SP and switches
; themselves, and MVN rows never touch either. (The old unconditional
; SP+shadow+RAMWRT+RAMRD teardown -- 3 slow I/O stores per chunk even
; on all-clean chunks -- is gone.)
                cli
                cpx     #200
                bcc     peiChunkNext
                brl     peiAllDone
peiChunkNext:
                brl     peiChunkBegin

; ---------------------------------------------------------------
; Slam run: gather (switches OFF, bank 0 readable), then one toggle
; set for the whole run. Rows are slammed DESCENDING so a full-width
; burst leaves SP exactly on the next row's top byte.
; ---------------------------------------------------------------
peiRunScan:                                  ; a8; X = first run row
                rep     #0x20
                .a16
                txa
                sta     gPeiRunFirst
                sep     #0x20
                .a8
                lda     gStageMaxWord,x
                sta     gPeiRunMax           ; low byte only (readers mask);
                                             ; never store 16-bit here -- the
                                             ; high byte is BSS-zero by
                                             ; construction
peiRunGather:
                inx
                iny
                cpx     #200
                bcs     peiRunGathered
                cpy     #40
                bcs     peiRunGathered       ; chunk budget clips the run
                lda     gStageMinWord,x
                inc     a
                beq     peiRunGathered       ; clean row ends the run
                lda     gStageMaxWord,x
                sec
                sbc     gStageMinWord,x
                cmp     #PEI_RUN_MIN - 1
                bcc     peiRunGathered       ; narrow row ends the run
                inc     a
                sta     gPeiWidthTmp
                lsr     a
                clc
                adc     gPeiWidthTmp
                cmp     gStageMaxWord,x
                beq     peiRunGathered       ; fails (max+1) <= 1.5W
                bcc     peiRunGathered
                lda     gStageMaxWord,x
                cmp     gPeiRunMax
                bcc     peiRunGather
                sta     gPeiRunMax           ; widen the run's entry word
                bra     peiRunGather
peiRunGathered:                              ; X/Y = scan resume values
                rep     #0x30
                .a16
                .i16
                txa
                sta     gPeiCurRow           ; resume: absolute row
                tya
                sta     gPeiChunkRow         ; resume: in-chunk row
                txa
                sec
                sbc     gPeiRunFirst
                sta     gPeiRunLen           ; rows in this run (1..40)
                ; STAGE B: computed entry. spOffset = 2*runMax + 1, parked
                ; at $E1:9DFE (the ONLY datum the switches-on loop loads);
                ; the burst enters the PEI table at word runMax, pushing
                ; words runMax..0 instead of always 79..0. Words left of
                ; the run's min are clean (stage == display there), so the
                ; over-push of them writes identical bytes.
                lda     gPeiRunMax
                and     #0x00FF
                asl     a
                inc     a                    ; A = 2*runMax + 1
                sta     0xE19DFE
                inc     a
                sta     gPeiRunBytes         ; telemetry: bytes pushed/row
                ; D = base of the run's LAST (highest, slammed first) row
                lda     gPeiCurRow
                dec     a
                asl     a
                tax
                lda     gRowOffsetLut,x
                clc
                adc     #0x2000
                tcd                          ; D = last row base (A kept)
                clc
                adc     0xE19DFE
                tcs                          ; SP = rowBase + 2*runMax + 1
                ; X = PEI table entry byte offset = 158 - 2*runMax
                lda     #160
                sec
                sbc     gPeiRunBytes
                tax
                ldy     gPeiRunLen           ; Y = row countdown
                ; toggles ON -- LAST bank-0 access above this line
                sep     #0x20
                .a8
                lda     gPeiOrigShadow
                and     #0xF7                ; clear bit 3 -> SHR shadow ON. SHR
                                             ; shadowing mirrors $2000-9FFF for
                                             ; BOTH banks; the burst pushes into
                                             ; aux $01 (RAMWRT) so it lands at $E1.
                sta     0xE0C035
                lda     #0
                sta     0xE0C005             ; RAMWRT on: pushes land in $01
                sta     0xE0C003             ; RAMRD on: PEI reads $01 stage
                rep     #0x20
                .a16
                jmp     (peiSlamJmpTab,x)    ; enter the table at word runMax

; Computed-entry jump table: byte offset 2k -> the PEI that pushes
; word 79-k. Same section => same OMF segment => same bank as the jmp
; (the cross-section 16-bit-transfer hazard does not apply).
peiSlamJmpTab:
.set peiK, 0
.rept 80
                .word   peiSlamTab + peiK
.set peiK, peiK + 2
.endr

peiSlamTab:
; 80 PEIs from DP+$9E down to DP+$00 (pushed words 79..0). The
; computed-entry table above maps byte offset 2k to the PEI that
; pushes word 79-k.
                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

; Row complete: SP == rowBase - 1. Countdown and step D one row down.
; Registers only + one long $E1 read -- NO bank-0 access (switches on).
                dey
                beq     peiRunDone
                tdc
                sec
                sbc     #160
                tcd                          ; D = next (lower) row base
                cpx     #0
                beq     peiRunStepFull       ; full-width: SP already == D+159
                clc
                adc     0xE19DFE             ; SP = D + 2*runMax + 1
                tcs
peiRunStepFull:
                jmp     (peiSlamJmpTab,x)
peiRunDone:
                ; switches OFF first, then bank 0 is readable again
                sep     #0x20
                .a8
                lda     #0
                sta     0xE0C004             ; RAMWRT off
                sta     0xE0C002             ; RAMRD off
                lda     gPeiOrigShadow
                sta     0xE0C035             ; shadow restored
                rep     #0x30
                .a16
                .i16
                lda     gPeiOrigSp
                tcs                          ; caller stack back
                ; telemetry (W2 Stage 0): gPresentCopiedBytes +=
                ; runLen * bytesPerRow. Repeated add (runLen <= 40),
                ; ~13 cyc per slammed row -- accepted for verifiability.
                ; DBR is the caller's globals bank (slam never touches
                ; DBR), so the absolute counter access is safe.
                ldx     gPeiRunLen
                lda     gPresentCopiedBytes
peiRunCntLoop:
                clc
                adc     gPeiRunBytes
                bcc     peiRunCntNext
                inc     gPresentCopiedBytes+2
peiRunCntNext:
                dex
                bne     peiRunCntLoop
                sta     gPresentCopiedBytes
                ; resume the scan
                lda     gPeiCurRow
                tax
                lda     gPeiChunkRow
                tay
                sep     #0x20
                .a8
                brl     peiRowBounds

; ---------------------------------------------------------------
; Narrow dirty row: exact-extent MVN of [minWord..maxWord], $01 -> $E1.
; No switches, no SP hijack. DBR reload comes from our own phb copy at
; 3,s (php=+1 phb=+1 phd=+2 frame), replacing the old slow $E1:9DFE
; read. GAS spells MVN dst,src so `mvn 0xE1, 0x01`.
; ---------------------------------------------------------------
peiMvnRow:                                   ; a8; X = row, Y = chunk count
                rep     #0x20
                .a16
                txa
                sta     gPeiCurRow
                tya
                sta     gPeiChunkRow
                txa
                asl     a
                tay                          ; Y = LUT byte index (X = row kept)
                sep     #0x20
                .a8
                lda     gStageMaxWord,x
                sec
                sbc     gStageMinWord,x
                rep     #0x20
                .a16
                and     #0x00FF
                asl     a
                inc     a
                sta     gPeiMvnCount         ; MVN count arg = bytes - 1
                sep     #0x20
                .a8
                lda     gStageMinWord,x
                rep     #0x20
                .a16
                and     #0x00FF
                asl     a
                clc
                adc     gRowOffsetLut,y
                adc     #0x2000              ; no carry possible: sum < $9D00
                tax
                txy                          ; src offset == dst offset
                lda     gPeiMvnCount
                mvn     0xE1, 0x01
                ; MVN left DBR=$E1; restore the caller's (= globals) bank
                ; from the phb copy. FRAME-LAYOUT DEPENDENCY: 3,s is the
                ; phb byte ONLY while nothing extra is pushed around the
                ; scan loop -- any new push above this point must adjust
                ; the offset.
                sep     #0x20
                .a8
                lda     3,s                  ; phb copy: caller DBR
                pha
                plb
                rep     #0x20
                .a16
                ; telemetry (W2 Stage 0): the MVN moved gPeiMvnCount + 1
                ; bytes (the MVN count argument is bytes - 1); sec + adc
                ; adds the + 1. sta does not touch carry, so the bcc
                ; still tests the add.
                lda     gPresentCopiedBytes
                sec
                adc     gPeiMvnCount
                sta     gPresentCopiedBytes
                bcc     peiMvnCntDone
                inc     gPresentCopiedBytes+2
peiMvnCntDone:
; Restore row counters and advance.
                lda     gPeiCurRow
                tax
                inx
                lda     gPeiChunkRow
                tay
                iny
                sep     #0x20
                .a8
                brl     peiRowBounds

peiAllDone:
                ; Arrived from peiChunkDone's brl in 8-bit M (the W2 scan
                ; runs a8 throughout); widen before the counter add.
                rep     #0x30
                .a16
                .i16
                ; telemetry (W2 Stage 0): every present examines all 200
                ; band rows exactly once (no early-out exists on IIgs).
                lda     gPresentScannedRows
                clc
                adc     #200
                sta     gPresentScannedRows
                bcc     peiScanCntDone
                inc     gPresentScannedRows+2
peiScanCntDone:
                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


; ====================================================================
; iigsStageBandsClearAll(void) / iigsStageBandsMarkAll(void)
;
; Constant-fill BOTH dirty-band arrays (gStageMinWord/gStageMaxWord,
; 200 bytes each) without libc memset. llvm-mos lowers memset to
; `jsl memset` -- libc's `while (n--) *d++ = c` byte loop behind a far
; call (finding #79 class) -- and stageDirtyClearAll runs after EVERY
; present, so the two memsets were ~10 ms of every present's fixed
; base. A C word-fill rewrite still emitted ~35 instructions per store
; (llvm-mos re-derives the far-pointer address each iteration), so the
; fill lives here: 100 descending abs,x word stores per array,
; ~12 cyc/word.
;
;   ClearAll: min = $FF (STAGE_DIRTY_CLEAN_MIN), max = $00 (CLEAN_MAX)
;   MarkAll:  min = $00, max = $4F (STAGE_DIRTY_FULL_MAX = 79)
;
; ABI: no args, no return. Same DBR-inherited abs,x write path as
; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT
; phk/plb). php/plp preserves caller widths; A/X are cdecl scratch.
; SURFACE_HEIGHT = 200 -> last word offset 198; `bpl` exits after the
; offset-0 store wraps X to $FFFE (N set).
; ====================================================================
.section .text.iigsStageBands,"ax"
.globl iigsStageBandsClearAll
iigsStageBandsClearAll:
                php
                rep     #0x30
                .a16
                .i16
                ldx     #198
                lda     #0xFFFF              ; CLEAN_MIN in both bytes
isbcMinLoop:
                sta     gStageMinWord,x
                dex
                dex
                bpl     isbcMinLoop
                ldx     #198
                lda     #0                   ; CLEAN_MAX in both bytes
isbcMaxLoop:
                sta     gStageMaxWord,x
                dex
                dex
                bpl     isbcMaxLoop
                plp
                rtl

.globl iigsStageBandsMarkAll
iigsStageBandsMarkAll:
                php
                rep     #0x30
                .a16
                .i16
                ldx     #198
                lda     #0                   ; minWord 0 in both bytes
isbmMinLoop:
                sta     gStageMinWord,x
                dex
                dex
                bpl     isbmMinLoop
                ldx     #198
                lda     #0x4F4F              ; STAGE_DIRTY_FULL_MAX in both bytes
isbmMaxLoop:
                sta     gStageMaxWord,x
                dex
                dex
                bpl     isbmMaxLoop
                plp
                rtl


; ====================================================================
; iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count)
;
; Fill `count` bytes at far pointer `dst` with the byte value
; replicated in BOTH bytes of `fill`. Replaces llvm-mos memset
; lowering (`jsl memset` far-call byte loop) for the SCB range fill;
; [dp],y long-indirect stores ignore DBR and the 65816 has no
; alignment fault, so any dst parity works. Odd trailing byte is
; stored first in M=8, then descending word stores. count >= 1
; (caller guards the empty range) and count < 32768 (the descending
; loop exits on N -- fine for the 200-byte SCB and any surface row).
;
; ABI: arg0 dst in A(lo16):X(bank). Stack after JSL: fill @4,s,
; count @6,s; after php+phd+phx+pha (+7): fill @11,s, count @13,s.
; D = SP via tsc/tcd, and SP points at the next FREE byte, so the
; pushed dst long pointer's bytes (lo, hi, bank -- pha last, phx
; first) sit at D+1..D+3: ibfDst = 1, NOT 0. (The 0 equate read one
; garbage byte into the pointer's low byte and slid the rest, writing
; the fill into a layout-roulette address -- DRAW happened to survive
; it, UBER crashed at boot.)
; ====================================================================
.globl iigsByteFill
iigsByteFill:
ibfDst   = 1

                php
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; dst bank word  -> D+2..3
                pha                          ; dst lo16       -> D+0..1
                tsc
                tcd                          ; D = SP: [ibfDst] = dst far ptr
                lda     13,s                 ; count
                tay
                lsr     a
                bcc     ibfEven              ; even count
                dey                          ; Y = count-1 = offset of odd tail
                sep     #0x20
                .a8
                lda     11,s                 ; fill value (low byte)
                sta     [ibfDst],y
                rep     #0x20
                .a16
                tya                          ; A = even remainder
                bra     ibfWords
ibfEven:
                tya                          ; A = count
ibfWords:
                beq     ibfDone              ; count was 1 -> only the tail
                tay
                dey
                dey                          ; Y = last word offset
                lda     11,s                 ; fill word
ibfLoop:
                sta     [ibfDst],y
                dey
                dey
                bpl     ibfLoop
ibfDone:
                pla                          ; drop dst lo16
                plx                          ; drop dst bank
                pld
                plp
                rtl


; ====================================================================
; iigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank,
;                  uint16_t rowBytes, uint16_t rows)
;
; Copy `rows` rows of `rowBytes` bytes from src to dst, both with the
; 160-byte surface stride (hunt-3 rank-1 finding: the C word-copy
; override in jlpSurfaceCopyRect re-derived BOTH far pointers per word
; on llvm-mos -- ~30+ instructions/word, 5.3 ms per 16x16 erase; this
; [dp],y pair loop is ~21 cyc/word). CONTRACT (single caller,
; jlpSurfaceCopyRect via the jlSurfaceCopyRect wrapper): rowBytes is
; EVEN (the wrapper widens w to 16-px groups) and >= 2, rows >= 1,
; rowBytes <= 160. Row advance propagates carry into the bank byte,
; so pixel buffers that cross a 64KB bank stay correct.
;
; ABI: arg0 dst in A:X; srcLo@4,s srcBank@6,s rowBytes@8,s rows@10,s
; on JSL entry. Frame: php+phb+phd, push dstBank/dstLo then
; srcBank/srcLo (both loads land at 14,s as the stack grows),
; tsc/inc/tcd -> D = SP+1: dp0..3 = src far ptr, dp4..7 = dst far
; ptr; args at dp N = stack (N+1),s.
; ====================================================================
icrSrc      = 0
icrDst      = 4
icrRowBytes = 19
icrRows     = 21
.globl iigsCopyRectRows
iigsCopyRectRows:
                php
                phb
                phd
                rep     #0x30
                .a16
                .i16
                phx                          ; dst bank word -> dp 6..7
                pha                          ; dst lo16      -> dp 4..5
                lda     14,s                 ; src bank word
                pha                          ;               -> dp 2..3
                lda     14,s                 ; src lo16
                pha                          ;               -> dp 0..1
                tsc
                inc     a
                tcd                          ; D = SP + 1
                ldx     icrRows
icrRowLoop:
                ldy     icrRowBytes
                dey
                dey                          ; Y = rowBytes - 2 (even)
icrWordLoop:
                lda     [icrSrc],y
                sta     [icrDst],y
                dey
                dey
                bpl     icrWordLoop
                ; advance both far pointers one 160-byte surface row;
                ; carry rides from the 16-bit add into the bank byte
                ; (sta/sep/lda do not touch C).
                lda     icrSrc
                clc
                adc     #160
                sta     icrSrc
                sep     #0x20
                .a8
                lda     icrSrc+2
                adc     #0
                sta     icrSrc+2
                rep     #0x20
                .a16
                lda     icrDst
                clc
                adc     #160
                sta     icrDst
                sep     #0x20
                .a8
                lda     icrDst+2
                adc     #0
                sta     icrDst+2
                rep     #0x20
                .a16
                dex
                bne     icrRowLoop
                tsc
                clc
                adc     #8                   ; drop parked src + dst
                tcs
                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<maxSp. Increments sp.
; M=16 X=16 on entry/exit; preserves caller A/Y via PHA/PHY.
wsPushXY:
                pha
                phy
                lda     wsSp
                cmp     wsMaxSpCache
                bcs     wsPushSkip
                asl     a
                tay
                lda     wsPushX
                sta     [wsStackX],y
                lda     wsScanY
                sta     [wsStackY],y
                lda     wsSp
                inc     a
                sta     wsSp
wsPushSkip:
                ply
                pla
                rts


; ====================================================================
; Data symbols defined by these functions (DRAWPRIMS scratch globals).
; gRowOffsetLut / gFloodSeedMatch / gFloodLeftX / gFloodRightX,
; gStageMinWord / gStageMaxWord, gPei* and gKey/gMouse/gJoy*Button*
; are defined ELSEWHERE (asm or C); they are left as externs.
; ====================================================================

; Private bank-0 D-frame for iigsFloodWalkAndScansInner (28 args bytes;
; pad to a clean 32).
.section .bss.wsFrame,"aw"
.globl wsFrame
wsFrame:
.zero 32

; iigsPollJoystickInner has NO output globals: it returns its result in
; registers (uint32_t in A:X). An asm WRITE of a global does not reach the
; C-read address in this toolchain (asm READING a C-defined global like
; gStageMinWord is fine; asm WRITING one that C reads is not), so the
; register-return ABI is used instead.

; iigsFloodWalkAndScansInner scratch.
.section .bss.wsLeftX,"aw"
.globl wsLeftX
wsLeftX:
.zero 2

.section .bss.wsRightX,"aw"
.globl wsRightX
wsRightX:
.zero 2

.section .bss.wsScanY,"aw"
.globl wsScanY
wsScanY:
.zero 2

.section .bss.wsMatchByte,"aw"
.globl wsMatchByte
wsMatchByte:
.zero 2

.section .bss.wsNewByte,"aw"
.globl wsNewByte
wsNewByte:
.zero 2

.section .bss.wsEqByte,"aw"
.globl wsEqByte
wsEqByte:
.zero 2

.section .bss.wsSp,"aw"
.globl wsSp
wsSp:
.zero 2

.section .bss.wsMaxSpCache,"aw"
.globl wsMaxSpCache
wsMaxSpCache:
.zero 2

.section .bss.wsSpanLen,"aw"
.globl wsSpanLen
wsSpanLen:
.zero 2

.section .bss.wsPushX,"aw"
.globl wsPushX
wsPushX:
.zero 2

.section .bss.wsNewHigh,"aw"
.globl wsNewHigh
wsNewHigh:
.zero 2

.section .bss.wsDoubledByte,"aw"
.globl wsDoubledByte
wsDoubledByte:
.zero 2

.section .bss.wsHasTrail,"aw"
.globl wsHasTrail
wsHasTrail:
.zero 2


; ---------------------------------------------------------------------
; iigsQuitGS -- issue GS/OS QuitGS ($2029) via the inline convention
; and never return on success. Pure constants on purpose: the first
; workaround built this stub in a C BSS array and hit the cross-bank
; DBR trap (byte stores went DB-relative to bank $08 while the jsl
; resolved to the array's true bank $10 -- executed zeros, BRK). The
; parameter block lives at the FIXED scratch address $E1:9DCC (inside
; the $E1:9DC8-$9DFD survives-present window; present parks $9DFE) and
; the C caller writes its pCount=0 word before calling, so this
; routine contains no relocations at all. If QuitGS errors, we rtl
; and the caller falls back to the (broken) runtime exit path.
	.section .text.iigsQuitGS,"ax"
	.globl iigsQuitGS
iigsQuitGS:
	jsl 0xE100A8
	.word 0x2029
	.long 0x00E19DCC
	rtl
