212 lines
11 KiB
C
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
|