// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // 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 (a library 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 calls for runtime-compiled draw code per // CPU). The machine ports have runtime emitters (IIgs 65816, // Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does // Win16 (x86, as DOS); // 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. Nothing compiles on demand: compile while loading, or every // draw interprets. The other hosted ports (JL_SPRITE_INTERPRETED) have no // emitter and always interpret. #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.c. 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 pass. Returns 0 if the file is missing, its magic / // target / shift-count does not match this build, its cel size is out of // range, or the 64 KB buffer a cel's code is read into cannot be allocated // (the interpreting ports read it too, then drop the code). 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, all of one size (uncompiled ones are cached // uncompiled, and nothing compiles them at load: jlSpriteCompile any whose // jlSpriteCompiledSize is 0); `palette` (or NULL) is embedded. Returns false // if the cels differ in size or 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 tileData is NULL, // widthTiles or heightTiles is 0, or memory runs out. The codegen arena // is not touched until jlSpriteCompile. 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, save, and restore routines into the codegen // arena. jlSpriteDraw never compiles on demand, so call this while // loading (or load a pre-compiled bank). After it returns true the draw, // save, and restore calls use the compiled routines that fit: a short // arena keeps only the x-on-an-8-pixel-boundary draws on the Amiga, ST, // and X68000, a routine over 16 KB is left out, and anything not compiled // (and every clipped draw) interprets. Returns false if the arena is full // even after compacting it (jlLastError says so), the sprite has no // source tile data, the compiler's 64 KB working buffer cannot be // allocated, no routine fits, or the port has no emitter // (JL_SPRITE_INTERPRETED); the sprite still draws, interpreted. // // 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); // 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); // How many _type elements a backup buffer needs for a widthTiles x // heightTiles sprite at ANY x, on every port (rounded up), so that // // static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)]; // // is big enough, and jlSpriteBackupLength(uint8_t, w, h) is the size in // bytes (for jlAlloc, say). The worst case is the 16-pixel-group-aligned // region covering the sprite (x rounded DOWN to a 16-px boundary, width // UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give // the buffer 2-byte alignment (a uint16_t array has it): on the Amiga, // ST, and X68000 an odd address sends save and restore down a much // slower path, and it crashes the trusted variants. #define jlSpriteBackupLength(_type, _wT, _hT) \ ((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type))) // Capture the destination region a subsequent jlSpriteDraw at the same // (x,y) would write to. backup->bytes must have room for // jlSpriteBackupLength(uint8_t, widthTiles, heightTiles) bytes 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/y/width/height/sizeBytes: x is rounded down to even on every // port, a save on the Amiga, ST, and X68000 takes the 16-px-aligned region // containing the sprite (or its visible part), a compiled save on the IIgs // only the rows holding an opaque pixel, and a clipped save the visible // part. // So the region a caller must not invalidate before restoring is the // RECORDED region, not just the sprite rect. On the IIgs the buffer must // not cross a 64 KB bank boundary. With backup->bytes NULL nothing is // copied: the call only fills in the region a save would record. 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 rows the compiled // save covers -- the sprite's pixel height, or on the IIgs the rows // holding an opaque pixel), // * on the Amiga, ST, and X68000, backup->bytes is at least 2-byte // aligned (the trusted save and save-and-draw need that too), // * the recorded region is fully on the destination surface. // It is the expert inner-loop entry for a game that pre-compiles its // sprites (jlSpriteCompile) 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 region on the 68k and x86 // 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. Returns NULL if src is NULL, a dimension is 0, x or y is // misaligned, the region is not wholly on the surface, or memory runs // out. 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. Total is what jlInit was granted, // which a machine short of memory can make less than codegenBytes; on the // IIgs in blocks of at most 64 KB that compacting never merges (one // sprite's code must fit in one). The interpreting ports have no arena: // both are 0. uint32_t jlSpriteCodegenBytesUsed(void); uint32_t jlSpriteCodegenBytesTotal(void); #endif