218 lines
11 KiB
C
218 lines
11 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// Tiles: 8x8 pixel blocks aligned on the 8-pixel grid of any surface.
|
|
//
|
|
// A "tile" in JoeyLib isn't a separate object -- it's just the 8x8
|
|
// region of a jlSurfaceT at block coordinates (bx, by), where bx is in
|
|
// [0, 39] and by is in [0, 24] (40x25 blocks per 320x200 surface).
|
|
// The tile API is a small set of operations that move 32-byte chunks
|
|
// between surfaces or fill them with a solid color.
|
|
//
|
|
// Why this shape: anything you can do with a regular surface --
|
|
// jlDrawPixel, jlDrawLine, jlFillRect, blits -- also works on tile-aligned
|
|
// regions, so authors can paint, edit, and procedurally generate tile
|
|
// content using the same primitives they use for everything else.
|
|
// "Fonts," "terrain tilesets," and "spritesheets" are just surfaces
|
|
// you treat as tile sources at draw time.
|
|
//
|
|
// Snap / paste use a small jlTileT value type so callers don't need a
|
|
// 32 KB scratch surface for save-under-style work.
|
|
//
|
|
// Block coords map to pixels by multiplying by 8 -- byte-aligned in
|
|
// 4bpp packed (8 px = 4 bytes per row), so all the operations are
|
|
// byte-shoveling memcpys with no shifting.
|
|
|
|
#ifndef JOEYLIB_TILE_H
|
|
#define JOEYLIB_TILE_H
|
|
|
|
#include "platform.h"
|
|
#include "surface.h"
|
|
#include "types.h"
|
|
|
|
// ----- Constants -----
|
|
|
|
#define JL_TILE_PIXELS_PER_SIDE 8
|
|
#define JL_TILE_BYTES_PER_ROW 4
|
|
#define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE)
|
|
#define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE)
|
|
#define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE)
|
|
|
|
// Sentinel for asciiMap entries that should not draw. jlDrawText
|
|
// advances the cursor past JL_TILE_NO_GLYPH chars without writing.
|
|
#define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu)
|
|
|
|
// ----- Types -----
|
|
|
|
// Stack-allocated 32-byte snapshot buffer for jlTileSnap / jlTilePaste.
|
|
// Target-native: plane-major on the Amiga and X68000, row by row with each
|
|
// row's four plane bytes together on the ST, chunky elsewhere.
|
|
typedef struct jlTileT {
|
|
uint8_t pixels[JL_TILE_BYTES];
|
|
} jlTileT;
|
|
|
|
// ----- API -----
|
|
|
|
// Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on
|
|
// dst, opaque. Out-of-range block coordinates on either side are
|
|
// silent no-ops; src and dst can be the same surface.
|
|
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
|
|
|
// Like jlTileCopy but pixels equal to transparentIndex are skipped --
|
|
// the destination pixel keeps its original value. Use this for fonts
|
|
// (transparentIndex = 0 leaves the page background showing through
|
|
// glyph backgrounds) and for any tileset where some pixels are meant
|
|
// to be see-through.
|
|
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex);
|
|
|
|
// Fill the 8x8 block at (bx, by) with a solid color. Equivalent to
|
|
// jlFillRect(s, bx*8, by*8, 8, 8, colorIndex) but skips the rect
|
|
// clipping math since tile coords are already known to be in range.
|
|
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
|
|
|
|
// Load up to maxTiles tiles from a baked .tbk file produced by
|
|
// tools/assetbake/assetbake.c. The file's per-target byte is checked
|
|
// against the build platform and the loader refuses mismatches.
|
|
//
|
|
// outValid (optional, NULL OK) gets one flag per slot in [0, maxTiles):
|
|
// true for each loaded tile, false for every other slot (all of them
|
|
// when the file is refused), so the caller can distinguish loaded
|
|
// tiles from any leftover slots in a fixed-size jlTileT[] array.
|
|
//
|
|
// outPalette (optional, NULL OK) receives the 16-entry $0RGB LE16
|
|
// palette if the file embeds one.
|
|
//
|
|
// Returns the number of tiles actually written. Returns 0 if the
|
|
// file is missing, the magic / target byte is wrong, or maxTiles is 0.
|
|
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, bool *outValid, uint16_t *outPalette);
|
|
|
|
// Stream a baked .tbk straight onto a surface's 8x8 block grid: tile i
|
|
// lands at block (i % JL_TILE_BLOCKS_PER_ROW, i / JL_TILE_BLOCKS_PER_ROW),
|
|
// the same row-major layout the bake tools author against. Use this
|
|
// instead of jlTileBankLoad + a jlTileT[] when the bank is too big for
|
|
// a caller-side buffer -- a full 1000-tile font sheet needs 32000
|
|
// bytes, which no IIgs example can afford from the C heap (32 KB to
|
|
// 48 KB in all) or hold in bank-0 BSS. One file pass with a
|
|
// small internal scratch; per-target/magic validation and outPalette
|
|
// behave exactly like jlTileBankLoad. Blocks beyond the file's tile
|
|
// count keep their prior surface content; tiles past the surface's 1000
|
|
// blocks are ignored. Returns tiles pasted.
|
|
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette);
|
|
|
|
// Capture the 8x8 block at (bx, by) into the caller's jlTileT. Used
|
|
// for save-under style work where allocating a scratch surface would
|
|
// be overkill.
|
|
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
|
|
|
|
// Paste a jlTileT back onto a surface at block (bx, by). Always
|
|
// opaque; use jlTileCopyMasked if you need transparency.
|
|
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
|
|
|
|
// Paste a jlTileT treating the source as a 1-bpp silhouette: any
|
|
// non-zero source pixel renders as fgColor on dst, zero pixels
|
|
// render as bgColor. Useful when the tile bitmap is a fixed glyph
|
|
// (e.g. a character cell) but the per-cell color comes from a
|
|
// separate map (e.g. C64-style color RAM). Both colors are masked
|
|
// to 4 bits. Opaque -- if you need transparent background, do
|
|
// jlTileFill(bg) first then mask separately, or follow up with a
|
|
// jlTileCopyMasked call.
|
|
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
|
|
|
|
// Paste an 8x8 ONE-BIT-PER-PIXEL glyph at block (bx, by): bits[row] is
|
|
// that row of the glyph, most significant bit leftmost; a set bit draws
|
|
// fgColor and a clear bit draws bgColor. Both colors are masked to 4
|
|
// bits. Opaque.
|
|
//
|
|
// This is the shape a character generator already has -- a font ROM, a
|
|
// C64 charset, anything 1bpp -- so nothing has to expand it into a
|
|
// jlTileT first. Prefer it over jlTilePasteMono for glyphs: a jlTileT
|
|
// is target-native (planar on the Amiga, ST, and X68000), whereas the
|
|
// silhouette a mono paste wants is chunky, so feeding one a tile that
|
|
// came from jlTileSnap or jlTileBankLoad is a format mismatch there that
|
|
// draws the wrong shape.
|
|
// Taking the bits directly makes the input unambiguous, and lets each
|
|
// port expand them once instead of twice.
|
|
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
|
|
|
|
|
|
// Draw a NUL-terminated ASCII string at block (bx, by) using glyphs
|
|
// pulled from fontSurface.
|
|
//
|
|
// asciiMap is a 256-entry table mapping ASCII code to glyph location
|
|
// on fontSurface, encoded as a packed uint16_t: low byte = source
|
|
// blockX, high byte = source blockY. asciiMap[c] == JL_TILE_NO_GLYPH,
|
|
// or an entry off the 40x25 grid, causes that character to be skipped
|
|
// (cursor advances, nothing drawn). Glyph color 0 is treated as
|
|
// transparent so the underlying surface shows through glyph backgrounds.
|
|
//
|
|
// The cursor wraps to the next row at the right edge and truncates
|
|
// at the bottom edge. A start off the grid uses up characters without
|
|
// drawing them: a start column past 39 drops the first character and
|
|
// carries on at the start of the next row; a start row past 24 draws
|
|
// nothing.
|
|
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str);
|
|
|
|
// Map entry value that leaves a cell untouched (nothing pasted, the
|
|
// destination shows through). Mirrors JL_TILE_NO_GLYPH's role in
|
|
// jlDrawText.
|
|
#define JL_TILE_MAP_SKIP ((uint8_t)0xFFu)
|
|
|
|
// Paste a wTiles x hTiles rectangle of tiles in ONE call: the
|
|
// validation, dispatch, and dirty union are paid once for the whole
|
|
// map instead of per tile (the jlDrawText batching pattern
|
|
// generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map`
|
|
// holds wTiles * hTiles tileset indices in row-major order
|
|
// (map[row * wTiles + col]). An index of JL_TILE_MAP_SKIP leaves that
|
|
// cell untouched; any other out-of-tileset index is the CALLER'S
|
|
// contract to avoid (the batch does not know the tileset length and
|
|
// does not range-check it). The whole rectangle must fit on the
|
|
// surface -- a map whose edge crosses JL_TILE_BLOCKS_PER_ROW/COL is
|
|
// rejected as a no-op, matching the per-tile ops' silent-reject
|
|
// convention. wTiles or hTiles of 0 is a no-op.
|
|
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map);
|
|
|
|
|
|
// ----- Tile layers (scrolling) -----
|
|
//
|
|
// A tile layer is a map of tile indices, up to JL_TILE_LAYER_MAX_SIDE
|
|
// tiles a side, seen through a band of a surface: whole tile rows
|
|
// [viewBy, viewBy + viewHeight), the full width. jlTileLayerDraw fills
|
|
// the band with the map as seen from a pixel origin, to the pixel both
|
|
// ways, so moving the origin a little each frame scrolls it. The map
|
|
// wraps at its edges (an origin past them, or a map smaller than the
|
|
// view, repeats it). An index of JL_TILE_MAP_SKIP draws colour 0.
|
|
//
|
|
// The layer keeps the tiles around the view drawn, so a draw only draws
|
|
// the edge tiles a move uncovered (a move of a whole view or more draws
|
|
// them all) before copying the band. A draw covers the band and never
|
|
// reads what was there: draw the layer, then the sprites (no save-under
|
|
// needed), then present.
|
|
//
|
|
// The layer keeps the tileset and the map you give it rather than
|
|
// copying them, so both must outlive it. Change a cell with
|
|
// jlTileLayerSetTile, not by writing the map: the layer would not see
|
|
// the write until the cell scrolled out of the view and back.
|
|
typedef struct jlTileLayerT jlTileLayerT;
|
|
|
|
#define JL_TILE_LAYER_MAX_SIDE 4096u
|
|
|
|
// Make a layer over `map` (mapWidth x mapHeight tile indices into `tiles`,
|
|
// row-major: map[row * mapWidth + col]) shown in tile rows [viewBy,
|
|
// viewBy + viewHeight). Returns NULL if an argument is NULL or out of
|
|
// range, or memory runs out. Nothing is drawn until jlTileLayerDraw.
|
|
jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map, uint16_t mapWidth, uint16_t mapHeight, uint8_t viewBy, uint8_t viewHeight);
|
|
|
|
// Free a layer (not its tileset or map). NULL is a no-op.
|
|
void jlTileLayerDestroy(jlTileLayerT *layer);
|
|
|
|
// Fill the layer's band of dst with the map seen from pixel (x, y): the
|
|
// band's top-left pixel shows map pixel (x, y), taken modulo the map's
|
|
// size in pixels.
|
|
void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer, uint16_t x, uint16_t y);
|
|
|
|
// Set map cell (tx, ty) to `tile`; the next draw shows it. Out-of-range
|
|
// cells are ignored.
|
|
void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty, uint8_t tile);
|
|
|
|
#endif
|