271 lines
14 KiB
C
271 lines
14 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// 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: jlSpriteDraw should be at least as fast as the
|
|
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
|
|
// code per CPU). The machine ports have runtime emitters (IIgs 65816,
|
|
// Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does
|
|
// Win16 (x86, as DOS);
|
|
// jlSpriteCompile compiles the shift/op variants into the codegen arena
|
|
// and the draw/save/restore calls dispatch to the compiled routines,
|
|
// falling back to the interpreter for clipped draws or uncompiled
|
|
// variants. Nothing compiles on demand: compile while loading, or every
|
|
// draw interprets. The other hosted ports (JL_SPRITE_INTERPRETED) have no
|
|
// emitter and always interpret.
|
|
|
|
#ifndef JOEYLIB_SPRITE_H
|
|
#define JOEYLIB_SPRITE_H
|
|
|
|
#include "platform.h"
|
|
#include "surface.h"
|
|
#include "types.h"
|
|
|
|
// Sprite codegen emits per-shift variants so smooth horizontal motion
|
|
// at any pixel position uses pre-shifted routines without runtime
|
|
// bit-shifting. The variant count is per-platform (chunky 4bpp ports
|
|
// need only x & 1; Amiga planar needs x & 7; ST word-interleaved
|
|
// planar needs the full x & 15 intra-group phase) and lives with the
|
|
// dispatcher's index macro in src/core/spriteInternal.h so count and
|
|
// mask cannot drift. jlSpriteT is opaque to applications, so the
|
|
// count is not part of the public ABI.
|
|
|
|
typedef struct jlSpriteT jlSpriteT;
|
|
|
|
// jlSpriteBackupT 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 jlSpriteBackupT plus a caller-owned byte buffer keeps
|
|
// the runtime allocation-free.
|
|
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;
|
|
|
|
// Load up to maxCels cels from a baked .spr file produced by
|
|
// tools/assetbake/assetbake.py. Each cel becomes a freshly-allocated
|
|
// jlSpriteT (release with jlSpriteDestroy); the file's cellCount is
|
|
// capped at maxCels.
|
|
//
|
|
// If outPalette is non-NULL and the file embeds a 16-entry $0RGB
|
|
// LE16 palette, it is copied in; otherwise outPalette is unchanged.
|
|
//
|
|
// Returns the number of cels actually written into outCels[]. Returns
|
|
// 0 if the file is missing, the magic / target byte is wrong, or no
|
|
// cel could be allocated.
|
|
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette);
|
|
|
|
// Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank
|
|
// (tools/spritebake output for THIS platform). Identical contract to
|
|
// jlSpriteBankLoad, but each cel arrives already compiled: its native blit
|
|
// routines are copied straight into the codegen arena and the sprite draws at
|
|
// full compiled speed with NO runtime jlSpriteCompile. A cel that does not fit
|
|
// the arena is loaded uncompiled (interpreter fallback), never an error -- so
|
|
// this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a
|
|
// jlSpriteCompile pass. Returns 0 if the file is missing, its magic /
|
|
// target / shift-count does not match this build, its cel size is out of
|
|
// range, or the 64 KB buffer a cel's code is read into cannot be allocated
|
|
// (the interpreting ports read it too, then drop the code).
|
|
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette);
|
|
|
|
// Write an already-compiled sprite bank to a .spc cache file (the runtime twin
|
|
// of the offline tools/spritebake). Call after jlSpriteBankLoad + a
|
|
// jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the
|
|
// cache and skip the JIT -- i.e. SHIP the small cross-platform .spr, compile
|
|
// once, and cache the (large) per-platform compiled bank on first run. `cels`
|
|
// are `count` sprites, all of one size (uncompiled ones are cached
|
|
// uncompiled, and nothing compiles them at load: jlSpriteCompile any whose
|
|
// jlSpriteCompiledSize is 0); `palette` (or NULL) is embedded. Returns false
|
|
// if the cels differ in size or the destination isn't writable (e.g. a
|
|
// write-protected disk), which is harmless -- the app just keeps compiling
|
|
// at startup.
|
|
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette);
|
|
|
|
// Wrap a tile-data blob in a jlSpriteT. The tile data must outlive the
|
|
// jlSpriteT; we do not copy it. Returns NULL if tileData is NULL,
|
|
// widthTiles or heightTiles is 0, or memory runs out. The codegen arena
|
|
// is not touched until jlSpriteCompile.
|
|
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles);
|
|
|
|
// Release a jlSpriteT and any codegen entries cached for it. The tile
|
|
// data the sprite was constructed from is NOT freed -- the caller
|
|
// owns that buffer.
|
|
void jlSpriteDestroy(jlSpriteT *sp);
|
|
|
|
// Compile the sprite's draw, save, and restore routines into the codegen
|
|
// arena. jlSpriteDraw never compiles on demand, so call this while
|
|
// loading (or load a pre-compiled bank). After it returns true the draw,
|
|
// save, and restore calls use the compiled routines that fit: a short
|
|
// arena keeps only the x-on-an-8-pixel-boundary draws on the Amiga, ST,
|
|
// and X68000, a routine over 16 KB is left out, and anything not compiled
|
|
// (and every clipped draw) interprets. Returns false if the arena is full
|
|
// even after compacting it (jlLastError says so), the sprite has no
|
|
// source tile data, the compiler's 64 KB working buffer cannot be
|
|
// allocated, no routine fits, or the port has no emitter
|
|
// (JL_SPRITE_INTERPRETED); the sprite still draws, interpreted.
|
|
//
|
|
// Idempotent: calling on a sprite that's already compiled is a
|
|
// no-op and returns true.
|
|
bool jlSpriteCompile(jlSpriteT *sp);
|
|
|
|
// Returns the number of bytes this sprite occupies in the codegen
|
|
// arena (sum of all compiled draw/save/restore shift variants).
|
|
// Returns 0 if the sprite has never been compiled or has no slot.
|
|
// Diagnostic only -- useful for tuning jlConfigT.codegenBytes against
|
|
// the actual per-cel cost of the current platform's emitter.
|
|
uint32_t jlSpriteCompiledSize(const jlSpriteT *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 jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
|
|
|
// TRUSTED draw (P7-1 family). Same trust contract as
|
|
// jlSpriteRestoreUnderTrusted: skips the per-call geometry validation
|
|
// for a caller that pre-compiles its sprites and guarantees the sprite
|
|
// is FULLY on-surface at (x,y); UB if violated (no clipping happens).
|
|
// Falls back to the fully-validated jlSpriteDraw for uncompiled
|
|
// sprites and on ports without a compiled fast path.
|
|
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
|
|
|
// How many _type elements a backup buffer needs for a widthTiles x
|
|
// heightTiles sprite at ANY x, on every port (rounded up), so that
|
|
//
|
|
// static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
|
|
//
|
|
// is big enough, and jlSpriteBackupLength(uint8_t, w, h) is the size in
|
|
// bytes (for jlAlloc, say). The worst case is the 16-pixel-group-aligned
|
|
// region covering the sprite (x rounded DOWN to a 16-px boundary, width
|
|
// UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give
|
|
// the buffer 2-byte alignment (a uint16_t array has it): on the Amiga,
|
|
// ST, and X68000 an odd address sends save and restore down a much
|
|
// slower path, and it crashes the trusted variants.
|
|
#define jlSpriteBackupLength(_type, _wT, _hT) \
|
|
((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type)))
|
|
|
|
// Capture the destination region a subsequent jlSpriteDraw at the same
|
|
// (x,y) would write to. backup->bytes must have room for
|
|
// jlSpriteBackupLength(uint8_t, widthTiles, heightTiles) bytes for
|
|
// fully in-bounds draws; for clipped draws only the visible bytes are
|
|
// stored. The captured region's exact geometry is reported in
|
|
// backup->x/y/width/height/sizeBytes: x is rounded down to even on every
|
|
// port, a save on the Amiga, ST, and X68000 takes the 16-px-aligned region
|
|
// containing the sprite (or its visible part), a compiled save on the IIgs
|
|
// only the rows holding an opaque pixel, and a clipped save the visible
|
|
// part.
|
|
// So the region a caller must not invalidate before restoring is the
|
|
// RECORDED region, not just the sprite rect. On the IIgs the buffer must
|
|
// not cross a 64 KB bank boundary. With backup->bytes NULL nothing is
|
|
// copied: the call only fills in the region a save would record.
|
|
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
|
|
|
|
// TRUSTED save-under (P7-1). Same trust contract as
|
|
// jlSpriteRestoreUnderTrusted (skips the per-call geometry validation
|
|
// for a caller that pre-compiles its sprites and keeps them on-screen;
|
|
// UB if violated). Falls back to the fully-validated jlSpriteSaveUnder
|
|
// for uncompiled sprites.
|
|
void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
|
|
|
|
// Repaint the destination region from a jlSpriteBackupT captured by a
|
|
// prior jlSpriteSaveUnder. The backup must not have been invalidated
|
|
// by other writes that overlapped its captured region.
|
|
void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
|
|
|
|
// TRUSTED fast-path restore (NATIVE-PERF Phase 7, P7-1). Identical
|
|
// effect to jlSpriteRestoreUnder, but SKIPS the geometry-validation
|
|
// chain (~11 range/alignment checks + the NULL and sprite-slot gates)
|
|
// that jlSpriteRestoreUnder runs every call. The caller MUST guarantee
|
|
// ALL of the following or behavior is UNDEFINED (out-of-bounds writes,
|
|
// crashes):
|
|
// * s and backup (and backup->sprite, backup->bytes) are non-NULL,
|
|
// * backup was produced by a jlSpriteSaveUnder that took the COMPILED
|
|
// path (a "trusted" backup: 16-px-aligned x/width on planar ports,
|
|
// even x/width on chunky, backup->height == the rows the compiled
|
|
// save covers -- the sprite's pixel height, or on the IIgs the rows
|
|
// holding an opaque pixel),
|
|
// * on the Amiga, ST, and X68000, backup->bytes is at least 2-byte
|
|
// aligned (the trusted save and save-and-draw need that too),
|
|
// * the recorded region is fully on the destination surface.
|
|
// It is the expert inner-loop entry for a game that pre-compiles its
|
|
// sprites (jlSpriteCompile) and already clamps positions on-screen --
|
|
// use jlSpriteRestoreUnder anywhere those invariants are not statically
|
|
// guaranteed. Anything the trusted fast path does not recognize (an
|
|
// uncompiled shift, a non-stage / non-16-row region on the 68k and x86
|
|
// ports)
|
|
// falls back to the fully-validated jlSpriteRestoreUnder, so a
|
|
// conservative caller degrades safely rather than corrupting -- but the
|
|
// NULL / on-surface guarantees above are still the caller's.
|
|
void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *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) per
|
|
// frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
|
|
//
|
|
// Identical semantics to:
|
|
// jlSpriteSaveUnder(s, sp, x, y, backup);
|
|
// jlSpriteDraw(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 jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
|
|
|
|
// TRUSTED save-and-draw (P7-1). The inner-loop companion to
|
|
// jlSpriteRestoreUnderTrusted -- same trust contract (skips the per-call
|
|
// geometry validation for a caller that pre-compiles its sprites and
|
|
// keeps them on-screen; UB if those guarantees are violated). The
|
|
// canonical pattern is a trusted save-and-draw this frame, a trusted
|
|
// restore next. Falls back to the fully-validated jlSpriteSaveAndDraw
|
|
// for anything the fast path does not recognize.
|
|
void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
|
|
|
|
// Snapshot an 8x8-aligned region of a jlSurfaceT into a new jlSpriteT.
|
|
// 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. Returns NULL if src is NULL, a dimension is 0, x or y is
|
|
// misaligned, the region is not wholly on the surface, or memory runs
|
|
// out.
|
|
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles);
|
|
|
|
// 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. jlSpriteT 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 jlSpriteCompact(void);
|
|
|
|
// Arena introspection. Used to gauge whether jlSpriteCompact is
|
|
// worth running, or whether the codegenBytes budget needs to grow.
|
|
// Free space is (Total - Used), but it may be fragmented across
|
|
// holes until jlSpriteCompact runs. Total is what jlInit was granted,
|
|
// which a machine short of memory can make less than codegenBytes; on the
|
|
// IIgs in blocks of at most 64 KB that compacting never merges (one
|
|
// sprite's code must fit in one). The interpreting ports have no arena:
|
|
// both are 0.
|
|
uint32_t jlSpriteCodegenBytesUsed(void);
|
|
uint32_t jlSpriteCodegenBytesTotal(void);
|
|
|
|
#endif
|