| Amiga present union-scan -- 68000 hand-rolled (NATIVE-PERF Phase 7).
|
| Replaces jlpPresent's C union-scan loop. The C version was register-
| pressure-bound on gcc-amigaos (4 band-array walkers + run state + the
| union fold spill), which measured as the Amiga present's dominant
| FIXED cost. This routine keeps all four walkers in dedicated address
| registers and the run state in data registers -- no spills.
|
| ONE pass over the 200 band rows does exactly what the C did:
|   * clean-clean row (curMin == prvMin == CLEAN_MIN): close any open
|     run; gPrev is already the (CLEAN_MIN, CLEAN_MAX) sentinel so no
|     snapshot write (the cold-path-only snapshot of the C version).
|   * dirty row: snapshot gPrev[y] <- cur[y], accumulate anyCurDirty,
|     fold union(cur,prev) into a plane-byte span, clamp, then coalesce
|     consecutive same-span rows into runs.
| Runs are emitted into the caller's gPresentRuns[] as 8-byte records
| {firstRow.w, lastRow.w, byteStart.w, byteLen.w}; the caller flushes
| them (blitter or CPU). lastRow is NOT tracked in a register: a run is
| always consecutive rows, so at every close lastRow == y - 1 (and the
| end-of-scan close uses SURFACE_HEIGHT - 1).
|
| The four constants (200 rows, 80 words/row, 0xFF clean sentinel, 8-byte
| run record) are guarded against the C definitions by compile-time
| typedefs in hal.c.
|
| ABI: cdecl. d2-d7/a2-a6 callee-save. Byte loads use moveq #0 first
| (partial-register-write hygiene -- the fold and emit do word math).
|
| uint32_t amigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax,
|                              uint8_t *prvMin, uint8_t *prvMax,
|                              AmigaPresentRunT *runList);
|
| Return d0: low 16 bits = number of runs written;
|            bit 16       = anyUnionDirty (>=1 run row);
|            bit 17       = anyCurDirty  (current frame touched >=1 row).
|
| Register allocation across the row loop:
|   a0 = gStageMinWord walker      a1 = gStageMaxWord walker
|   a2 = gPrevStageMinWord (r/w)   a3 = gPrevStageMaxWord (r/w)
|   a4 = run-list write cursor     a5 = run-list base (for the count)
|   d2 = y (row index 0..199)      d4 = runFirst (-1.w == no open run)
|   d5 = open run byteStart        d6 = open run byteLen
|   d7 = flags (bit0 anyCurDirty, bit1 anyUnionDirty)
|   d0, d1, d3 = per-row scratch

                .text

                .globl  _amigaPresentScan68k

_amigaPresentScan68k:
                movem.l %d2-%d7/%a2-%a6,-(%sp)   | 44 bytes saved
                move.l  48(%sp),%a0              | curMin base
                move.l  52(%sp),%a1              | curMax base
                move.l  56(%sp),%a2              | prvMin base (read + snapshot)
                move.l  60(%sp),%a3              | prvMax base (read + snapshot)
                move.l  64(%sp),%a4              | run-list write cursor
                move.l  %a4,%a5                  | run-list base (count = (a4-a5)/8)

                moveq   #0,%d7                   | flags = 0
                moveq   #-1,%d4                  | runFirst = -1 (no open run)
                moveq   #0,%d2                   | y = 0

| ---- PS_ONE_ROW: one row of the scan (union + snapshot + run fold) ----
| Numeric local labels only (expanded 4x in the dirty-group arm).
| Exactly the old .oneRow body: row-wise outputs (run records, prev
| snapshots, flags, walker advances) are BYTE-IDENTICAL; only the loop
| plumbing around it changed. Falls through with y (d2) advanced by 1.

                .macro  PS_ONE_ROW
                moveq   #0,%d0
                move.b  (%a0)+,%d0               | d0 = curMin (advance a0)
                moveq   #0,%d1
                move.b  (%a2),%d1                | d1 = prvMin (a2 not advanced yet)
                cmp.b   #0xFF,%d0                | clean-clean test
                bne     1f
                cmp.b   #0xFF,%d1
                bne     1f
                | clean-clean: lockstep walkers, no snapshot, close any run.
                addq.l  #1,%a1                   | skip curMax
                addq.l  #1,%a2                   | skip prvMin (already sentinel)
                addq.l  #1,%a3                   | skip prvMax
                tst.w   %d4                       | open run?
                bmi     8f
                move.w  %d4,(%a4)+               | firstRow
                move.w  %d2,%d0
                subq.w  #1,%d0
                move.w  %d0,(%a4)+               | lastRow = y - 1
                move.w  %d5,(%a4)+               | byteStart
                move.w  %d6,(%a4)+               | byteLen
                moveq   #-1,%d4                   | close run
                bra     8f
1:              | dirty row
                moveq   #0,%d3
                move.b  (%a1)+,%d3               | d3 = curMax (advance a1)
                move.b  %d0,(%a2)+               | snapshot prvMin = curMin (advance a2)
                cmp.b   #0xFF,%d0                | anyCurDirty if curMin touched
                beq     2f
                bset    #0,%d7
2:
                cmp.b   %d0,%d1                  | rowMin = min(cur,prv) -> d1
                bls     3f
                move.b  %d0,%d1
3:
                moveq   #0,%d0
                move.b  (%a3),%d0                | d0 = OLD prvMax (a3 not advanced)
                move.b  %d3,(%a3)+               | snapshot prvMax = curMax (advance a3)
                cmp.b   %d0,%d3                  | rowMax = max(cur,prv) -> d3
                bcc     4f
                move.b  %d0,%d3
4:
                cmp.b   #80,%d3                  | clamp >= 80 -> 79
                bcs     5f
                moveq   #79,%d3
5:
                bset    #1,%d7                   | anyUnionDirty
                lsr.b   #1,%d1                   | rowByteStart = rowMin >> 1
                lsr.b   #1,%d3                   | rowMax >> 1
                addq.b  #1,%d3
                sub.b   %d1,%d3                  | rowByteLen
                tst.w   %d4                       | open run?
                bmi     7f
                cmp.w   %d5,%d1                   | same byteStart?
                bne     6f
                cmp.w   %d6,%d3                   | same byteLen?
                bne     6f
                bra     8f                        | coalesce: extend open run
6:              | break run
                move.w  %d4,(%a4)+               | firstRow
                move.w  %d2,%d0
                subq.w  #1,%d0
                move.w  %d0,(%a4)+               | lastRow = y - 1
                move.w  %d5,(%a4)+               | old byteStart
                move.w  %d6,(%a4)+               | old byteLen
7:              | new run
                move.w  %d2,%d4                   | runFirst = y
                move.w  %d1,%d5                   | runByteStart
                move.w  %d3,%d6                   | runByteLen
8:
                addq.w  #1,%d2
                .endm

| ---- group loop: 50 fixed 4-row groups (perf-hunt-2 rank 11 v2) ----
| INVARIANT: y (d2) is 0 (mod 4) at .groupLoop -- it starts 0 and both
| arms advance it by exactly 4 (the skip's addq, or 4 PS_ONE_ROW
| fall-throughs). So ONE guard per group replaces the old per-row
| cmp #200 / btst #0 / cmp #197 policing (3 instructions per row), and
| the phantom-row-200 class (the bisect-proven gameFrameClean heap
| corruption) is structurally impossible: entering EITHER arm requires
| y <= 196, and no path advances y by anything but 4. The band
| pointers stay word-aligned at group top (aligned(2) arrays + y even)
| for the move.l fast test.
.groupLoop:
                cmp.w   #200,%d2
                bge     .scanEnd
                move.l  (%a0),%d0                 | 4 cur min bytes
                addq.l  #1,%d0                    | 0xFFFFFFFF+1 == 0 <=> all clean
                bne     .groupDirty
                move.l  (%a2),%d0                 | 4 prev min bytes
                addq.l  #1,%d0
                bne     .groupDirty
                | all 4 rows clean-clean: close any open run, skip 4
                tst.w   %d4
                bmi     .fastSkip
                move.w  %d4,(%a4)+                | firstRow
                move.w  %d2,%d0
                subq.w  #1,%d0
                move.w  %d0,(%a4)+                | lastRow = y - 1
                move.w  %d5,(%a4)+                | byteStart
                move.w  %d6,(%a4)+                | byteLen
                moveq   #-1,%d4                    | close run
.fastSkip:
                lea     4(%a0),%a0
                lea     4(%a1),%a1
                lea     4(%a2),%a2
                lea     4(%a3),%a3
                addq.w  #4,%d2
                bra     .groupLoop

.groupDirty:
                | Some row in the group is touched (cur or prev): process
                | all 4 rows with the exact old one-row logic, fall-through.
                PS_ONE_ROW
                PS_ONE_ROW
                PS_ONE_ROW
                PS_ONE_ROW
                bra     .groupLoop

.scanEnd:
                | close the final open run [runFirst .. 199]
                tst.w   %d4
                bmi     .done
                move.w  %d4,(%a4)+               | firstRow
                move.w  #199,(%a4)+              | lastRow = SURFACE_HEIGHT - 1
                move.w  %d5,(%a4)+               | byteStart
                move.w  %d6,(%a4)+               | byteLen

.done:
                move.l  %a4,%d0
                sub.l   %a5,%d0                   | bytes written
                lsr.l   #3,%d0                    | / 8 = numRuns
                btst    #1,%d7                     | anyUnionDirty -> bit 16
                beq     .noUnion
                bset    #16,%d0
.noUnion:
                btst    #0,%d7                     | anyCurDirty -> bit 17
                beq     .noCurRet
                bset    #17,%d0
.noCurRet:
                movem.l (%sp)+,%d2-%d7/%a2-%a6
                rts
