432 lines
15 KiB
Text
432 lines
15 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*.
|
|
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(_type, _wT, _hT) /* ... */
|
|
----
|
|
|
|
How many `_type` elements a backup buffer needs for a `widthTiles` by
|
|
`heightTiles` sprite at any x position, on every port, rounded up. Use it to
|
|
size your backing array; with `uint8_t` it is the size in bytes.
|
|
|
|
[source,c]
|
|
----
|
|
static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
|
|
----
|
|
|
|
[WARNING]
|
|
====
|
|
Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and
|
|
X68000 an odd buffer address sends the save and restore calls down a much
|
|
slower path, and it makes the trusted variants crash; the
|
|
other ports do not mind. Declaring it `uint16_t`, as above, guarantees the
|
|
alignment.
|
|
====
|
|
|
|
=== 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_behaviour_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 colour 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 buffer of at least `jlSpriteBackupLength(uint8_t, w, h)` bytes 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 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 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.
|