// 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: spriteDraw should be at least as fast as the // IIgs reference (the DESIGN.md spec calls for runtime-compiled draw // code per CPU). v1 ships with an interpreted fallback that gives // correct output everywhere; codegen lands per platform after the // API stabilizes. spritePrewarm is a hint that the application is // about to draw the sprite repeatedly -- a future codegen-enabled // build will use it to compile shift variants ahead of the first // draw. With the interpreter it is a no-op. #ifndef JOEYLIB_SPRITE_H #define JOEYLIB_SPRITE_H #include "platform.h" #include "surface.h" #include "types.h" // Sprite codegen emits per-shift variants. Chunky 4bpp ports (DOS, // IIgs, Atari ST) only need 2 shifts -- pixel offset 0 (sprite/dest // byte boundaries align) and offset 1 (every dest byte combines two // sprite bytes' nibbles). Planar ports (Amiga -- 8 px per plane byte) // need 8 shifts: one for each x % 8 alignment, so smooth horizontal // motion at any pixel position uses pre-shifted source bytes without // runtime bit-shifting. Allocate the max so routineOffsets[] has // slots for every variant; chunky ports leave shifts 2..7 as // SPRITE_NOT_COMPILED, planar ports use all 8. #define JOEY_SPRITE_SHIFT_COUNT 8 typedef enum { SPRITE_FLAGS_NONE = 0 } SpriteFlagsE; typedef struct SpriteT SpriteT; // SpriteBackupT 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 SpriteBackupT plus a caller-owned byte buffer keeps // the runtime allocation-free. typedef struct { SpriteT *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; } SpriteBackupT; // Wrap a tile-data blob in a SpriteT. The tile data must outlive the // SpriteT; 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. SpriteT *spriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, SpriteFlagsE flags); // Release a SpriteT and any codegen entries cached for it. The tile // data the sprite was constructed from is NOT freed -- the caller // owns that buffer. void spriteDestroy(SpriteT *sp); // Compile the sprite's draw routines into the codegen arena. After // this returns true, spriteDraw uses the compiled fast path on // platforms where the emitter is wired (currently x86/DOS). Returns // false if the arena is full (caller may run spriteCompact and // retry), the platform doesn't have a real emitter yet, or the // sprite has no source tile data (e.g., it was loaded already // compiled via spriteLoadFile). // // Idempotent: calling on a sprite that's already compiled is a // no-op and returns true. bool spriteCompile(SpriteT *sp); // Hint that this sprite will be drawn soon. Currently a wrapper // around spriteCompile 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 spritePrewarm(SpriteT *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 spriteDraw(SurfaceT *s, SpriteT *sp, int16_t x, int16_t y); // Capture the destination region a subsequent spriteDraw at the same // (x,y) would write to. backup->bytes must have at least // (widthTiles*4) * (heightTiles*8) bytes of capacity for fully // in-bounds draws; for clipped draws only the visible bytes are // stored. The captured region's exact size is reported in // backup->sizeBytes. void spriteSaveUnder(const SurfaceT *s, SpriteT *sp, int16_t x, int16_t y, SpriteBackupT *backup); // Repaint the destination region from a SpriteBackupT captured by a // prior spriteSaveUnder. The backup must not have been invalidated // by other writes that overlapped its captured region. void spriteRestoreUnder(SurfaceT *s, const SpriteBackupT *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 ORCA-C) per // frame versus calling spriteSaveUnder + spriteDraw separately. // // Identical semantics to: // spriteSaveUnder(s, sp, x, y, backup); // spriteDraw(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 spriteSaveAndDraw(SurfaceT *s, SpriteT *sp, int16_t x, int16_t y, SpriteBackupT *backup); // Snapshot an 8x8-aligned region of a SurfaceT into a new SpriteT. // 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. SpriteT *spriteCreateFromSurface(const SurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles, SpriteFlagsE flags); // Load a sprite from a `.spr` file produced by the host-side // joeysprite tool or by spriteSaveFile. Format is target-native for // the compiled-code section: // byte 0 widthTiles // byte 1 heightTiles // bytes 2-3 codeSize (LE16) // bytes 4-5 tileBytes (LE16) = widthTiles*heightTiles*32 // ... offsets table (JOEY_SPRITE_SHIFT_COUNT * // SPRITE_OP_COUNT * uint16_t LE) // ... compiled code (codeSize bytes) // ... raw tile data (tileBytes bytes; tile-major 4bpp) // The runtime keeps both the compiled bytes (fast-path draws) and // the tile data (interpreter clip path), so loaded sprites work for // partially-off-surface draws without crashing. SpriteT *spriteLoadFile(const char *path, SpriteFlagsE flags); // Same as spriteLoadFile but parses bytes already in memory. SpriteT *spriteFromCompiledMem(const uint8_t *data, uint32_t length, SpriteFlagsE flags); // Persist a sprite to disk in `.spr` format. Sprites created via // spriteCreate / spriteCreateFromSurface that have not been // compiled yet are force-compiled here (so the resulting file can // always be loaded back via spriteLoadFile). Returns false if the // codegen arena is full or the platform's emitter is not yet // implemented. bool spriteSaveFile(SpriteT *sp, const char *path); // 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. SpriteT 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 spriteCompact(void); // Arena introspection. Used to gauge whether spriteCompact is // worth running, or whether the codegenBytes budget needs to grow. // Free space is (Total - Used), but it may be fragmented across // holes until spriteCompact runs. uint32_t spriteCodegenBytesUsed(void); uint32_t spriteCodegenBytesTotal(void); #endif