joeylib2/src/core/tile.c

249 lines
9.1 KiB
C

// Tiles as 8x8 surface regions. The whole API is byte-shoveling
// between jlSurfaceT regions (or between a jlSurfaceT region and a
// stack jlTileT buffer); no separate tileset container, no allocator.
//
// Block coords (bx, by) map to pixel (bx*8, by*8). At 4bpp packed
// each tile row is 4 bytes wide, so byte-aligned row copies are the
// inner loop for everything except the masked / transparent variant,
// which has to read-modify-write each byte to preserve destination
// pixels under transparent (color-0 by convention) source nibbles.
#include <stddef.h>
#include "joey/tile.h"
#include "port.h"
#include "surfaceInternal.h"
// The chunky tile inner loops live in src/generic/genericTile.c (the
// jlpGenericTile* ops); this TU is validation, dispatch, and dirty
// marking only -- it never touches pixel bytes.
// Block -> pixel coordinate for the dirty-rect bookkeeping below.
// Spelled as a shift because ORCA-C does not strength-reduce the *8
// into one -- it emits the ~150-cycle software multiply helper, the
// same trap SURFACE_ROW_OFFSET in surfaceInternal.h documents
// (PERF-AUDIT #48). The shift count 3 assumes TILE_PIXELS_PER_SIDE is
// 8; the guard keeps the two in sync.
#if TILE_PIXELS_PER_SIDE != 8
#error "TILE_BLOCK_TO_PIXEL assumes TILE_PIXELS_PER_SIDE == 8"
#endif
#define TILE_BLOCK_TO_PIXEL(_b) ((uint16_t)((uint16_t)(_b) << 3))
// Prototype (helper below is the only static in this TU).
static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds);
// jlDrawText's per-glyph dirty-union tracking, hoisted out of the
// glyph loop into a noinline helper: keeping five live union locals
// inside the loop overflows the llvm-mos w65816 register allocator
// ("ran out of registers" hard error). bounds[] = {minCx, maxCx,
// minCy, maxCy, drewAny}. noinline is portable gcc/clang syntax.
static void __attribute__((noinline)) drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds) {
if (bounds[4] == 0u) {
bounds[4] = 1u;
bounds[0] = cx;
bounds[1] = cx;
bounds[2] = cy;
} else {
if (cx < bounds[0]) {
bounds[0] = cx;
}
if (cx > bounds[1]) {
bounds[1] = cx;
}
}
// cy never decreases, so the last drawn glyph's row is always the
// union's bottom edge.
bounds[3] = cy;
}
// ----- Public API (alphabetical) -----
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str) {
uint16_t entry;
uint8_t cx;
uint8_t cy;
uint8_t ch;
uint8_t srcBx;
uint8_t srcBy;
uint8_t bounds[5];
if (dst == NULL || fontSurface == NULL || asciiMap == NULL || str == NULL) {
return;
}
cx = bx;
cy = by;
// Entry sanitation (PERF-AUDIT #22 verifier correction): the main
// loop below hands cx/cy straight to jlpTileCopyMasked, which does
// no bounds checking, so an out-of-range start must be disposed of
// here -- the wrap logic only guarantees in-range cx/cy after the
// first wrap. The historical contract is preserved bit-for-bit
// (the chk:drawText-offgrid golden covers it): characters landing
// on an out-of-range position are consumed without drawing while
// the cursor advances and wraps exactly as for drawn characters.
// For the documented bx in [0,39] / by in [0,24] inputs this whole
// loop costs two compares.
while (cx >= TILE_BLOCKS_PER_ROW || cy >= TILE_BLOCKS_PER_COL) {
if (*str == '\0') {
return;
}
str++;
cx++;
if (cx >= TILE_BLOCKS_PER_ROW) {
cx = 0;
cy++;
if (cy >= TILE_BLOCKS_PER_COL) {
return;
}
}
}
bounds[4] = 0u;
while (*str != '\0') {
ch = (uint8_t)*str++;
entry = asciiMap[ch];
if (entry != TILE_NO_GLYPH) {
srcBx = (uint8_t)(entry & 0x00FFu);
srcBy = (uint8_t)((entry >> 8) & 0x00FFu);
// Only the asciiMap-supplied source block still needs
// validation per glyph: cx/cy are in range by the entry
// sanitation + wrap invariant and the NULL checks ran once
// above, so the public jlTileCopyMasked wrapper (which
// would re-check all of it and issue a dirty mark per
// glyph) is skipped (PERF-AUDIT #22).
if (srcBx < TILE_BLOCKS_PER_ROW && srcBy < TILE_BLOCKS_PER_COL) {
jlpTileCopyMasked(dst, cx, cy, fontSurface, srcBx, srcBy, 0u);
drawTextUnion(cx, cy, bounds);
}
}
cx++;
if (cx >= TILE_BLOCKS_PER_ROW) {
cx = 0;
cy++;
if (cy >= TILE_BLOCKS_PER_COL) {
break;
}
}
}
// One union dirty mark for the whole string instead of an 8-row
// mark per glyph (PERF-AUDIT #22). Over-approximates when the
// string wraps rows, which is safe: dirty state only widens what
// present uploads.
if (bounds[4] != 0u) {
surfaceMarkDirtyRect(dst, (int16_t)TILE_BLOCK_TO_PIXEL(bounds[0]), (int16_t)TILE_BLOCK_TO_PIXEL(bounds[2]), TILE_BLOCK_TO_PIXEL(bounds[1] - bounds[0] + 1u), TILE_BLOCK_TO_PIXEL(bounds[3] - bounds[2] + 1u));
}
}
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint16_t dstPixelX;
uint16_t dstPixelY;
if (dst == NULL || src == NULL) {
return;
}
if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
// Single op: machine asm/planar override or the generic chunky default.
jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
uint16_t dstPixelX;
uint16_t dstPixelY;
if (dst == NULL || src == NULL) {
return;
}
if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint16_t pixelX;
uint16_t pixelY;
if (s == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single compile-time-selected op: machine asm/planar override or the
// generic chunky default.
jlpTileFill(s, bx, by, colorIndex);
surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
uint16_t pixelX;
uint16_t pixelY;
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single op: machine asm/planar override or the generic chunky default.
jlpTilePaste(dst, bx, by, &in->pixels[0]);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
uint16_t pixelX;
uint16_t pixelY;
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single compile-time op: planar ports colorize in their own tile format
// (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
// Replaces the old runtime pixels==NULL planar/chunky branch.
jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out) {
if (src == NULL || out == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
// Read-only single op: machine asm/planar override or the generic chunky
// default. No dirty mark.
jlpTileSnap(src, bx, by, &out->pixels[0]);
}