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