// Internal sprite definitions shared between sprite.c and the
// per-platform codegen emitters. Public API users include
// joey/sprite.h instead.

#ifndef JOEYLIB_SPRITE_INTERNAL_H
#define JOEYLIB_SPRITE_INTERNAL_H

#include "codegenArenaInternal.h"
#include "joey/sprite.h"

#define SPRITE_OP_DRAW    0
#define SPRITE_OP_SAVE    1
#define SPRITE_OP_RESTORE 2
#define SPRITE_OP_COUNT   3

// Per-platform shift-variant count + dispatcher index. Count and mask
// live in ONE ladder so they cannot drift: count == mask + 1 always.
// Chunky 4bpp ports (IIgs/DOS/generic) store one nibble per pixel pair
// so the only sub-byte alignment is x & 1 -- shifts 0+1 cover EVERY x.
// Amiga planar packs 8 pixels per plane byte so all 8 bit-phases
// matter. ST word-interleaved planar groups 16 pixels per plane word
// AND the displacement pattern differs between a sprite starting in
// the word's high byte vs low byte (+1 within a group vs +7 crossing
// to the next group's high half), so the full intra-group phase x & 15
// keys the variant: 16 slots, with today's byte-aligned phases at
// slots 0 (x%16==0) and 8 (x%16==8).
// SPRITE_DEGRADE_DRAW_MASK: the DRAW shifts kept by jlSpriteCompile's
// degrade tier when the arena cannot hold the full plan -- the cheap
// byte-aligned phases every port compiled before NATIVE-PERF Phase 1.
#if defined(JOEYLIB_PLATFORM_ATARIST)
#define JOEY_SPRITE_SHIFT_COUNT   16
#define SPRITE_SHIFT_INDEX(x)     ((uint8_t)((x) & 15))
#define SPRITE_DEGRADE_DRAW_MASK  0x0101u   // phases 0 and 8
#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000)
// Both emit through spriteEmitPlanar68k.c, whose shift is the x % 8 BIT phase
// within a 1bpp plane byte -- not the chunky x % 2 nibble phase. X68000 took
// the chunky #else before this arm existed, so only 2 of 8 phases were ever
// compiled and every odd x rendered at the wrong bit offset.
#define JOEY_SPRITE_SHIFT_COUNT   8
#define SPRITE_SHIFT_INDEX(x)     ((uint8_t)((x) & 7))
#define SPRITE_DEGRADE_DRAW_MASK  0x0001u   // shift 0
#else
#define JOEY_SPRITE_SHIFT_COUNT   2
#define SPRITE_SHIFT_INDEX(x)     ((uint8_t)((x) & 1))
#define SPRITE_DEGRADE_DRAW_MASK  0x0003u   // both nibble phases
#endif

// Sentinel stored in routineOffsets[shift][op] when that op's emitter
// returned 0 bytes (i.e., the platform doesn't implement compiled
// codegen for that op yet). Distinct from a real offset of 0, which
// is valid for the first emitted op (typically DRAW shift 0).
#define SPRITE_NOT_COMPILED 0xFFFFu

// ----- Save/restore width classes (NATIVE-PERF Phase 1) -----
//
// Compiled save/restore on the planar 68k ports operates on the
// 16-px-group-aligned window covering the sprite (bx = x & ~15,
// bw = nGroups * 16), which makes every row a contiguous raw span (ST)
// or an even-offset plane run (Amiga) -- pure move.l/move.w copies.
// Because save/restore bake no masks, the emitted body depends only on
// the group count, so ALL x phases funnel into exactly TWO class
// variants per op: class 0 = ceil(w/16) groups, class 1 = one more
// (when (x & 15) pushes the sprite across a group boundary). Save and
// restore route by these macros, NOT by SPRITE_SHIFT_INDEX -- their
// table entries live at shifts 0/1 regardless of the DRAW shift count.
//
// SPRITE_RESTORE_CLASS derives the class from the backup's RECORDED
// geometry (never from x): returns 0/1 for a compiled-format backup,
// 0xFF to route to the interpreted paths. The (bx & 15) == 0 term is
// CORRECTNESS-CRITICAL on both ports, not an optimization: a backup
// written by the byte-aligned interpreted walker at x % 16 == 8 can
// record copyBytes numerically equal to a class width while holding a
// DIFFERENT byte format (ST byte-col vs raw-span) or run length
// (Amiga), and on the ST its odd byte offset would address-error the
// compiled move.l reader. Chunky expansions are token-identical to the
// pre-Phase-1 code so IIgs/DOS dispatch is provably unchanged.
//
// SPRITE_BACKUP_PTR_OK: the compiled 68k routines move.l/move.w through
// backup->bytes, so an odd caller buffer must route BOTH ops to the
// byte-safe interpreted paths (pointer parity is stable between save
// and restore, so provenance never splits).
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000)
#define SPRITE_GROUPS0(_wPx)        ((uint16_t)(((_wPx) + 15u) >> 4))
// NOTE (P7-1, 2026-07-11): an algebraically-reduced form of these two
// macros (save class = (x&15)+((widthPx+15)&15) >= 16; restore class =
// copyBytes>>3 - GROUPS0 with an unsigned range test) was proven
// value-identical over the full input space and benched -- it is a NET
// LOSS: save was bit-identical on both 68k ports, restore was neutral
// on ST but REGRESSED Amiga 3% (1603 -> 1553 ops/s) because gcc-amigaos
// lowered the "simpler" C to +3 instructions. Same compiler-bound
// outcome as the IIgs fnAddr-cache autopsy: the wrapper is gated by
// register allocation, not arithmetic op count. Do not resurrect
// without a codegen-level win. The real 68k sprite lever is structural
// (trusted fast-path entry / restore-mark asm fusion), not a macro
// tightening.
#define SPRITE_SAVE_CLASS(_sp, _x)  ((uint8_t)((uint16_t)((((uint16_t)(_x) & 15u) + (_sp)->widthPx + 15u) >> 4) > SPRITE_GROUPS0((_sp)->widthPx) ? 1u : 0u))
#define SPRITE_RESTORE_CLASS(_sp, _bx, _bw, _copyBytes) \
    ((uint8_t)((((uint16_t)(_bx) | (uint16_t)(_bw)) & 15u) != 0u ? 0xFFu : \
               ((_copyBytes) == (uint16_t)(SPRITE_GROUPS0((_sp)->widthPx) << 3) ? 0u : \
               ((_copyBytes) == (uint16_t)((SPRITE_GROUPS0((_sp)->widthPx) + 1u) << 3) ? 1u : 0xFFu))))
#define SPRITE_BACKUP_PTR_OK(_p)    ((((uintptr_t)(_p)) & 1u) == 0u)
#else
#define SPRITE_SAVE_CLASS(_sp, _x)  ((uint8_t)((_x) & 1))
#define SPRITE_RESTORE_CLASS(_sp, _bx, _bw, _copyBytes) \
    ((uint8_t)((_copyBytes) == (uint16_t)((_sp)->widthPx >> 1) ? 0u : \
               ((_copyBytes) == (uint16_t)(((_sp)->widthPx >> 1) + 1u) ? 1u : 0xFFu)))
#define SPRITE_BACKUP_PTR_OK(_p)    (1)
#endif

// The 4bpp nibble value treated as transparent by the interpreters
// (sprite.c) and every per-CPU emitter. Single definition so the
// transparency contract cannot drift between the compiled and
// interpreted paths.
#define TRANSPARENT_NIBBLE 0

// 1D index into the [JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT] tables
// (routineOffsets, cachedDstBank, cachedSrcBank). Spelled as
// (shift << 1) + shift instead of shift * SPRITE_OP_COUNT because the
// multiply lowers to a helper call (~MUL4) on the 65816; the shift/add
// form is two ASLs and an ADC. Must stay in lockstep with
// SPRITE_OP_COUNT == 3.
#define SPRITE_ROUTINE_INDEX(_shift, _op) ((uint16_t)((((uint16_t)(_shift) << 1) + (uint16_t)(_shift)) + (_op)))

// routineOffsets read at a precomputed SPRITE_ROUTINE_INDEX. The
// byte-pointer arithmetic dodges the multiply helper that plain
// `uint16_t arr[N][M]` indexing emits on the 65816. Yields the stored
// uint16_t offset (SPRITE_NOT_COMPILED when the op is absent for that
// shift).
#define SPRITE_ROUTE_OFFSET_AT(_sp, _idx) (*(uint16_t *)((uint8_t *)(_sp)->routineOffsets + ((uint16_t)((_idx) << 1))))

// routineOffsets[shift][op] in one step -- the form every dispatcher
// gate and spriteCompiled* callee uses. Use SPRITE_ROUTINE_INDEX +
// SPRITE_ROUTE_OFFSET_AT instead when the index is also needed for the
// bank-patch cache pointers (IIgs save/restore).
#define SPRITE_ROUTE_OFFSET(_sp, _shift, _op) SPRITE_ROUTE_OFFSET_AT((_sp), SPRITE_ROUTINE_INDEX((_shift), (_op)))

struct jlSpriteT {
    // wTiles * hTiles * 32 bytes; never NULL for a live sprite.
    // Caller-owned for jlSpriteCreate; a sprite-owned copy
    // (ownsTileData true) for jlSpriteCreateFromSurface and
    // jlSpriteBankLoad, freed by jlSpriteDestroy.
    const uint8_t *tileData;
    uint8_t        widthTiles;
    uint8_t        heightTiles;
    bool           ownsTileData; // true if jlSpriteDestroy must free tileData

    // Create-time pixel dimensions (widthTiles * 8 / heightTiles * 8),
    // cached by spriteInitFields so the per-call dispatchers and
    // spriteCompiled* callees never re-derive them (#29).
    uint16_t       widthPx;
    uint16_t       heightPx;

    // Compiled-path state. slot==NULL means not yet compiled (or
    // compile failed); jlSpriteDraw falls back to the interpreter.
    // The fn-call address for (shift, op) is computed at draw time:
    //   (codegenArenaBase() + slot->offset + routineOffsets[shift][op])
    // so a codegenArenaCompact that moves the slot's bytes is
    // transparent to the caller.
    ArenaSlotT    *slot;
    uint16_t       routineOffsets[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];

#if defined(JOEYLIB_PLATFORM_IIGS)
    // Per-shift, per-op MVN bank-patch cache for IIgs save/restore.
    // patchMvnBanks rewrites 16+ MVN bank operands every call, but the
    // banks themselves rarely change frame-to-frame (screen surface
    // is fixed; backup buffer is allocated once). After the first
    // patch, subsequent calls compare requested banks to the cache
    // and skip the re-stamp loop. 0xFF means "never patched yet".
    // IIgs-only: no other port bank-patches, so the fields (and their
    // resets in spriteResetCompiledState) compile away elsewhere.
    uint8_t        cachedDstBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
    uint8_t        cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];

    // Cached `copyBytes * heightPx` per shift for spriteCompiledSaveUnder's
    // `backup->sizeBytes` field. uint16_t * uint16_t goes through a
    // software multiply helper (~30-50 cyc); cache hit dodges it. Filled
    // lazily on first call (0 sentinel = uncached).
    // (A W1 resolved-fnAddr cache with arena-epoch invalidation also
    // lived here briefly -- benched as a net LOSS on llvm-mos and
    // removed; see the note in spriteDispatch.h.)
    uint16_t       cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT];
#endif
};

// Compiled entry points. Implemented alongside jlSpriteCompile in
// src/codegen/spriteCompile.c. Each handles the per-platform calling
// convention the emitted bytes use (cdecl stack args on x86 and 68k;
// C-ABI function pointers with per-call MVN bank patching on IIgs).
// The dispatchers in src/core/sprite.c call these when sp->slot is
// non-NULL, the matching routineOffsets entry is not
// SPRITE_NOT_COMPILED, and the draw/save/restore is fully on-surface.
// The gate already derived shift (SPRITE_SHIFT_INDEX) and routeOffset
// (SPRITE_ROUTE_OFFSET) to make that decision, so it passes them in
// and the callees never re-derive them (#29). routeOffset is the
// gate-checked entry for the callee's own op; shift feeds the IIgs
// bank-patch cache index and the chunky save copyBytes (draw needs
// neither, so it only takes routeOffset).
// The spriteCompiled* runtime dispatchers are always-inline functions
// in spriteDispatch.h, included ONLY by sprite.c (NATIVE-PERF Phase 2
// R5) -- no extern declarations here or the static inline definitions
// would conflict.

// Initialize a freshly-allocated jlSpriteT to the uncompiled/default
// state (tile-data fields, 0xFF routine-offset fill, bank/size cache
// sentinels) and register it for spriteSystemShutdown. The single
// init path for every sprite -- jlSpriteCreate and
// jlSpriteCreateFromSurface in sprite.c plus jlSpriteBankLoad in
// assetLoad.c -- so the sentinel rules live in one place
// (PERF-AUDIT.md #32). Defined in sprite.c.
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);

// Sprite registry (sprite.c, PERF-AUDIT.md #34). spriteInitFields
// registers every sprite it initializes; jlSpriteCompile re-registers
// on success (idempotent) as a belt-and-braces guard, since compile
// is the moment the sprite first holds an arena slot.
// jlSpriteDestroy unregisters. The registry holds at most
// SPRITE_REGISTRY_CAP sprites (see sprite.c); beyond that,
// registration is silently skipped and the sprite MUST be destroyed
// before jlShutdown -- the same implicit contract that applied to
// every sprite before the registry existed.
void spriteRegistryAdd(jlSpriteT *sp);

// Called from jlShutdown BEFORE codegenArenaShutdown: resets every
// registered sprite's compiled-path state (slot pointer, routine
// offsets, patch caches) so a sprite surviving a shutdown -> re-init
// cycle can neither free a stale ArenaSlotT (heap corruption) nor
// execute stale offsets inside the new arena. Frees nothing --
// codegenArenaShutdown releases the slots right after.
void spriteSystemShutdown(void);

#endif
