// Compiled-sprite runtime dispatchers as always-inline functions
// (NATIVE-PERF Phase 2 R5). Bodies moved verbatim from
// src/codegen/spriteCompile.c so the hot entries in src/core/sprite.c
// inline them -- deleting a cross-TU call per compiled sprite op
// (~130 cycles on the 68000, more on the 65816 with its JSL + frame)
// and letting the compiler share the slot/field loads the caller
// already performed. sprite.c is the ONLY consumer; include this
// header nowhere else. The compile-time machinery (jlSpriteCompile,
// emitters, arena plumbing) stays in spriteCompile.c.
#ifndef JOEY_SPRITE_DISPATCH_H
#define JOEY_SPRITE_DISPATCH_H

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

#include "codegenArenaInternal.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"

#if defined(JOEYLIB_PLATFORM_AMIGA)
#include "amigaPlanar.h"
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#include "stPlanar.h"
#elif defined(JOEYLIB_PLATFORM_X68000)
#include "x68kPlanar.h"
#endif

#if defined(JOEYLIB_PLATFORM_IIGS)

// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs;
// declared in surfaceInternal.h. Uses a single indexed long-mode read
// for the multiply.

// IIgs sprite dispatch. The emitted routines (spriteEmitIigs.c) are ordinary
// C-ABI functions called through C function pointers, so the compiler emits the
// cross-bank JSL itself (large memory model -> 24-bit pointers). The DRAW
// routine takes the 24-bit dst pointer in A:X and sets up its own DBR (needed
// for `sta abs,Y`, whose high byte comes from DBR) + Y in a fixed prologue, then
// restores DBR before returning. SAVE/RESTORE take packed src/dst offsets and
// have only their MVN bank operands patched per call (a plain C pointer write
// into the arena from the dispatcher). No self-modifying call stub, no inline
// asm -- the machinery that hid the old un-instrumentable #19 corruption.
static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
    uint8_t  *dstRow0;
    uint32_t  fnAddr;
    void    (*drawFn)(uint8_t *);

    // Clean C-ABI dispatch. The emitted DRAW routine is a normal C function
    // -- void draw(uint8_t *dstRow0) -- so the compiler emits the indirect
    // JSL itself: NO self-modifying call stub, NO inline asm, NO hand-rolled
    // per-call bank/offset patching (that machinery was the source of the
    // old un-instrumentable corruption). dstRow0 (the sprite's top-left
    // screen byte) is passed in A:X; the routine's prologue (spriteEmitIigs.c)
    // sets up its own DBR/Y and restores DBR on exit. routeOffset is the
    // gate-derived DRAW offset for this shift (#29).
    dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
    fnAddr  = codegenArenaBaseAddr()
            + sp->slot->offset
            + (uint32_t)routeOffset;
    drawFn  = (void (*)(uint8_t *))fnAddr;
    drawFn(dstRow0);
}


// patchMvnBanks stamps the dst/src bank operand bytes into each MVN inside an
// emitted C-ABI save/restore routine -- the routine's only runtime-dynamic
// bytes, since MVN's banks are immediate operands and the screen/backup banks
// vary at runtime. The patch is a plain C pointer write into the arena from
// the dispatcher (no self-modifying call stub). C-ABI routine layout from
// spriteEmitIigs.c::emitMvnCabiRoutine:
//   prologue (4):  8B DA AA 7A
//   row 0    (6):  A9 lo hi 54 db sb
//   row R    (12): 8A/98 18 69 lo hi AA/A8 A9 lo hi 54 db sb
//   epilogue (2):  AB 6B
// The 4-byte prologue shifts MVN dstbk to routine offset (12*R + 8), srcbk to
// (12*R + 9).
static inline void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank, uint8_t srcBank) {
    uint16_t r;

    for (r = 0; r < heightPx; r++) {
        routine[12u * r + 8u] = dstBank;
        routine[12u * r + 9u] = srcBank;
    }
}



// Split a 24-bit pointer into its low 16 bits + bank byte. To avoid
// invoking a 32-bit-shift helper for the `>> 16` to extract the bank
// byte, we cast to uint32_t and then byte-alias the storage -- gets
// the same bytes with simple loads.
#define SPLIT_POINTER(_ptr, _outLo, _outBank) \
    do { \
        uint32_t        spAddr_  = (uint32_t)(_ptr); \
        const uint8_t  *spB_     = (const uint8_t *)&spAddr_; \
        *(_outLo)   = (uint16_t)(spB_[0] | ((uint16_t)spB_[1] << 8)); \
        *(_outBank) = spB_[2]; \
    } while (0)

// The backup->bytes pointer is split with SPLIT_POINTER directly. A
// single-entry global split cache used to live here (#59); it assumed
// one backup buffer per program, but the normal pattern is one buffer
// per moving sprite, so with 2+ sprites the cached pointer ping-ponged
// and every call paid a 24-bit compare AND the full split. The split
// itself is ~6 simple loads/stores -- cheaper than the compare on a
// hit -- so the cache was deleted rather than moved per-sprite.


// Fused stage entries (src/iigs/joeyDraw.s, .text.iigsFusedRect).
// Only sprite.c consumes this header, so the externs live here, not
// in port.h (they are not jlp ops). Each runs the compiled arena
// routine AND the dirty-band widen in one JSL; the caller owns the
// s == gStage gate and validation.
// NOTE (W1 measurement): an R12-style resolved-fnAddr cache
// (per-(shift,op) address + arena-epoch invalidation) was built and
// benched here 2026-07-07 and REMOVED: on llvm-mos the epoch-checked
// hit path costs as much as the 3-far-load derive it replaces, and
// the extra 32-bit value pressured the caller frames -- save (cache
// only, no fusion) measured 602 -> 543 ops/s. The derive below is the
// cheap form on this compiler.
extern void iigsSpriteDrawMark(uint32_t fnAddr, uint16_t x, uint16_t y, uint16_t widthPx, uint16_t heightPx);
extern void iigsSpriteRestoreMark(uint32_t fnAddr, uint16_t backupLo, uint16_t bx, uint16_t by, uint16_t bw, uint16_t bh);
extern void iigsSpriteSaveStage(uint32_t fnAddr, uint16_t backupLo, uint16_t x, uint16_t y);


// Fused-draw C glue: derive the arena address (same three-term form
// as spriteCompiledDraw) and make the ONE JSL that runs the compiled
// body and the dirty-band widen. Caller owns s == gStage +
// isFullyOnSurface + the NOT_COMPILED gate.
static inline __attribute__((always_inline)) void spriteCompiledDrawMark(const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
    uint32_t fnAddr;

    fnAddr = codegenArenaBaseAddr()
           + sp->slot->offset
           + (uint32_t)routeOffset;
    iigsSpriteDrawMark(fnAddr, (uint16_t)x, (uint16_t)y, sp->widthPx, sp->heightPx);
}


static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    int16_t   clippedX;
    uint16_t  heightPx;
    uint16_t  copyBytes;
    uint16_t  screenLo;
    uint16_t  backupLo;
    uint8_t   screenBank;
    uint8_t   backupBank;
    uint32_t  fnAddr;
    uint8_t  *routine;
    uint8_t  *screenPtr;
    uint16_t  cacheIdx;       /* shift * SPRITE_OP_COUNT + SPRITE_OP_SAVE, computed once */
    uint8_t  *cachedDst;      /* &sp->cachedDstBank[0][0] + cacheIdx */
    uint8_t  *cachedSrc;      /* &sp->cachedSrcBank[0][0] + cacheIdx */

    // shift and routeOffset arrive pre-derived from the dispatcher
    // gate (#29); widthPx/heightPx are create-time constants cached in
    // the sprite struct.
    clippedX  = (int16_t)(x & ~1);
    heightPx  = sp->heightPx;
    copyBytes = (uint16_t)((sp->widthPx >> 1) + shift);  /* shift is x & 1, exactly 0 or 1 */

    screenPtr = (uint8_t *)&src->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)clippedX >> 1)];
    SPLIT_POINTER(screenPtr,     &screenLo, &screenBank);
    SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);

    backup->sprite    = sp;
    backup->x         = clippedX;
    backup->y         = y;
    backup->width     = (uint16_t)(copyBytes << 1);
    backup->height    = heightPx;
    /* sizeBytes is constant per (sprite, shift); cache to dodge the
     * per-call ~CUMUL2 (uint16_t * uint16_t) helper. The byte-pointer
     * arithmetic avoids reintroducing ~MUL4 for the uint16_t array
     * indexing. */
    {
        uint16_t *sizeCachePtr = (uint16_t *)((uint8_t *)sp->cachedSizeBytes + ((uint16_t)shift << 1));
        if (*sizeCachePtr == 0) {
            *sizeCachePtr = (uint16_t)(copyBytes * heightPx);
        }
        backup->sizeBytes = *sizeCachePtr;
    }

    /* Compute the 1D index into the cached* bank arrays once;
     * SPRITE_ROUTINE_INDEX (spriteInternal.h) owns the multiply-free
     * derivation. */
    cacheIdx  = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_SAVE);
    cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
    cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;

    fnAddr = codegenArenaBaseAddr()
           + sp->slot->offset
           + (uint32_t)routeOffset;

    // Patch the MVN bank operands only if the dst/src bank pair changed since
    // last call (the routine's only dynamic bytes). SAVE copies screen ->
    // backup: dst = backup bank, src = screen bank.
    if (*cachedDst != backupBank || *cachedSrc != screenBank) {
        routine = (uint8_t *)fnAddr;
        patchMvnBanks(routine, heightPx, /*dst*/backupBank, /*src*/screenBank);
        *cachedDst = backupBank;
        *cachedSrc = screenBank;
    }

    // Clean C-ABI dispatch -- void save(uint32_t packed), packed = srcOff |
    // (dstOff << 16). The compiler emits the indirect JSL: no self-modifying
    // call stub, no inline asm. SAVE: src = screen offset, dst = backup offset.
    {
        uint32_t packed = (uint32_t)screenLo | ((uint32_t)backupLo << 16);
        ((void (*)(uint32_t))fnAddr)(packed);
    }
}


// Fused stage-save C glue (perf-hunt-2 rank 10): keeps the backup-
// metadata fills but replaces spriteCompiledSaveUnder's C-side
// screen-pointer derive (SURFACE_ROW_OFFSET + pointer split) and
// indirect fn-pointer call with ONE JSL into iigsSpriteSaveStage,
// which derives the screen offset from the pinned stage base asm-side.
// Caller owns s == gStage, the NOT_COMPILED gate, and (for the safe
// entry) validation. Mirrors spriteCompiledRestoreMark's shape.
// P7-2 STAGE 2 WAS BUILT AND REVERTED (2026-07-19): moving these
// metadata fills into a 10-arg asm entry benched save 722 -> 602 --
// llvm-mos's marshalling of the extra stack args + pointer splits cost
// more than the far stores it deleted (the fnAddr-cache wall again).
static inline __attribute__((always_inline)) void spriteCompiledSaveUnderStage(jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    int16_t   clippedX;
    uint16_t  heightPx;
    uint16_t  copyBytes;
    uint16_t  backupLo;
    uint8_t   backupBank;
    uint8_t   screenBank;
    uint32_t  fnAddr;
    uint16_t  cacheIdx;
    uint8_t  *cachedDst;
    uint8_t  *cachedSrc;

    clippedX  = (int16_t)(x & ~1);
    heightPx  = sp->heightPx;
    copyBytes = (uint16_t)((sp->widthPx >> 1) + shift);  /* shift is x & 1 */

    SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);
    screenBank = ((const uint8_t *)&gStage->pixels)[2];

    backup->sprite = sp;
    backup->x      = clippedX;
    backup->y      = y;
    backup->width  = (uint16_t)(copyBytes << 1);
    backup->height = heightPx;
    {
        uint16_t *sizeCachePtr = (uint16_t *)((uint8_t *)sp->cachedSizeBytes + ((uint16_t)shift << 1));
        if (*sizeCachePtr == 0) {
            *sizeCachePtr = (uint16_t)(copyBytes * heightPx);
        }
        backup->sizeBytes = *sizeCachePtr;
    }

    cacheIdx  = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_SAVE);
    cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
    cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;

    fnAddr = codegenArenaBaseAddr()
           + sp->slot->offset
           + (uint32_t)routeOffset;

    if (*cachedDst != backupBank || *cachedSrc != screenBank) {
        patchMvnBanks((uint8_t *)fnAddr, heightPx, /*dst*/backupBank, /*src*/screenBank);
        *cachedDst = backupBank;
        *cachedSrc = screenBank;
    }
    iigsSpriteSaveStage(fnAddr, backupLo, (uint16_t)clippedX, (uint16_t)y);
}


static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    uint16_t  heightPx;
    uint16_t  screenLo;
    uint16_t  backupLo;
    uint8_t   screenBank;
    uint8_t   backupBank;
    uint32_t  fnAddr;
    uint8_t  *routine;
    uint8_t  *screenPtr;
    jlSpriteT  *sp;
    uint16_t  cacheIdx;       /* shift * SPRITE_OP_COUNT + SPRITE_OP_RESTORE, computed once */
    uint8_t  *cachedDst;
    uint8_t  *cachedSrc;

    // shift and routeOffset arrive pre-derived from the
    // jlSpriteRestoreUnder gate (#29) -- no per-call re-inference of
    // the shift from the backup here.
    sp       = backup->sprite;
    heightPx = backup->height;

    screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
    SPLIT_POINTER(screenPtr,     &screenLo, &screenBank);
    SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);

    /* Hoist 2D-array indexing -- see save-side comment. */
    cacheIdx  = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE);
    cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
    cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;

    fnAddr = codegenArenaBaseAddr()
           + sp->slot->offset
           + (uint32_t)routeOffset;

    // Patch the MVN bank operands only if they changed (the routine's only
    // dynamic bytes). RESTORE copies backup -> screen: dst = screen bank,
    // src = backup bank.
    if (*cachedDst != screenBank || *cachedSrc != backupBank) {
        routine = (uint8_t *)fnAddr;
        patchMvnBanks(routine, heightPx, /*dst*/screenBank, /*src*/backupBank);
        *cachedDst = screenBank;
        *cachedSrc = backupBank;
    }

    // Clean C-ABI dispatch -- void restore(uint32_t packed), packed = srcOff |
    // (dstOff << 16). RESTORE: src = backup offset, dst = screen offset.
    {
        uint32_t packed = (uint32_t)backupLo | ((uint32_t)screenLo << 16);
        ((void (*)(uint32_t))fnAddr)(packed);
    }
}


// Fused-restore C glue (NATIVE-PERF Phase 6 W1): MVN bank-patch check
// (screen bank read from the stage's pinned pixels pointer -- byte 2
// of the 32-bit field, no second $01 source of truth in C), then ONE
// JSL into iigsSpriteRestoreMark, which derives the screen offset from
// the pinned stage base, long-calls the compiled MVN routine, and
// widens the dirty band in asm. Caller validated the backup geometry,
// gated s == gStage, and gate-checked routeOffset; bh == sp->heightPx
// (validated), which patchMvnBanks needs.
static inline __attribute__((always_inline)) void spriteCompiledRestoreMark(jlSpriteT *sp, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset, int16_t bx, int16_t by, uint16_t bw, uint16_t bh) {
    uint16_t  backupLo;
    uint8_t   backupBank;
    uint8_t   screenBank;
    uint32_t  fnAddr;
    uint16_t  cacheIdx;
    uint8_t  *cachedDst;
    uint8_t  *cachedSrc;

    SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);
    screenBank = ((const uint8_t *)&gStage->pixels)[2];

    fnAddr = codegenArenaBaseAddr()
           + sp->slot->offset
           + (uint32_t)routeOffset;

    cacheIdx  = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE);
    cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
    cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
    if (*cachedDst != screenBank || *cachedSrc != backupBank) {
        patchMvnBanks((uint8_t *)fnAddr, bh, screenBank, backupBank);
        *cachedDst = screenBank;
        *cachedSrc = backupBank;
    }
    iigsSpriteRestoreMark(fnAddr, backupLo, (uint16_t)bx, (uint16_t)by, bw, bh);
}


#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000)

// Amiga AND X68000 planar dispatchers. Both ports emit through
// spriteEmitPlanar68k.c (see src/codegen/spriteStage.c), so both MUST call
// through the 4-plane signature below. X68000 was added to the emitter ladder
// without this arm, so it fell through to the chunky #else and called a
// 4-argument routine with ONE argument: p1/p2/p3 became the caller's saved
// d3/d4/d5, and the emitted code wrote through them. In uber.c's drawShowcase
// d4 held &jlFillRect, so the sprite draw overwrote that function's own
// prologue and the next call through it faulted on a garbage stack slot.
// The two ports' planar layouts are identical -- X68kPlanarT mirrors
// AmigaPlanarT field-for-field and both strides are SURFACE_WIDTH/8 = 40 --
// so one implementation serves both through these three aliases.
//
// Amiga planar dispatchers. spriteEmitPlanar68k.c emits DRAW routines
// with a cdecl(p0, p1, p2, p3) signature that write directly to
// bitplanes, one pre-shifted variant per x % 8 phase (all 8 compiled
// since NATIVE-PERF Phase 1). Compute byteOff = y*40 + x/8 and pass
// plane[i]+byteOff as the 4 plane args. Every caller gate in
// src/core/sprite.c checks routineOffsets[shift][op] !=
// SPRITE_NOT_COMPILED before calling here (#29); anything uncompiled
// (clip, degrade-tier truncation) falls back to the interpreted
// jlpSpriteDrawPlanes / jlpSpriteSavePlanes / jlpSpriteRestorePlanes.
//
// Plane pointers come straight from portData via amigaPlanar.h's
// inline accessor (NATIVE-PERF Phase 2) -- the old jlpSurfacePlanePtr
// cross-TU call cost ~190 cycles PER PLANE, x4 per dispatch. The
// pd == NULL gate preserves the "not a planar surface -> no-op"
// contract those calls provided.

#if defined(JOEYLIB_PLATFORM_X68000)
#include "x68kPlanar.h"
#define JL_PLANAR_T            X68kPlanarT
#define JL_SURFACE_PLANAR(_s)  x68kSurfacePlanar(_s)
#define JL_PLANAR_BYTES_PER_ROW X68K_BYTES_PER_ROW
#else
#include "amigaPlanar.h"
#define JL_PLANAR_T            AmigaPlanarT
#define JL_SURFACE_PLANAR(_s)  amigaSurfacePlanar(_s)
#define JL_PLANAR_BYTES_PER_ROW AMIGA_BYTES_PER_ROW
#endif

static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
    typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
    JL_PLANAR_T  *pd;
    uint16_t      byteOff;
    DrawFn        fn;

    pd = JL_SURFACE_PLANAR(dst);
    if (pd == NULL) {
        return;
    }
    byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)x >> 3));
    fn      = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff);
}


// Compiled save/restore (NATIVE-PERF Phase 1): the emitted routines are
// cdecl(p0, p1, p2, p3, buf) plane-run copiers over the 16-px-group
// window (bx = x & ~15, nGroups = SPRITE_GROUPS0 + class). shift here
// is the width CLASS from SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS,
// not the draw bit-phase. Row displacements are baked into the
// routine; the window's x phase enters only through the plane base
// pointers (byteOff is even by construction -- bx % 16 == 0 -- so the
// emitted move.w/move.l are 68000-legal).
static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf);
    JL_PLANAR_T  *pd;
    int16_t       bx;
    uint16_t      nGroups;
    uint16_t      byteOff;
    CopyFn        fn;

    pd = JL_SURFACE_PLANAR(src);
    if (pd == NULL) {
        return;
    }
    bx      = (int16_t)(x & ~15);
    nGroups = (uint16_t)(SPRITE_GROUPS0(sp->widthPx) + shift);
    byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)bx >> 3));

    backup->x         = bx;
    backup->y         = y;
    backup->width     = (uint16_t)(nGroups << 4);
    backup->height    = sp->heightPx;
    backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);

    fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
}


static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf);
    JL_PLANAR_T  *pd;
    uint16_t      byteOff;
    CopyFn        fn;

    (void)shift;
    pd = JL_SURFACE_PLANAR(dst);
    if (pd == NULL) {
        return;
    }
    byteOff = (uint16_t)((uint16_t)backup->y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
    fn      = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset);
    fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
}

#undef JL_PLANAR_T
#undef JL_SURFACE_PLANAR
#undef JL_PLANAR_BYTES_PER_ROW

#elif defined(JOEYLIB_PLATFORM_ATARIST)

// ST word-interleaved planar runtime dispatch. The JIT routine takes
// one arg: groupBase = pd->base + y*160 + (x>>4)*8 (the address of
// the first 16-pixel group the sprite touches). It walks rows by
// adda.w #160 at the end of each row. Per (row, tile_col, plane) it
// emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at
// d16(a0).
//
// shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX): the full
// intra-group phase x & 15. Phases 0/8 are the unrolled byte-aligned
// JIT; the 14 sub-byte phases are thunk + pre-shifted data tables
// driven by stSpriteDrawShiftWalker (all compiled since NATIVE-PERF
// Phase 1). Anything uncompiled falls back to jlpSpriteDrawPlanes via
// the caller gates (#29).

// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
// fact (group-interleaved layout, not chunky), so it gets its own
// name. Each 16-pixel group is 4 plane words = 8 bytes.
#define ST_GROUP_BYTES_PER_ROW   (SURFACE_WIDTH * 4 / 8)
#define ST_BYTES_PER_PLANE_GROUP 8u

// Buffer base comes straight from portData via stPlanar.h's inline
// accessor (NATIVE-PERF Phase 2); the pd == NULL gate preserves the
// "not a planar surface -> no-op" contract the old jlpSurfacePlanePtr
// cross-TU call provided.

#include "stPlanar.h"

static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
    typedef void (*DrawFn)(uint8_t *groupBase);
    StPlanarT *pd;
    uint8_t   *groupBase;
    DrawFn     fn;

    pd = stSurfacePlanar(dst);
    if (pd == NULL) {
        return;
    }
    groupBase = pd->base
              + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
              + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
    fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(groupBase);
}


// Compiled save/restore (NATIVE-PERF Phase 1): the emitted routines are
// cdecl(src, dst) raw-span copiers over the 16-px-group window
// (bx = x & ~15, nGroups = SPRITE_GROUPS0 + class); a group window's
// rows are CONTIGUOUS byte spans in the interleaved layout, so the
// routine is a pure move.l chain with the inter-row stride baked in.
// shift here is the width CLASS, not the draw bit-phase. rowStart is
// always even (bx % 16 == 0, base is long-aligned), so move.l is
// 68000-legal.
static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
    StPlanarT *pd;
    int16_t    bx;
    uint16_t   nGroups;
    uint8_t   *rowStart;
    CopyFn     fn;

    pd = stSurfacePlanar(src);
    if (pd == NULL) {
        return;
    }
    bx       = (int16_t)(x & ~15);
    nGroups  = (uint16_t)(SPRITE_GROUPS0(sp->widthPx) + shift);
    rowStart = pd->base
             + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
             + (uint16_t)((uint16_t)bx >> 4) * ST_BYTES_PER_PLANE_GROUP;

    backup->x         = bx;
    backup->y         = y;
    backup->width     = (uint16_t)(nGroups << 4);
    backup->height    = sp->heightPx;
    backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);

    fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(rowStart, backup->bytes);
}


static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
    StPlanarT *pd;
    uint8_t   *rowStart;
    CopyFn     fn;

    (void)shift;
    pd = stSurfacePlanar(dst);
    if (pd == NULL) {
        return;
    }
    rowStart = pd->base
             + (uint16_t)backup->y * ST_GROUP_BYTES_PER_ROW
             + (uint16_t)((uint16_t)backup->x >> 4) * ST_BYTES_PER_PLANE_GROUP;
    fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset);
    fn(backup->bytes, rowStart);
}

#else

static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
    typedef void (*DrawFn)(uint8_t *destRow);
    uint8_t *destRow;
    DrawFn   fn;

    destRow = &dst->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
    fn      = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(destRow);
}


// x86 / 68k compiled save: bytes are a cdecl
//   void copy(const uint8_t *src, uint8_t *dst)
// that walks heightPx rows of copyBytes from screen (stride
// SURFACE_BYTES_PER_ROW) into the contiguous backup buffer. shift and
// routeOffset arrive pre-derived from the dispatcher gate (#29).
static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
    int16_t  clippedX;
    uint16_t heightPx;
    uint16_t copyBytes;
    uint8_t *screenPtr;
    CopyFn   fn;

    clippedX  = (int16_t)(x & ~1);
    heightPx  = sp->heightPx;
    copyBytes = (uint16_t)((sp->widthPx >> 1) + shift);  /* shift is x & 1, exactly 0 or 1 */

    screenPtr = (uint8_t *)&src->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)clippedX >> 1)];

    backup->sprite    = sp;
    backup->x         = clippedX;
    backup->y         = y;
    backup->width     = (uint16_t)(copyBytes << 1);
    backup->height    = heightPx;
    backup->sizeBytes = (uint16_t)(copyBytes * heightPx);

    fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(screenPtr, backup->bytes);
}


// Mirror of save: caller swaps arg order so the same emitted shape
// drives backup -> screen. The screen-side stride lives inside the
// emitted bytes, so RESTORE has its own routine bytes (stride is
// applied to dst instead of src). routeOffset arrives pre-derived
// from the jlSpriteRestoreUnder gate (#29); shift is unused here (no
// bank-patch cache off-IIgs) but kept for the shared signature.
static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
    typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
    jlSpriteT *sp;
    uint8_t   *screenPtr;
    CopyFn     fn;

    (void)shift;
    sp        = backup->sprite;
    screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];
    fn        = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
    fn(backup->bytes, screenPtr);
}

#endif

#endif /* JOEY_SPRITE_DISPATCH_H */
