// 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