// 68k sprite codegen for the ST word-interleaved planar layout.
//
// Emits a position-independent, cdecl-callable DRAW routine that blits
// one sprite into the ST planar buffer. The sprite's tileData is fixed
// at compile time, so the chunky->planar (c2p) conversion and every
// destination offset are constant-folded here; the emitted routine is a
// flat unrolled chain of GROUP-level long ops (move.l immediates for
// opaque groups, movem.l whole-group RMW, movep.l byte-lane scatter for
// lone columns -- perf-hunt-2 rank 8) with plane bytes and masks baked
// in as immediates.
//
// Calling convention (m68k cdecl, the routine is invoked through a
// C function pointer by spriteCompiledDraw in spriteCompile.c):
//   void draw(uint8_t *groupBase);
//   groupBase = pd->base + y*160 + (x>>4)*8  (top-left 16-px group).
// The single arg sits at 4(sp) on entry (0(sp) = return address). The
// routine loads it into a0, advances a0 by 160 (ST_BYTES_PER_ROW) at
// the start of each subsequent row, and writes groups at d16(a0).
// Only d0/d1/a0 (cdecl scratch) are touched, so no callee-save is
// needed.
//
// ST planar layout: each 16-pixel group is 4 plane words (8 bytes) at
// group_off + plane*2; the word's high byte (offset +0) holds the left
// 8 pixels, the low byte (offset +1) the right 8. A byte-aligned sprite
// tile column writes exactly one half (one plane byte) per plane, so the
// per-plane work is a single byte RMW:
//   move.b  disp(a0),d0
//   andi.b  #~pop,d0          ; clear the opaque pixel bits
//   ori.b   #planeByte,d0     ; set this plane's bits (omitted if zero)
//   move.b  d0,disp(a0)
// matching stSpriteDrawByteAligned in src/atarist/hal.c bit-for-bit.
//
// The shift argument is the full intra-group phase x & 15 (16 slots on
// the ST; see spriteInternal.h). Only the byte-aligned phases 0
// (x mod 16 == 0) and 8 (x mod 16 == 8) are compiled today; the other
// 14 phases return 0 so jlSpriteCompile records SPRITE_NOT_COMPILED
// and the dispatcher routes those x through the interpreted
// jlpSpriteDrawPlanes walker (until NATIVE-PERF Phase 1 fills them in).

#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitC2p.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"


// ----- Constants -----

// Tile geometry (TILE_BYTES / TILE_BYTES_PER_ROW / TILE_PIXELS_PER_SIDE)
// comes from joey/tile.h.
#define ST_PLANAR_STRIDE      160u  // 320 px * 4 planes / 8 bits
#define ST_GROUP_BYTES        8u    // 4 plane words per 16-px group

// 68k opcode words (big-endian), operands follow as separate words:
//   movea.l 4(sp),a0   : 206F 0004  (mode 101 = (d16,An); 205F would be (An)+)
//   adda.w  #160,a0    : D0FC 00A0
//   move.b  d16(a0),d0 : 1028 dddd
//   andi.b  #imm,d0    : 0200 00ii
//   ori.b   #imm,d0    : 0000 00ii
//   move.b  d0,d16(a0) : 1140 dddd
//   move.b  #imm,d16(a0): 117C 00ii dddd  (fully-opaque column: direct store)
//   rts                : 4E75
// Group forms (perf-hunt-2 rank 8 -- see the emit loop for when each
// fires; all operate on the 8-byte 16-px group or one byte lane of it):
//   move.l  #imm,d16(a0) : 217C hhhh llll dddd  (dual-opaque half-group)
//   movem.l d16(a0),d0-d1: 4CE8 0003 dddd       (whole-group read)
//   movem.l d0-d1,d16(a0): 48E8 0003 dddd       (whole-group write; d16(An)
//                          is a control mode -- NO predec mask reversal)
//   andi.l  #imm,d0      : 0280 hhhh llll        (d1: 0281)
//   ori.l   #imm,d0      : 0080 hhhh llll        (d1: 0081)
//   move.l  #imm,d0      : 203C hhhh llll
//   movep.l d0,d16(a0)   : 01C8 dddd             (byte-lane scatter, 4 bytes
//   movep.l d16(a0),d0   : 0148 dddd              at disp,+2,+4,+6; odd disp ok)
#define OP_MOVEA_4SP_A0   0x206Fu
#define OP_ADDA_W_A0      0xD0FCu
#define OP_MOVE_B_A0_D0   0x1028u
#define OP_ANDI_B_D0      0x0200u
#define OP_ORI_B_D0       0x0000u
#define OP_MOVE_B_D0_A0   0x1140u
#define OP_MOVE_B_IMM_A0  0x117Cu
#define OP_RTS            0x4E75u
#define OP_MOVE_L_IMM_A0  0x217Cu
#define OP_MOVEM_L_A0_D01 0x4CE8u
#define OP_MOVEM_L_D01_A0 0x48E8u
#define MOVEM_MASK_D01    0x0003u
#define OP_ANDI_L_D0      0x0280u
#define OP_ANDI_L_D1      0x0281u
#define OP_ORI_L_D0       0x0080u
#define OP_ORI_L_D1       0x0081u
#define OP_MOVE_L_IMM_D0  0x203Cu
#define OP_MOVEP_L_D0_A0  0x01C8u
#define OP_MOVEP_L_A0_D0  0x0148u

// Emitted bytes for the trailing rts.
#define EPILOGUE_BYTES            2u

// Shifted-phase thunk opcodes (see emitShiftedDraw):
//   movea.l 4(sp),a0    : 206F 0004   (groupBase -- unchanged dispatcher ABI)
//   lea     16(pc),a1   : 43FA 0010   (data table at thunk+22; ext word at +6)
//   move.w  #imm,d0     : 303C rrrr   (rows - 1)
//   move.w  #imm,d1     : 323C gggg   (groupsPerRow - 1)
//   jmp     abs.l       : 4EF9 hhhh llll  (stSpriteDrawShiftWalker)
#define OP_LEA_16PC_A1    0x43FAu
#define LEA_PC_DISP       0x0010u
#define OP_MOVE_W_IMM_D0  0x303Cu
#define OP_MOVE_W_IMM_D1  0x323Cu
#define OP_JMP_ABSL       0x4EF9u

#define SHIFT_THUNK_BYTES     22u
#define SHIFT_GROUP_DATA_BYTES 12u  // notMask long + bits01 long + bits23 long

// The shared shifted-phase walker (src/atarist/spriteWalk.s). Its
// absolute address is baked into each thunk's jmp at emit time --
// emission happens at runtime (post-TOS-relocation), and arena moves
// via codegenArenaCompact relocate only the thunk+data, whose one
// internal reference (lea 16(pc)) is PC-relative.
extern void stSpriteDrawShiftWalker(void);

// Per-row widened accumulators for emitShiftedDraw. File statics, NOT
// stack locals: the compile path runs deep and big frames overflowed
// the Amiga shell's default 4 KB task stack (same class on ST/TOS).
// Emit-time only and single-threaded, so statics are safe.
static uint8_t gStM[SURFACE_WIDTH / 8 + 1];
static uint8_t gStBp[4][SURFACE_WIDTH / 8 + 1];


// ----- Emit helpers -----

// c2pColumn (identical pixel walk to stSpriteDrawByteAligned:
// hi(b0),lo(b0),...,hi(b3),lo(b3) at bit positions 7..0) and putWord
// are shared with the Amiga emitter via spriteEmitC2p.h.


// ----- Emit helpers (shifted phases) -----

// Emit one sub-byte phase (shift & 7 != 0): a 22-byte thunk that tail-
// jumps into the shared stSpriteDrawShiftWalker, followed by the
// phase's pre-shifted data table (SHIFT_GROUP_DATA_BYTES per group per
// row). The table is built with the SAME c2pColumn walk the aligned
// JIT uses; a source column's bits split across two adjacent widened
// byte columns (>> s into col c, << 8-s into col c+1, disjoint at the
// seam), and byte columns pack into 16-px groups two at a time with
// the intra-group half phase h = shift >> 3 offsetting the mapping.
// The walker's masked movem RMW of a group composes to exactly the
// interpreted walker's per-byte RMWs, so pixels are bit-identical.
static uint32_t emitShiftedDraw(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t cursor;
    uint32_t walker;
    uint16_t wTiles;
    uint16_t srcH;
    uint16_t groupsPerRow;
    uint16_t rowBytes;
    uint8_t  s;
    uint8_t  h;
    int16_t  row;
    int16_t  col;
    int16_t  g;
    uint8_t  p;

    // The minimal on-surface x for this phase is `shift` itself, so a
    // phase the gate can never dispatch gets no dead table bytes.
    if ((uint16_t)shift + sp->widthPx > SURFACE_WIDTH) {
        return 0u;
    }
    s            = (uint8_t)(shift & 7u);
    h            = (uint8_t)(shift >> 3);
    wTiles       = sp->widthTiles;
    srcH         = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    groupsPerRow = (uint16_t)((uint16_t)((h + wTiles) >> 1) + 1u);
    rowBytes     = (uint16_t)(groupsPerRow * SHIFT_GROUP_DATA_BYTES);

    cursor = 0u;
    if (cursor + SHIFT_THUNK_BYTES > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    // uintptr_t intermediate so this compiles clean on the 64-bit host bake
    // tool (tools/spritebake) too; on the 32-bit ST target it is a no-op. The
    // baked value is don't-care off-target anyway -- the .spc loader re-stamps
    // the real walker address at load (spritePrecompiled.h).
    walker = (uint32_t)(uintptr_t)stSpriteDrawShiftWalker;
    cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
    cursor = putWord(out, cursor, 0x0004u);
    cursor = putWord(out, cursor, OP_LEA_16PC_A1);
    cursor = putWord(out, cursor, LEA_PC_DISP);
    cursor = putWord(out, cursor, OP_MOVE_W_IMM_D0);
    cursor = putWord(out, cursor, (uint16_t)(srcH - 1u));
    cursor = putWord(out, cursor, OP_MOVE_W_IMM_D1);
    cursor = putWord(out, cursor, (uint16_t)(groupsPerRow - 1u));
    cursor = putWord(out, cursor, OP_JMP_ABSL);
    cursor = putWord(out, cursor, (uint16_t)(walker >> 16));
    cursor = putWord(out, cursor, (uint16_t)(walker & 0xFFFFu));

    for (row = 0; row < (int16_t)srcH; row++) {
        uint16_t       tileY       = (uint16_t)((uint16_t)row >> 3);
        uint16_t       inTileY     = (uint16_t)((uint16_t)row & 7u);
        const uint8_t *tileRowBase = sp->tileData
                                   + (uint32_t)tileY * wTiles * TILE_BYTES
                                   + (uint32_t)inTileY * TILE_BYTES_PER_ROW;

        if (cursor + rowBytes > cap) {
            return SPRITE_EMIT_OVERFLOW;
        }
        // Widened per-byte-column mask/data for this row: source column
        // col contributes >> s to widened col `col` and << (8-s) to
        // widened col `col + 1` (indices relative to the sprite's first
        // written byte column, which sits at absolute byte column h).
        for (col = 0; col <= (int16_t)wTiles; col++) {
            gStM[col]     = 0u;
            gStBp[0][col] = 0u;
            gStBp[1][col] = 0u;
            gStBp[2][col] = 0u;
            gStBp[3][col] = 0u;
        }
        for (col = 0; col < (int16_t)wTiles; col++) {
            const uint8_t *trp = tileRowBase + (uint32_t)col * TILE_BYTES;
            uint8_t        plane[4];
            uint8_t        opaque;

            c2pColumn(trp, plane, &opaque);
            if (opaque == 0u) {
                continue;
            }
            gStM[col]     = (uint8_t)(gStM[col]     | (uint8_t)(opaque >> s));
            gStM[col + 1] = (uint8_t)(gStM[col + 1] | (uint8_t)(opaque << (8u - s)));
            for (p = 0u; p < 4u; p++) {
                gStBp[p][col]     = (uint8_t)(gStBp[p][col]     | (uint8_t)(plane[p] >> s));
                gStBp[p][col + 1] = (uint8_t)(gStBp[p][col + 1] | (uint8_t)(plane[p] << (8u - s)));
            }
        }
        // Pack widened columns into group records. Group g's high byte
        // is absolute column 2g, low byte 2g+1; absolute column a maps
        // to widened index a - h (outside [0, wTiles] contributes 0).
        for (g = 0; g < (int16_t)groupsPerRow; g++) {
            int16_t  hiJ  = (int16_t)(2 * g - (int16_t)h);
            int16_t  loJ  = (int16_t)(hiJ + 1);
            uint8_t  mHi  = (hiJ >= 0 && hiJ <= (int16_t)wTiles) ? gStM[hiJ] : 0u;
            uint8_t  mLo  = (loJ >= 0 && loJ <= (int16_t)wTiles) ? gStM[loJ] : 0u;
            uint16_t m16  = (uint16_t)(((uint16_t)mHi << 8) | mLo);
            uint16_t nm16 = (uint16_t)~m16;

            cursor = putWord(out, cursor, nm16);
            cursor = putWord(out, cursor, nm16);
            for (p = 0u; p < 4u; p++) {
                uint8_t  dHi = (hiJ >= 0 && hiJ <= (int16_t)wTiles) ? gStBp[p][hiJ] : 0u;
                uint8_t  dLo = (loJ >= 0 && loJ <= (int16_t)wTiles) ? gStBp[p][loJ] : 0u;

                cursor = putWord(out, cursor, (uint16_t)(((uint16_t)dHi << 8) | dLo));
            }
        }
    }
    return cursor;
}


// ----- Emit API -----

uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    uint32_t cursor;
    uint16_t wTiles;
    uint16_t srcH;
    uint16_t xMod16;
    int16_t  row;
    int16_t  tileCol;

    // Shift is the full intra-group phase x & 15 (spriteInternal.h).
    // The byte-aligned phases 0 (x%16==0, high byte halves) and 8
    // (x%16==8, low byte halves) emit the unrolled byte-RMW JIT below;
    // the 14 sub-byte phases emit a thunk + pre-shifted data table
    // driven by the shared stSpriteDrawShiftWalker.
    if ((shift & 7u) != 0u) {
        return emitShiftedDraw(out, cap, sp, shift);
    }
    wTiles = sp->widthTiles;
    srcH   = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
    xMod16 = shift;                    // phase 0 -> 0, phase 8 -> 8

    cursor = 0u;

    // Prologue: movea.l 4(sp),a0
    if (cursor + 4u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
    cursor = putWord(out, cursor, 0x0004u);

    for (row = 0; row < (int16_t)srcH; row++) {
        uint16_t       tileY       = (uint16_t)((uint16_t)row >> 3);
        uint16_t       inTileY     = (uint16_t)((uint16_t)row & 7u);
        const uint8_t *tileRowBase = sp->tileData
                                   + (uint32_t)tileY * wTiles * TILE_BYTES
                                   + (uint32_t)inTileY * TILE_BYTES_PER_ROW;

        if (row > 0) {
            // adda.w #160,a0 -- advance to this row's planar base.
            if (cursor + 4u > cap) {
                return SPRITE_EMIT_OVERFLOW;
            }
            cursor = putWord(out, cursor, OP_ADDA_W_A0);
            cursor = putWord(out, cursor, ST_PLANAR_STRIDE);
        }

        // Group walk (perf-hunt-2 rank 8): consume columns in 16-px
        // groups so the whole 8-byte group is written with long ops
        // instead of per-plane byte RMWs. Phase 0 pairs columns
        // (0,1)(2,3)...; phase 8 starts with a lone low half, then
        // pairs (1,2)(3,4)... Forms, cheapest first:
        //   both columns fully opaque -> two move.l #imm,(d16,a0)
        //   both present, any partial -> movem.l whole-group RMW d0/d1
        //   one column present        -> movep.l byte-lane (pure write
        //                                if opaque, RMW if partial)
        // A fully-transparent column inside a present pair contributes
        // mask FF / data 00, so the movem write-back leaves its bytes
        // bit-identical -- final memory matches the old per-plane
        // byte-RMW form exactly (the hash gate). d0/d1/a0 are cdecl
        // scratch (the shifted-phase thunks already clobber d1).
        // Exact reservation per form: a conservative reserve would make
        // the emit pass -- which gets exactly the size the sizing pass
        // reported -- falsely overflow on trailing shorter writes.
        tileCol = 0;
        while (tileCol < (int16_t)wTiles) {
            uint16_t pixOff       = (uint16_t)(xMod16 + (uint16_t)tileCol * 8u);
            uint16_t groupByteOff = (uint16_t)((uint16_t)(pixOff >> 4) * ST_GROUP_BYTES);
            uint16_t halfByte     = (uint16_t)((pixOff & 8u) ? 1u : 0u);
            uint8_t  planeH[4];
            uint8_t  planeL[4];
            uint8_t  opqH = 0u;
            uint8_t  opqL = 0u;

            if (halfByte == 0u) {
                c2pColumn(tileRowBase + (uint32_t)tileCol * TILE_BYTES, planeH, &opqH);
                if ((int16_t)(tileCol + 1) < (int16_t)wTiles) {
                    c2pColumn(tileRowBase + (uint32_t)(tileCol + 1) * TILE_BYTES, planeL, &opqL);
                    tileCol = (int16_t)(tileCol + 2);
                } else {
                    tileCol = (int16_t)(tileCol + 1);
                }
            } else {
                c2pColumn(tileRowBase + (uint32_t)tileCol * TILE_BYTES, planeL, &opqL);
                tileCol = (int16_t)(tileCol + 1);
            }

            if (opqH == 0u && opqL == 0u) {
                continue;                    // nothing opaque in this group
            }

            if (opqH == 0xFFu && opqL == 0xFFu) {
                // Dual fully-opaque: all 8 group bytes are constants.
                // Two move.l #imm,(d16,a0) -- 16 bytes, ~72 cyc vs the
                // old 8 byte-stores' ~128.
                uint32_t imm01 = ((uint32_t)planeH[0] << 24) | ((uint32_t)planeL[0] << 16) | ((uint32_t)planeH[1] << 8) | planeL[1];
                uint32_t imm23 = ((uint32_t)planeH[2] << 24) | ((uint32_t)planeL[2] << 16) | ((uint32_t)planeH[3] << 8) | planeL[3];

                if (cursor + 16u > cap) {
                    return SPRITE_EMIT_OVERFLOW;
                }
                cursor = putWord(out, cursor, OP_MOVE_L_IMM_A0);
                cursor = putWord(out, cursor, (uint16_t)(imm01 >> 16));
                cursor = putWord(out, cursor, (uint16_t)imm01);
                cursor = putWord(out, cursor, groupByteOff);
                cursor = putWord(out, cursor, OP_MOVE_L_IMM_A0);
                cursor = putWord(out, cursor, (uint16_t)(imm23 >> 16));
                cursor = putWord(out, cursor, (uint16_t)imm23);
                cursor = putWord(out, cursor, (uint16_t)(groupByteOff + 4u));
            } else if (opqH != 0u && opqL != 0u) {
                // Both halves present, at least one partial: whole-group
                // movem RMW. d0 = plane 0-1 bytes, d1 = plane 2-3; the
                // AND mask is per-column opacity, identical in both regs
                // (an opaque half's mask byte is 00 -> full replace).
                uint8_t  mH    = (uint8_t)~opqH;
                uint8_t  mL    = (uint8_t)~opqL;
                uint32_t mask  = ((uint32_t)mH << 24) | ((uint32_t)mL << 16) | ((uint32_t)mH << 8) | mL;
                uint32_t dat01 = ((uint32_t)planeH[0] << 24) | ((uint32_t)planeL[0] << 16) | ((uint32_t)planeH[1] << 8) | planeL[1];
                uint32_t dat23 = ((uint32_t)planeH[2] << 24) | ((uint32_t)planeL[2] << 16) | ((uint32_t)planeH[3] << 8) | planeL[3];
                uint32_t need  = (uint32_t)(24u + ((dat01 != 0u) ? 6u : 0u) + ((dat23 != 0u) ? 6u : 0u));

                if (cursor + need > cap) {
                    return SPRITE_EMIT_OVERFLOW;
                }
                cursor = putWord(out, cursor, OP_MOVEM_L_A0_D01);
                cursor = putWord(out, cursor, MOVEM_MASK_D01);
                cursor = putWord(out, cursor, groupByteOff);
                cursor = putWord(out, cursor, OP_ANDI_L_D0);
                cursor = putWord(out, cursor, (uint16_t)(mask >> 16));
                cursor = putWord(out, cursor, (uint16_t)mask);
                if (dat01 != 0u) {
                    cursor = putWord(out, cursor, OP_ORI_L_D0);
                    cursor = putWord(out, cursor, (uint16_t)(dat01 >> 16));
                    cursor = putWord(out, cursor, (uint16_t)dat01);
                }
                cursor = putWord(out, cursor, OP_ANDI_L_D1);
                cursor = putWord(out, cursor, (uint16_t)(mask >> 16));
                cursor = putWord(out, cursor, (uint16_t)mask);
                if (dat23 != 0u) {
                    cursor = putWord(out, cursor, OP_ORI_L_D1);
                    cursor = putWord(out, cursor, (uint16_t)(dat23 >> 16));
                    cursor = putWord(out, cursor, (uint16_t)dat23);
                }
                cursor = putWord(out, cursor, OP_MOVEM_L_D01_A0);
                cursor = putWord(out, cursor, MOVEM_MASK_D01);
                cursor = putWord(out, cursor, groupByteOff);
            } else {
                // Single present column: movep.l scatters d0's 4 bytes
                // across this column's byte lane (disp, +2, +4, +6 = its
                // 4 plane bytes; odd disp is fine, movep is byte access).
                const uint8_t *pl   = (opqH != 0u) ? planeH : planeL;
                uint8_t        opq  = (opqH != 0u) ? opqH : opqL;
                uint16_t       disp = (uint16_t)(groupByteOff + ((opqH != 0u) ? 0u : 1u));
                uint32_t       data = ((uint32_t)pl[0] << 24) | ((uint32_t)pl[1] << 16) | ((uint32_t)pl[2] << 8) | pl[3];

                if (opq == 0xFFu) {
                    // move.l #data,d0 ; movep.l d0,(disp,a0) -- 10 bytes,
                    // ~36 cyc vs the old 4 byte-stores' ~64.
                    if (cursor + 10u > cap) {
                        return SPRITE_EMIT_OVERFLOW;
                    }
                    cursor = putWord(out, cursor, OP_MOVE_L_IMM_D0);
                    cursor = putWord(out, cursor, (uint16_t)(data >> 16));
                    cursor = putWord(out, cursor, (uint16_t)data);
                    cursor = putWord(out, cursor, OP_MOVEP_L_D0_A0);
                    cursor = putWord(out, cursor, disp);
                } else {
                    uint8_t  m    = (uint8_t)~opq;
                    uint32_t mask = ((uint32_t)m << 24) | ((uint32_t)m << 16) | ((uint32_t)m << 8) | m;
                    uint32_t need = (uint32_t)(14u + ((data != 0u) ? 6u : 0u));

                    if (cursor + need > cap) {
                        return SPRITE_EMIT_OVERFLOW;
                    }
                    cursor = putWord(out, cursor, OP_MOVEP_L_A0_D0);
                    cursor = putWord(out, cursor, disp);
                    cursor = putWord(out, cursor, OP_ANDI_L_D0);
                    cursor = putWord(out, cursor, (uint16_t)(mask >> 16));
                    cursor = putWord(out, cursor, (uint16_t)mask);
                    if (data != 0u) {
                        cursor = putWord(out, cursor, OP_ORI_L_D0);
                        cursor = putWord(out, cursor, (uint16_t)(data >> 16));
                        cursor = putWord(out, cursor, (uint16_t)data);
                    }
                    cursor = putWord(out, cursor, OP_MOVEP_L_D0_A0);
                    cursor = putWord(out, cursor, disp);
                }
            }
        }
    }

    // rts
    if (cursor + EPILOGUE_BYTES > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_RTS);
    return cursor;
}


// Compiled save/restore (NATIVE-PERF Phase 1): raw-span copiers over
// the 16-px-group window. A group window's rows are CONTIGUOUS spans
// of interleaved bytes, so each row is a pure move.l chain; the shift
// argument is the width CLASS (0 = ceil(w/16) groups, 1 = one more --
// see SPRITE_SAVE_CLASS in spriteInternal.h), and only classes 0/1
// emit. cdecl void copy(const uint8_t *src, uint8_t *dst): SAVE gets
// (screenRowStart, buf) -- a0 strided, a1 contiguous; RESTORE gets
// (buf, screenRowStart) -- a0 contiguous, a1 strided. Byte format is
// identical to jlpSpriteSavePlanes' raw-span branch (format law:
// pure function of the recorded geometry).
//   movea.l 4(sp),a0 : 206F 0004      movea.l 8(sp),a1 : 226F 0008
//   move.l (a0)+,(a1)+ : 22D8
//   lea d16(a0),a0 : 41E8 dddd        lea d16(a1),a1 : 43E9 dddd
#define OP_MOVEA_8SP_A1  0x226Fu
#define OP_MOVE_L_POSTI  0x22D8u
#define OP_LEA_D16_A0    0x41E8u
#define OP_LEA_D16_A1    0x43E9u

static uint32_t emitGroupSpanCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool strideOnDst) {
    uint32_t cursor;
    uint16_t nGroups;
    uint16_t spanBytes;
    uint16_t longsPerRow;
    uint16_t strideFix;
    uint16_t srcH;
    int16_t  row;
    uint16_t i;

    if (shift > 1u) {
        return 0u;
    }
    nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift);
    if ((uint16_t)(nGroups << 4) > SURFACE_WIDTH) {
        // Degenerate: the widened window would exceed the surface, so
        // this class is unreachable through the on-surface gate. Both
        // save and restore bail identically (pair provenance).
        return 0u;
    }
    spanBytes   = (uint16_t)(nGroups << 3);
    longsPerRow = (uint16_t)(spanBytes >> 2);
    strideFix   = (uint16_t)(ST_PLANAR_STRIDE - spanBytes);
    srcH        = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);

    cursor = 0u;
    if (cursor + 8u > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
    cursor = putWord(out, cursor, 0x0004u);
    cursor = putWord(out, cursor, OP_MOVEA_8SP_A1);
    cursor = putWord(out, cursor, 0x0008u);
    for (row = 0; row < (int16_t)srcH; row++) {
        if (cursor + (uint32_t)(longsPerRow << 1) + 4u + EPILOGUE_BYTES > cap) {
            return SPRITE_EMIT_OVERFLOW;
        }
        for (i = 0u; i < longsPerRow; i++) {
            cursor = putWord(out, cursor, OP_MOVE_L_POSTI);
        }
        if (row < (int16_t)(srcH - 1u)) {
            // Advance the strided (screen) side to the next row; the
            // buffer side is contiguous and needs no fix.
            cursor = putWord(out, cursor, strideOnDst ? OP_LEA_D16_A1 : OP_LEA_D16_A0);
            cursor = putWord(out, cursor, strideFix);
        }
    }
    if (cursor + EPILOGUE_BYTES > cap) {
        return SPRITE_EMIT_OVERFLOW;
    }
    cursor = putWord(out, cursor, OP_RTS);
    return cursor;
}


uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    return emitGroupSpanCopy(out, cap, sp, shift, false);
}


uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
    return emitGroupSpanCopy(out, cap, sp, shift, true);
}
