joeylib2/src/core/spriteInternal.h

212 lines
11 KiB
C

// 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)
#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)
#define SPRITE_GROUPS0(_wPx) ((uint16_t)(((_wPx) + 15u) >> 4))
#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).
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