joeylib2/include/joey/sprite.h

249 lines
13 KiB
C

// Sprites: rectangles of 8x8 tiles drawn at arbitrary pixel positions
// with color-0 transparency.
//
// A sprite's pixel data is `widthTiles * heightTiles * 32` bytes,
// 4bpp packed, laid out as a flat blob of 8x8 tiles. Tile order is
// row-major (tile (0,0), tile (1,0), ..., tile (widthTiles-1,0),
// tile (0,1), ...). Within each tile, rows are top-to-bottom and
// each row is 4 bytes (8 pixels at 4bpp packed; high nibble = left
// pixel).
//
// Color 0 is always transparent on draw (DESIGN.md contract). Use a
// tile-block draw if you need an opaque rectangle.
//
// Performance contract: jlSpriteDraw should be at least as fast as the
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST
// 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op
// variants into the codegen arena and the draw/save/restore calls
// dispatch to the compiled routines, falling back to the interpreter
// for clipped draws or uncompiled variants. On the IIgs the compiled
// path is temporarily toolchain-gated off (PERF-AUDIT.md #77).
// jlSpritePrewarm compiles ahead of the first draw so the emit cost
// does not land mid-frame.
#ifndef JOEYLIB_SPRITE_H
#define JOEYLIB_SPRITE_H
#include "platform.h"
#include "surface.h"
#include "types.h"
// Sprite codegen emits per-shift variants so smooth horizontal motion
// at any pixel position uses pre-shifted routines without runtime
// bit-shifting. The variant count is per-platform (chunky 4bpp ports
// need only x & 1; Amiga planar needs x & 7; ST word-interleaved
// planar needs the full x & 15 intra-group phase) and lives with the
// dispatcher's index macro in src/core/spriteInternal.h so count and
// mask cannot drift. jlSpriteT is opaque to applications, so the
// count is not part of the public ABI.
typedef struct jlSpriteT jlSpriteT;
// jlSpriteBackupT holds the destination bytes that lived under a sprite
// before it was drawn, so the application can restore them after the
// sprite moves. Sized for the largest backup the app will need; a
// stack-allocated jlSpriteBackupT plus a caller-owned byte buffer keeps
// the runtime allocation-free.
typedef struct {
jlSpriteT *sprite;
int16_t x;
int16_t y;
uint16_t width; // pixels
uint16_t height; // pixels
uint8_t *bytes; // caller-owned, capacity >= sizeBytes
uint16_t sizeBytes;
} jlSpriteBackupT;
// Load up to maxCels cels from a baked .spr file produced by
// tools/assetbake/assetbake.py. Each cel becomes a freshly-allocated
// jlSpriteT (release with jlSpriteDestroy); the file's cellCount is
// capped at maxCels.
//
// If outPalette is non-NULL and the file embeds a 16-entry $0RGB
// LE16 palette, it is copied in; otherwise outPalette is unchanged.
//
// Returns the number of cels actually written into outCels[]. Returns
// 0 if the file is missing, the magic / target byte is wrong, or no
// cel could be allocated.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
// Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank
// (tools/spritebake output for THIS platform). Identical contract to
// jlSpriteBankLoad, but each cel arrives already compiled: its native blit
// routines are copied straight into the codegen arena and the sprite draws at
// full compiled speed with NO runtime jlSpriteCompile. A cel that does not fit
// the arena is loaded uncompiled (interpreter fallback), never an error -- so
// this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a
// jlSpriteCompile/Prewarm pass. Returns 0 if the file is missing or its magic
// / target / shift-count does not match this build.
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
// Write an already-compiled sprite bank to a .spc cache file (the runtime twin
// of the offline tools/spritebake). Call after jlSpriteBankLoad + a
// jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the
// cache and skip the JIT -- i.e. SHIP the small cross-platform .spr, compile
// once, and cache the (large) per-platform compiled bank on first run. `cels`
// are `count` sprites (uncompiled ones are cached as-is and JIT again next
// run); `palette` (or NULL) is embedded. Returns false if the destination
// isn't writable (e.g. a write-protected disk), which is harmless -- the app
// just keeps compiling at startup.
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
uint16_t count, const uint16_t *palette);
// Wrap a tile-data blob in a jlSpriteT. The tile data must outlive the
// jlSpriteT; we do not copy it. Returns NULL if widthTiles or
// heightTiles is 0, or if the codegen arena cannot fit a placeholder
// entry for this sprite.
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles);
// Release a jlSpriteT and any codegen entries cached for it. The tile
// data the sprite was constructed from is NOT freed -- the caller
// owns that buffer.
void jlSpriteDestroy(jlSpriteT *sp);
// Compile the sprite's draw routines into the codegen arena. After
// this returns true, jlSpriteDraw uses the compiled fast path on
// platforms where the emitter is wired (all four ports). Returns
// false if the arena is full (caller may run jlSpriteCompact and
// retry), the platform doesn't have a real emitter yet, or the
// sprite has no source tile data.
//
// Idempotent: calling on a sprite that's already compiled is a
// no-op and returns true.
bool jlSpriteCompile(jlSpriteT *sp);
// Returns the number of bytes this sprite occupies in the codegen
// arena (sum of all compiled draw/save/restore shift variants).
// Returns 0 if the sprite has never been compiled or has no slot.
// Diagnostic only -- useful for tuning jlConfigT.codegenBytes against
// the actual per-cel cost of the current platform's emitter.
uint32_t jlSpriteCompiledSize(const jlSpriteT *sp);
// Hint that this sprite will be drawn soon. Currently a wrapper
// around jlSpriteCompile that ignores the return value, kept for API
// symmetry with the rest of the library and for callers that don't
// care about compile success.
void jlSpritePrewarm(jlSpriteT *sp);
// Draw the sprite at pixel (x,y) on the destination surface. Pixels
// equal to color 0 in the sprite source are skipped (transparent).
// Off-surface portions are clipped.
void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// TRUSTED draw (P7-1 family). Same trust contract as
// jlSpriteRestoreUnderTrusted: skips the per-call geometry validation
// for a caller that pre-compiles its sprites and guarantees the sprite
// is FULLY on-surface at (x,y); UB if violated (no clipping happens).
// Falls back to the fully-validated jlSpriteDraw for uncompiled
// sprites and on ports without a compiled fast path.
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// Worst-case backup capacity for a widthTiles x heightTiles sprite at
// ANY x, on every port. The planar 68k compiled save/restore operates
// on the 16-pixel-group-aligned window covering the sprite (x rounded
// DOWN to a 16-px boundary, width UP), so the worst case is one extra
// 16-px group: (ceil(w/16) + 1) * 8 bytes per row. This also covers the
// chunky ports' (w/2 + 1) * h trivially. Give the backing array 2-byte
// alignment (e.g. uint16_t backing or __attribute__((aligned(2)))) --
// odd buffer pointers route save/restore to the slower interpreted
// paths on planar ports (68000 word ops require even addresses).
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) \
((uint16_t)((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)))
// Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least
// JOEY_SPRITE_BACKUP_BYTES(widthTiles, heightTiles) bytes of capacity
// for fully in-bounds draws; for clipped draws only the visible bytes
// are stored. The captured region's exact geometry is reported in
// backup->x/width/height/sizeBytes -- on planar ports (Amiga, ST) it is
// the 16-px-aligned window containing the sprite, so the region a
// caller must not invalidate before restoring is the RECORDED window,
// not just the sprite rect.
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// TRUSTED save-under (P7-1). Same trust contract as
// jlSpriteRestoreUnderTrusted (skips the per-call geometry validation
// for a caller that pre-compiles its sprites and keeps them on-screen;
// UB if violated). Falls back to the fully-validated jlSpriteSaveUnder
// for uncompiled sprites.
void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// Repaint the destination region from a jlSpriteBackupT captured by a
// prior jlSpriteSaveUnder. The backup must not have been invalidated
// by other writes that overlapped its captured region.
void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
// TRUSTED fast-path restore (NATIVE-PERF Phase 7, P7-1). Identical
// effect to jlSpriteRestoreUnder, but SKIPS the geometry-validation
// chain (~11 range/alignment checks + the NULL and sprite-slot gates)
// that jlSpriteRestoreUnder runs every call. The caller MUST guarantee
// ALL of the following or behavior is UNDEFINED (out-of-bounds writes,
// crashes):
// * s and backup (and backup->sprite, backup->bytes) are non-NULL,
// * backup was produced by a jlSpriteSaveUnder that took the COMPILED
// path (a "trusted" backup: 16-px-aligned x/width on planar ports,
// even x/width on chunky, backup->height == the sprite's pixel
// height, buffer at least 2-byte aligned),
// * the recorded window is fully on the destination surface.
// It is the expert inner-loop entry for a game that pre-compiles its
// sprites (jlSpritePrewarm) and already clamps positions on-screen --
// use jlSpriteRestoreUnder anywhere those invariants are not statically
// guaranteed. Anything the trusted fast path does not recognize (an
// uncompiled shift, a non-stage / non-16-row window on the 68k ports)
// falls back to the fully-validated jlSpriteRestoreUnder, so a
// conservative caller degrades safely rather than corrupting -- but the
// NULL / on-surface guarantees above are still the caller's.
void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup);
// Combined save-then-draw entry point. The common animation pattern
// captures the destination bytes about to be overwritten, then draws
// the sprite. Both ops share validation, the destination ptr is
// computed once, and a single dirty-rect mark covers both. Saves
// roughly one full dispatcher chain (~150 cyc on IIgs) per
// frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
//
// Identical semantics to:
// jlSpriteSaveUnder(s, sp, x, y, backup);
// jlSpriteDraw(s, sp, x, y);
// modulo: the dirty rect is marked once for the union (which here is
// just the draw rect, since save doesn't write).
void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// TRUSTED save-and-draw (P7-1). The inner-loop companion to
// jlSpriteRestoreUnderTrusted -- same trust contract (skips the per-call
// geometry validation for a caller that pre-compiles its sprites and
// keeps them on-screen; UB if those guarantees are violated). The
// canonical pattern is a trusted save-and-draw this frame, a trusted
// restore next. Falls back to the fully-validated jlSpriteSaveAndDraw
// for anything the fast path does not recognize.
void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// Snapshot an 8x8-aligned region of a jlSurfaceT into a new jlSpriteT.
// The captured pixel data is copied into a sprite-owned buffer so
// the source surface can be modified afterwards. Width and height
// are in TILES (each tile = 8x8 pixels). x and y are in pixels and
// must be aligned to a tile boundary (multiple of 8) on the source
// surface; misaligned coordinates return NULL.
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y,
uint8_t widthTiles, uint8_t heightTiles);
// Defragment the codegen arena. Walks live sprite slots and
// memmoves them down to consolidate free space; any holes left by
// destroyed sprites are reclaimed. Costs O(arena_used_bytes); call
// between levels rather than per frame. jlSpriteT pointers held by
// the application are NOT invalidated -- internal indirection
// through the slot record means draw calls automatically pick up
// the new code address on the next call.
void jlSpriteCompact(void);
// Arena introspection. Used to gauge whether jlSpriteCompact is
// worth running, or whether the codegenBytes budget needs to grow.
// Free space is (Total - Used), but it may be fragmented across
// holes until jlSpriteCompact runs.
uint32_t jlSpriteCodegenBytesUsed(void);
uint32_t jlSpriteCodegenBytesTotal(void);
#endif