| Atari ST fused dirty present -- 68000 hand-rolled (NATIVE-PERF W2).
|
| ONE pass over the 200 band rows replaces jlpPresent's three C passes
| (union scan + per-row memcpy copy loop + snapshot loop):
|   * 4-row fast path: one long compare against the CLEAN sentinel
|     pattern on cur AND prev min bytes skips 4 rows in ~30 cyc/row.
|     Valid under the clipped-marks invariant (surfaceInternal.h:
|     min != 0xFF iff touched; untouched rows hold exactly (FF,00));
|     prev arrays only ever hold snapshot copies of cur, so a clean
|     prev group is also exactly (FF,00) and the snapshot is a no-op.
|   * slow path per row: union(cur,prev) band, snapshot prev <- cur,
|     copy the group-rounded band via a jsr'd move.l chain
|     (5.0 cyc/B, no memcpy dispatch overhead below 256 B).
|   * full-width rows collect into runs; a closed run >= 1 row goes
|     through _memcpy whose movem.l copy256 path (~4.3 cyc/B) wins
|     for >= 256 B contiguous spans (present-after-clear case). Runs
|     stay open across 4-row group boundaries (closing at every group
|     head would cap runs at 640 B and cost ~50 memcpy dispatches on a
|     full-screen present); only a clean group, a partial row, a clean
|     row, or the end of the walk closes one.
| Cur bands are NOT cleared here -- core stageDirtyClearAll still owns
| that (jlStagePresent contract unchanged).
|
| ABI: cdecl. d2-d7/a2-a6 callee-save.
|
|   uint32_t present68kStSlam(const uint8_t *srcBase, uint8_t *dstBase);
|
| Return d0: low 16 bits = bytes copied (max 32,000 fits),
|            bit 16      = "current frame marked at least one row"
|                          (the gPrevAnyDirty snapshot value).
|
| Register allocation:
|   a0 = gStageMinWord walker        a1 = gStageMaxWord walker
|   a2 = gPrevStageMinWord walker    a3 = gPrevStageMaxWord walker
|   a4 = src row cursor (shadow)     a5 = dst row cursor (back page)
|   a6 = copy chain base
|   d3 = open full-width-run src start address (0 = no run)
|   d4 = 0xFFFFFFFF fast-path constant
|   d5 = AND-accumulator of cur min bytes (FF iff no row touched)
|   d6 = 4-row group counter (dbra)
|   d7 = copied-bytes accumulator
|   d0-d2 = row scratch
| a4/a5 advance in lockstep on every path, so (a5 - a4) is the
| constant dst-src delta -- run close derives its dst pointer from it.
| Geometry (200 rows, 160 B/row, 20 groups, 4 rows/long) is guarded by
| a compile-time typedef in hal.c.

                .text

                .equ    SP_PS_SAVED, 44
                .equ    SP_PS_SRC,   SP_PS_SAVED + 4
                .equ    SP_PS_DST,   SP_PS_SAVED + 8


| ---- PS_ROW: one slow-path row (union + snapshot + copy) ----
| moveq #0 before every byte load: partial-register hygiene (word math
| on d0/d1 follows).

                .macro  PS_ROW
                moveq   #0,%d0
                move.b  (%a0)+,%d0             | cur min
                and.b   %d0,%d5                | fold into any-cur-dirty
                move.b  (%a2),%d2              | prev min
                move.b  %d0,(%a2)+             | snapshot min
                cmp.b   %d2,%d0
                bls.s   1f
                move.b  %d2,%d0                | union min
1:
                moveq   #0,%d1
                move.b  (%a1)+,%d1             | cur max
                move.b  (%a3),%d2              | prev max
                move.b  %d1,(%a3)+             | snapshot max
                cmp.b   %d2,%d1
                bcc.s   2f
                move.b  %d2,%d1                | union max
2:
                cmp.b   %d1,%d0
                bhi.s   6f                     | min > max -> clean row
                cmp.b   #79,%d1                | clamp (defense-in-depth,
                bls.s   3f                     | mirrors the old C)
                moveq   #79,%d1
3:
                lsr.b   #2,%d0                 | min group
                lsr.b   #2,%d1                 | max group
                sub.b   %d0,%d1
                addq.b  #1,%d1                 | d1 = group count 1..20
                tst.b   %d0
                bne.s   4f
                cmp.b   #20,%d1
                beq.s   7f                     | full-width -> run collector
4:
                tst.l   %d3                    | partial row closes any
                beq.s   5f                     | open full-width run
                bsr     .LpsRunClose
5:
| Inline 1-2-group arm (perf-hunt-2 rank 12): the jsr/rts chain entry
| (~38 cyc) dwarfs an 8-16 B band copy, and the sprite-window rows on
| P2/gameFrame are exactly 1-2 groups. Placed AFTER the run close (an
| open full-width run must close first) and only on partial rows, so
| full-width presents (P1) never pay the extra compare. Byte
| accounting stays addq-exact (d7 += 8 per group -> P2 stays 768).
                cmp.b   #2,%d1
                bhi.s   10f                    | > 2 groups -> chain path
                lsl.w   #3,%d0                 | byteStart = minGroup * 8
                move.w  #160,%d2
                sub.w   %d0,%d2                | d2 = 160 - byteStart
                adda.w  %d0,%a4
                adda.w  %d0,%a5                | cursors -> band start
                move.l  (%a4)+,(%a5)+
                move.l  (%a4)+,(%a5)+          | group 1 (8 B)
                addq.w  #8,%d7                 | copied += 8
                subq.w  #8,%d2
                subq.b  #1,%d1
                beq.s   11f
                move.l  (%a4)+,(%a5)+
                move.l  (%a4)+,(%a5)+          | group 2 (8 B)
                addq.w  #8,%d7                 | copied += 8
                subq.w  #8,%d2
11:
                adda.w  %d2,%a4
                adda.w  %d2,%a5                | cursors -> next row base
                bra.s   9f
10:
                lsl.w   #3,%d0                 | byteStart = minGroup * 8
                lsl.w   #3,%d1                 | byteLen   = groups * 8
                add.w   %d1,%d7                | copied += byteLen
                move.w  #160,%d2
                sub.w   %d0,%d2
                sub.w   %d1,%d2                | d2 = stride remainder
                adda.w  %d0,%a4
                adda.w  %d0,%a5                | cursors -> band start
                lsr.w   #1,%d1                 | groups * 4 = chain bytes
                neg.w   %d1                    | entry = 80 - groups*4
                jsr     80(%a6,%d1.w)          | unrolled move.l chain
                adda.w  %d2,%a4
                adda.w  %d2,%a5                | cursors -> next row base
                bra.s   9f
6:
                tst.l   %d3                    | clean row: a run cannot
                beq.s   8f                     | continue across it
                bsr     .LpsRunClose
8:
                lea     160(%a4),%a4
                lea     160(%a5),%a5
                bra.s   9f
7:
                tst.l   %d3                    | full-width: open/extend
                bne.s   8b                     | run; bytes counted at close
                move.l  %a4,%d3                | run start = this row base
                bra.s   8b
9:
                .endm


| ---- run close: memcpy(dstStart, srcStart, a4 - d3) ----
| memcpy clobbers d0-d1/a0-a1 (d2 saved internally; spilled anyway for
| uniformity); everything else live here is callee-saved by memcpy's
| own movem or untouched. Callers may hold live band math in d0/d1.

.LpsRunClose:
                movem.l %d0-%d2/%a0-%a1,-(%sp)
                move.l  %a4,%d2
                sub.l   %d3,%d2                | d2 = run length in bytes
                add.l   %d2,%d7                | copied += length
                move.l  %a5,%d0
                sub.l   %a4,%d0                | dst-src delta (lockstep)
                add.l   %d3,%d0                | d0 = dst run start
                move.l  %d2,-(%sp)             | len
                move.l  %d3,-(%sp)             | src
                move.l  %d0,-(%sp)             | dst
                jsr     _memcpy
                lea     12(%sp),%sp
                movem.l (%sp)+,%d0-%d2/%a0-%a1
                moveq   #0,%d3                 | run closed
                rts


                .globl  _present68kStSlam

_present68kStSlam:
                movem.l %d2-%d7/%a2-%a6,-(%sp)
                move.l  SP_PS_SRC(%sp),%a4
                move.l  SP_PS_DST(%sp),%a5
                lea     _gStageMinWord,%a0
                lea     _gStageMaxWord,%a1
                lea     _gPrevStageMinWord,%a2
                lea     _gPrevStageMaxWord,%a3
                lea     .LpsChain(%pc),%a6
                moveq   #-1,%d4                | CLEAN sentinel pattern
                moveq   #-1,%d5                | AND-acc seed
                moveq   #0,%d3                 | no open run
                moveq   #0,%d7                 | copied bytes
                moveq   #49,%d6                | 50 groups of 4 rows

.LpsGroup:
                move.l  (%a0),%d0              | 4 cur min bytes
                cmp.l   %d4,%d0
                bne.s   .LpsSlow               | some cur row touched
                move.l  (%a2),%d0              | 4 prev min bytes
                cmp.l   %d4,%d0
                bne.s   .LpsSlow               | some prev row dirty
                tst.l   %d3                    | clean group ends any open
                beq.s   1f                     | full-width run (a run must
                bsr     .LpsRunClose           | not bridge clean rows)
1:
                addq.l  #4,%a0                 | both sides all-clean:
                addq.l  #4,%a1                 | nothing to copy and the
                addq.l  #4,%a2                 | snapshot is (FF,00) over
                addq.l  #4,%a3                 | (FF,00) -- skip 4 rows
                lea     640(%a4),%a4
                lea     640(%a5),%a5
                dbra    %d6,.LpsGroup
                bra.w   .LpsEnd

.LpsSlow:
                PS_ROW
                PS_ROW
                PS_ROW
                PS_ROW
                dbra    %d6,.LpsGroup

.LpsEnd:
                tst.l   %d3                    | run reaching row 199
                beq.s   .LpsPack
                bsr     .LpsRunClose
.LpsPack:
                moveq   #0,%d0
                move.w  %d7,%d0                | low word = copied bytes
                addq.b  #1,%d5                 | FF+1 -> 0: Z iff clean
                beq.s   .LpsRet
                bset    #16,%d0                | current frame dirty
.LpsRet:
                movem.l (%sp)+,%d2-%d7/%a2-%a6
                rts


| ---- copy chain: 20 groups x 2 move.l, entered at 80 - groups*4 ----

.LpsChain:
                .rept   20
                move.l  (%a4)+,(%a5)+
                move.l  (%a4)+,(%a5)+
                .endr
                rts
