joeylib2/docs/ApiSprite.adoc
2026-09-26 21:02:41 -05:00

406 lines
12 KiB
Text

== Sprites
Declared in `<joey/sprite.h>`.
A sprite is a rectangle of 8x8 tiles drawn at an arbitrary pixel position with
colour-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*.
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.
[IMPORTANT]
====
`jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through
an interpreter roughly *forty times slower*, and nothing reports this. Always
call `jlSpriteCompile` or `jlSpritePrewarm` 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. On planar ports the captured region is the
*16-pixel-aligned window* containing the sprite, not the sprite rectangle, and
the `x`, `width`, `height` and `sizeBytes` fields report that true geometry. It
is the recorded window you must not overwrite before restoring.
=== JOEY_SPRITE_BACKUP_BYTES
[source,c]
----
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) /* ... */
----
Worst-case backup capacity for a `widthTiles` by `heightTiles` sprite at any x
position, on every port. Use it to size your backing array.
[source,c]
----
// 2-byte aligned by construction -- see the warning below.
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
----
[WARNING]
====
Give the backing array *at least 2-byte alignment*. An odd buffer address
routes save and restore onto slow interpreted paths on the planar ports,
because the 68000 cannot do word accesses at odd addresses. Declaring it
`uint16_t`, as above, guarantees this.
====
=== 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 or its
magic, target or shift count does not match this build.
=== 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. Uncompiled ones are cached
as-is and will compile again next run.
|`count` |`uint16_t` |How many sprites.
|`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given.
|===
*Returns:* `true` on success; `false` if 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 either dimension is 0 or the codegen arena
cannot fit a placeholder entry.
=== 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 routines into the codegen arena. 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 the arena is full (you
may call `jlSpriteCompact` and retry) or the sprite has no source tile data.
=== 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.
=== jlSpritePrewarm
[source,c]
----
void jlSpritePrewarm(jlSpriteT *sp);
----
Hints that a sprite will be drawn soon. A wrapper around `jlSpriteCompile` that
ignores the return value, for callers that do not need to handle failure.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`sp` |`jlSpriteT *` |Sprite to compile.
|===
*Returns:* nothing.
=== 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 colour 0 are skipped.
Off-surface portions are clipped.
[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 buffer of at least `JOEY_SPRITE_BACKUP_BYTES(w, h)` capacity for fully
in-bounds draws. For clipped draws only the visible bytes are stored.
|===
*Returns:* nothing. The captured geometry is reported in the backup's `x`,
`width`, `height` and `sizeBytes` fields.
=== 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 window.
|===
*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 gates.
Use them only in a game that pre-compiles its sprites and already clamps
positions on screen. Anything the fast path does not recognise 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]
====
Behaviour 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 planar ports, even on chunky, height equal
to the sprite's pixel height, and a buffer at least 2-byte aligned;
* the recorded window 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 the coordinates are not tile-aligned.
=== 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.