joeylib2/src/core/tile.c
2026-09-29 19:31:34 -05:00

403 lines
16 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))
// Whole-tile dirty mark (NATIVE-PERF Phase 2 R1). Every whole-tile op
// marks the compile-time-constant rect h = TILE_PIXELS_PER_SIDE with
// word band exactly [bx*2, bx*2 + 1]: 8 px per tile over 4 px per
// dirty word means SURFACE_WORD_INDEX(bx*8) == bx*2 and
// SURFACE_WORD_INDEX(bx*8 + 7) == bx*2 + 1. Deriving the band from bx
// and widening the 8 rows inline replaces the out-of-line
// surfaceMarkDirtyRows call (~800 cycles measured steady-state on the
// 68000 ports). Callers own the `s == gStage` gate (non-stage
// surfaces carry no dirty state), exactly like surfaceMarkDirtyRect.
// The IIgs keeps its single-JSL asm marker -- already the cheap path
// there.
//
// Compile-time proof of the band identity (breaks the build if either
// the tile side or the 4-px-per-word grain ever changes):
typedef char tileMarkBandProof[(SURFACE_WORD_INDEX(TILE_PIXELS_PER_SIDE) == 2) ? 1 : -1];
// jlTileMapPaste indexes its tileset as base + (idx << 5); pin the
// struct size so that shift can never drift from sizeof(jlTileT).
typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1];
#ifdef JOEYLIB_PLATFORM_IIGS
#define tileMarkDirty(_pixelY, _bx) iigsMarkDirtyRowsInner((uint16_t)(_pixelY), (uint16_t)((_pixelY) + TILE_PIXELS_PER_SIDE), (uint16_t)((uint16_t)(_bx) << 1), (uint16_t)(((uint16_t)(_bx) << 1) + 1u))
// Fill/copy/paste FUSE the mark into their stage-arm asm entries on
// the IIgs (port.h jlpTile* macros -> iigsTile*Mark, NATIVE-PERF
// Phase 2 Step D), so their C-side mark compiles out here.
// copyMasked/pasteMono still mark separately via tileMarkDirty.
#define tileMarkDirtyFused(_pixelY, _bx) ((void)0)
// pasteGlyph's IIgs stage arm fuses the mark the same way (iigsTilePasteGlyphMark).
#if defined(JL_HAS_TILE_PASTE_GLYPH)
#define tileMarkDirtyGlyphFused(_pixelY, _bx) ((void)0)
#else
#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx))
#endif
#else
// On non-IIgs ports nothing fuses the mark, so the "fused" spelling
// is the plain helper.
#define tileMarkDirtyFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx))
#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx))
// Explicit 8-row unroll: gcc-6.5 -O2 keeps the equivalent loop rolled
// (8 iterations of compare + spill-reload, ~670 cycles); the d16(An)
// unroll is ~450. Same widen semantics, row by row, as the loop.
#define TILE_MARK_ROW(_i) do { \
if (minWord < minPtr[_i]) { \
minPtr[_i] = minWord; \
} \
if (maxWord > maxPtr[_i]) { \
maxPtr[_i] = maxWord; \
} \
} while (0)
static void tileMarkDirty(uint16_t pixelY, uint8_t bx) {
uint8_t *minPtr;
uint8_t *maxPtr;
uint8_t minWord;
uint8_t maxWord;
minWord = (uint8_t)(bx << 1);
maxWord = (uint8_t)(minWord + 1u);
// A whole-tile mark is 8 rows starting at by*8, so it covers
// EXACTLY one dirty group -- one store, no range loop. (This arm is
// non-IIgs, where the map is not maintained; the macro is a no-op.)
stageMarkDirtyGroups(pixelY, pixelY + TILE_PIXELS_PER_SIDE);
minPtr = &gStageMinWord[pixelY];
maxPtr = &gStageMaxWord[pixelY];
TILE_MARK_ROW(0);
TILE_MARK_ROW(1);
TILE_MARK_ROW(2);
TILE_MARK_ROW(3);
TILE_MARK_ROW(4);
TILE_MARK_ROW(5);
TILE_MARK_ROW(6);
TILE_MARK_ROW(7);
}
#endif
// Prototype (drawTextUnion is the only other 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) {
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;
}
// Single op: machine asm/planar override or the generic chunky default.
jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
if (dst == gStage) {
tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
}
}
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
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;
}
jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
if (dst == gStage) {
tileMarkDirty(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
}
}
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
if (s == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
// Single compile-time-selected op: machine asm/planar override or the
// generic chunky default.
jlpTileFill(s, bx, by, colorIndex);
if (s == gStage) {
tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx);
}
}
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) {
uint8_t tx;
uint8_t ty;
uint8_t idx;
const uint8_t *row;
if (dst == NULL || tiles == NULL || map == NULL) {
return;
}
if (wTiles == 0u || hTiles == 0u) {
return;
}
if ((uint16_t)bx + wTiles > TILE_BLOCKS_PER_ROW || (uint16_t)by + hTiles > TILE_BLOCKS_PER_COL) {
return;
}
// One validation above, one dirty union below: the per-cell loop
// pays only the jlp dispatch (inline on the 68k ports since Phase
// 2 R4a; the fused stage entry on IIgs). Tileset indexing is a
// 16-bit shift folded into the far base ONCE per cell -- the
// natural tiles[idx] spelling made the 65816 do a 32-bit indexed
// multiply per cell, which alone erased the batch win there.
row = map;
#ifdef JOEYLIB_PLATFORM_IIGS
// The stage test is hoisted OUT of the loop on the IIgs: the
// jlpTilePaste macro's per-cell (dst == gStage) ternary
// materializes a boolean (~65 cycles/cell) that a 10x5 map pays
// 50 times. The fused stage entry marks each pasted tile itself,
// so the stage arm needs no union; non-stage surfaces carry no
// dirty state.
if (dst == gStage) {
// ONE JSL for the whole map (NATIVE-PERF Phase 4): the asm
// walker amortizes the per-tile JSL + arg staging + derive +
// 8-row mark across the rectangle and issues one band union.
iigsTileMapPasteMark(bx, by, wTiles, hTiles, (const uint8_t *)tiles, map);
return;
}
for (ty = 0; ty < hTiles; ty++) {
for (tx = 0; tx < wTiles; tx++) {
idx = row[tx];
if (idx != TILE_MAP_SKIP) {
iigsTilePasteInner(&dst->pixels[SURFACE_ROW_OFFSET((uint16_t)(by + ty) << 3) + ((uint16_t)(bx + tx) << 2)], (const uint8_t *)tiles + ((uint16_t)idx << 5));
}
}
row += wTiles;
}
#elif defined(jlpTileMapPaste)
// Whole-map engine (NATIVE-PERF Phase 4 ST / Phase 6 W3 seam):
// port.h aliases jlpTileMapPaste to the port's engine (ST
// stTileMapPaste, DOS/DVX x86TileMapPaste), which hoists the setup --
// portData/pixels check, row-base derive, group or dword walk --
// out of the per-cell loop the generic arm below pays. One arm,
// one mark stanza, however many ports bring engines.
(void)tx;
(void)ty;
(void)idx;
(void)row;
jlpTileMapPaste(dst, bx, by, wTiles, hTiles, tiles, map);
// One rectangle-union mark for the whole map. Over-marks
// TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
// pixels unchanged).
if (dst == gStage) {
surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
}
#else
for (ty = 0; ty < hTiles; ty++) {
for (tx = 0; tx < wTiles; tx++) {
idx = row[tx];
if (idx != TILE_MAP_SKIP) {
jlpTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), (const uint8_t *)tiles + ((uint16_t)idx << 5));
}
}
row += wTiles;
}
// One rectangle-union mark for the whole map. Over-marks
// TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
// pixels unchanged).
if (dst == gStage) {
surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
}
#endif
}
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
// Single op: machine asm/planar override or the generic chunky default.
jlpTilePaste(dst, bx, by, &in->pixels[0]);
if (dst == gStage) {
tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx);
}
}
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) {
if (dst == NULL || bits == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
jlpTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor);
if (dst == gStage) {
tileMarkDirtyGlyphFused(TILE_BLOCK_TO_PIXEL(by), bx);
}
}
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
// 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);
if (dst == gStage) {
tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx);
}
}
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]);
}