// 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 #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]); }