== Sprites Declared in ``. A sprite is a rectangle of 8x8 tiles drawn at an arbitrary pixel position with color-0 transparency. Sprite pixel data is `widthTiles * heightTiles * 32` bytes of 4bpp packed data, laid out as a flat blob of tiles in row-major order. The distinguishing feature is that *sprite blitters are compiled at run time*. On the IIgs, Amiga, ST, X68000, DOS, DVX, and Win16 the library emits native 65816, 68000, or x86 code for each sprite, including pre-shifted variants so smooth horizontal motion needs no runtime bit shifting. SDL3, the web, Win32, the PSP, Haiku, and BeOS have no code emitter and always use the interpreter. [IMPORTANT] ==== `jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through an interpreter *two to six times slower*, depending on the machine, and nothing reports this. Always call `jlSpriteCompile` during loading. ==== === jlSpriteT and jlSpriteBackupT [source,c] ---- typedef struct jlSpriteT jlSpriteT; // opaque 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; ---- You supply `bytes` and its capacity; the library fills in the rest to describe what it actually captured, in `x`, `y`, `width`, `height`, and `sizeBytes`. That is often not the sprite rectangle: * `x` is rounded down to an even pixel, and `width` grows to match, on every port. * On the Amiga, ST, and X68000 a save captures the *16-pixel-aligned region* containing the sprite (or its visible part). * A compiled save on the IIgs captures only the rows that hold an opaque pixel, so `y` can be lower and `height` shorter than the sprite's. * A save of a sprite partly off the surface captures only the visible part (on the Amiga, ST, and X68000, the 16-pixel-aligned region holding it). It is the recorded region you must not overwrite before restoring. === jlSpriteBackupLength [source,c] ---- #define jlSpriteBackupLength(_wT, _hT) /* ... */ ---- How many `uint16_t` a backup buffer needs for a `widthTiles` by `heightTiles` sprite at any x position, on every port. A backup buffer *must be an array of `uint16_t`*: [source,c] ---- static uint16_t gBacking[jlSpriteBackupLength(4, 4)]; ---- That gives it the 2-byte alignment the Amiga, ST, and X68000 need: an odd address sends the save and restore calls down a much slower path there, and makes the trusted variants crash. A buffer from `jlAlloc` is `jlSpriteBackupLength(w, h) * sizeof(uint16_t)` bytes. === jlSpriteBankLoad [source,c] ---- uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); ---- Loads cels from a baked `.spr` file. Each cel becomes a freshly allocated `jlSpriteT`, released with `jlSpriteDestroy`. `.spr` banks are cross-target -- the same file works on every platform. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`path` |`const char *` |File path. |`outCels` |`jlSpriteT **` |Receives the sprites. Caller-owned array. |`maxCels` |`uint16_t` |Capacity of `outCels`; the file's cel count is capped to this. |`outPalette` |`uint16_t *` |Optional, may be `NULL`. Receives the embedded 16-entry `$0RGB` palette if present; otherwise left unchanged. |=== *Returns:* the number of cels written. Returns 0 if the file is missing, the magic or target byte is wrong, or no cel could be allocated. === jlSpriteBankLoadPrecompiled [source,c] ---- uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); ---- Loads cels from a `.spc` pre-compiled bank built for *this* platform. Identical contract to `jlSpriteBankLoad`, except each cel arrives already compiled: its native routines are copied straight into the codegen arena and it draws at full speed with no runtime compilation. A cel that does not fit the arena loads uncompiled and falls back to the interpreter -- never an error. This makes it a drop-in, faster-starting replacement for `jlSpriteBankLoad` plus a compile pass. *Parameters:* as `jlSpriteBankLoad`. *Returns:* the number of cels written, or 0 if the file is missing, its magic, target, or shift count does not match this build, its cel size is out of range, or the 64 KB the loader reads each cel's code into cannot be allocated (on the ports that interpret every sprite too). === jlSpriteBankSavePrecompiled [source,c] ---- bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette); ---- Writes an already-compiled bank to a `.spc` cache file -- the runtime twin of the offline `spritebake`. Call it after loading and compiling so a later launch can skip the compile. This lets you ship the small cross-platform `.spr` and cache the large per-platform compiled form on first run. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`path` |`const char *` |Destination path. |`cels` |`jlSpriteT **` |The sprites to cache, all of one size. Uncompiled ones are cached uncompiled, and nothing compiles them when the cache is loaded: call `jlSpriteCompile` on any cel whose `jlSpriteCompiledSize` is 0. |`count` |`uint16_t` |How many sprites. |`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given. |=== *Returns:* `true` on success; `false` if the cels are not all one size or the destination is not writable, which is harmless -- the program simply keeps compiling at startup. === jlSpriteCreate [source,c] ---- jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles); ---- Wraps a tile-data blob in a sprite. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`tileData` |`const uint8_t *` |`widthTiles * heightTiles * 32` bytes. *Not copied* -- it must outlive the sprite, and you keep ownership. |`widthTiles` |`uint8_t` |Width in tiles. Must be non-zero. |`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero. |=== *Returns:* the sprite, or `NULL` if `tileData` is `NULL`, either dimension is 0, or memory runs out. Creating a sprite does not touch the codegen arena. === jlSpriteDestroy [source,c] ---- void jlSpriteDestroy(jlSpriteT *sp); ---- Releases a sprite and any codegen entries cached for it. The tile data it was built from is *not* freed -- that buffer is yours. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`sp` |`jlSpriteT *` |Sprite to release. |=== *Returns:* nothing. === jlSpriteCompile [source,c] ---- bool jlSpriteCompile(jlSpriteT *sp); ---- Compiles the sprite's draw, save, and restore routines into the codegen arena, right away. No draw ever compiles on demand, so call it while loading, not in the frame loop (a bank loaded with `jlSpriteBankLoadPrecompiled` arrives compiled already). Idempotent -- compiling an already-compiled sprite is a no-op that returns `true`. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`sp` |`jlSpriteT *` |Sprite to compile. |=== *Returns:* `true` if the sprite is compiled. `false` if it will be drawn by the interpreter instead: the arena is full even after compacting it (`jlLastError` says so; raise `codegenBytes` or destroy sprites you no longer need), the sprite has no source tile data, the 64 KB the compiler works in cannot be allocated, no routine fits the compiler's limits (16 KB a routine), or the port has no code emitter (the Sprites row of <<_per_platform_behavior_summary>>). Only the full arena sets `jlLastError`. A `false` is never fatal: the sprite still draws, only slower. `true` means at least part of the sprite compiled, not all of it. When the arena is short, the Amiga, ST, and X68000 keep only the draws for x on an 8-pixel boundary, and a routine larger than 16 KB is left out on every port. A draw the compiled routines do not cover, and every draw of a sprite partly off the surface, goes through the interpreter. === jlSpriteCompiledSize [source,c] ---- uint32_t jlSpriteCompiledSize(const jlSpriteT *sp); ---- Diagnostic. The right way to size `jlConfigT.codegenBytes` for real artwork on the current platform, rather than guessing. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`sp` |`const jlSpriteT *` |Sprite to measure. |=== *Returns:* bytes occupied in the codegen arena, summed over all compiled shift variants. Returns 0 if the sprite was never compiled. === jlSpriteDraw [source,c] ---- void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); ---- Draws the sprite at a pixel position. Source pixels of color 0 are skipped. Off-surface portions are clipped. Compiled routines do not clip, so a sprite partly off the surface draws through the interpreter, compiled or not. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`s` |`jlSurfaceT *` |Destination surface. |`sp` |`jlSpriteT *` |Sprite to draw. |`x`, `y` |`int16_t` |Top-left pixel position. |=== *Returns:* nothing. === jlSpriteSaveUnder [source,c] ---- void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); ---- Captures the destination region that a `jlSpriteDraw` at the same position would write, so it can be restored later. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`s` |`const jlSurfaceT *` |Source surface. |`sp` |`jlSpriteT *` |Sprite whose footprint to capture. |`x`, `y` |`int16_t` |Position the sprite will be drawn at. |`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to a `uint16_t` buffer of at least `jlSpriteBackupLength(w, h)` elements for fully in-bounds draws. For clipped draws only the visible bytes are stored. On the IIgs the buffer must not cross a 64 KB bank boundary; `jlAlloc`, the C heap, and static arrays never give one that does. With `bytes` `NULL` nothing is copied: the call only fills in the region a save would record. |=== *Returns:* nothing. The captured geometry is reported in the backup's `x`, `y`, `width`, `height`, and `sizeBytes` fields (see <<_jlspritet_and_jlspritebackupt>>). === jlSpriteRestoreUnder [source,c] ---- void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup); ---- Repaints the destination from a previous capture. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`s` |`jlSurfaceT *` |Destination surface. |`backup` |`const jlSpriteBackupT *` |A backup from `jlSpriteSaveUnder`, not invalidated by other writes overlapping its recorded region. |=== *Returns:* nothing. === jlSpriteSaveAndDraw [source,c] ---- void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); ---- Captures then draws, in one call. Identical in effect to `jlSpriteSaveUnder` followed by `jlSpriteDraw`, but validation is shared, the destination pointer is computed once, and one dirty-rectangle mark covers both. Saves roughly a full dispatcher chain per sprite per frame. *Parameters:* as `jlSpriteSaveUnder`. *Returns:* nothing. === The Trusted Variants [source,c] ---- void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup); void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); ---- Expert inner-loop entry points. Each has the same effect as its plain counterpart but *skips the per-call geometry validation* -- roughly eleven range and alignment checks plus the NULL and sprite-slot checks. Use them only in a game that pre-compiles its sprites and already clamps positions on screen. Anything the fast path does not recognize falls back to the fully validated call, so a conservative caller degrades safely rather than corrupting -- but the guarantees below are still yours to keep. [WARNING] ==== Behavior is *undefined* -- out-of-bounds writes, crashes -- unless all of the following hold: * `s`, `backup`, `+backup->sprite+`, and `+backup->bytes+` are non-`NULL`; * the sprite is fully on-surface at `(x, y)`, because *no clipping happens*; * for the restore, the backup came from a save that took the compiled path: 16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the other ports, and a height equal to the sprite's pixel height (on the IIgs, to the rows the compiled save keeps, those holding an opaque pixel); * on the Amiga, ST, and X68000, `+backup->bytes+` is at least 2-byte aligned, for the trusted save and save-and-draw as well as the restore; * the recorded region is fully on the destination surface. Use the plain calls anywhere those invariants are not statically guaranteed. ==== The canonical pattern is a trusted save-and-draw this frame, a trusted restore next frame. === jlSpriteCreateFromSurface [source,c] ---- jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles); ---- Snapshots a tile-aligned region of a surface into a new sprite. The pixels are copied into a sprite-owned buffer, so the source may be modified afterwards. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`src` |`const jlSurfaceT *` |Source surface. |`x`, `y` |`int16_t` |Top-left in *pixels*, and both must be multiples of 8. |`widthTiles`, `heightTiles` |`uint8_t` |Size in *tiles*, each 8x8 pixels. |=== *Returns:* the new sprite, or `NULL` if `src` is `NULL`, a dimension is 0, `x` or `y` is not a multiple of 8, the region is not wholly on the surface, or memory runs out. === jlSpriteCompact [source,c] ---- void jlSpriteCompact(void); ---- Defragments the codegen arena, consolidating free space and reclaiming holes left by destroyed sprites. Costs time proportional to the bytes in use, so call it between levels rather than per frame. Sprite pointers you hold are *not* invalidated: internal indirection means draw calls pick up new code addresses automatically. *Parameters:* none. *Returns:* nothing. === jlSpriteCodegenBytesUsed / jlSpriteCodegenBytesTotal [source,c] ---- uint32_t jlSpriteCodegenBytesUsed(void); uint32_t jlSpriteCodegenBytesTotal(void); ---- Arena introspection, for judging whether to compact or to raise `codegenBytes`. *Parameters:* none. *Returns:* bytes in use, and total arena size. Free space is the difference, though it may be fragmented across holes until `jlSpriteCompact` runs. The total is what `jlInit` was granted, which can be less than `codegenBytes` asked for on a machine short of memory. On the IIgs it is split into blocks of at most 64 KB that compacting never merges: one sprite's code must fit in one block. The ports that interpret every sprite have no arena: both are 0.