403 lines
16 KiB
C
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]);
|
|
}
|