joeylib2/docs/ApiSprite.adoc

431 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.
* A compiled save on the Amiga, ST, and X68000 captures the *16-pixel-aligned
region* containing the sprite.
* 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.
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.