// Drawing primitives on chunky 4bpp packed surfaces. // // Byte layout: two pixels per byte, high nibble is the LEFT pixel. // Clipping is handled at the C API boundary so ASM inner loops (future) // can run with branch-free, pre-validated parameters. #include #include #include "joey/draw.h" #include "joey/debug.h" #include "hal.h" #include "surfaceInternal.h" // ----- Constants ----- // Flood-fill seed stack: each entry is (x, y) = 4 bytes, so 512 slots // = 2 KB. For 320x200 surfaces with reasonable region sizes this is // well above the worst-case scanline-fill seed depth (typically <50). // On overflow the fill silently truncates rather than crashing. #define FLOOD_STACK_SIZE 512 // Sentinel extents for an empty accumulated dirty bounding box. min* seed // above any valid coordinate and max* below any valid coordinate so the // first plotted point sets every edge, and a box that never receives an // on-surface point collapses to a no-op after clamping (min > max). #define DIRTY_BOX_MIN_INIT ((int16_t)0x7FFF) #define DIRTY_BOX_MAX_INIT ((int16_t)0x8000) // ----- Prototypes ----- static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual); static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex); // ----- Internal helpers (alphabetical) ----- static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { uint8_t nibble = colorIndex & 0x0F; uint8_t doubled = (uint8_t)((nibble << 4) | nibble); int16_t row; uint16_t pxStart; uint16_t pxEnd; uint16_t midBytes; uint8_t *line; // Planar ports have NULL s->pixels; the planar dual-write hook // handles the actual plane fill. Guarding here keeps the chunky // fallback from dereferencing NULL when a port has no chunky store. if (s->pixels == NULL) { return; } /* px* and midBytes are uint16_t (clipped values are non-negative) * so `>>1` lowers to a single LSR instead of a signed-shift * helper. Same with `<<1` for midBytes. */ for (row = 0; row < h; row++) { line = &s->pixels[SURFACE_ROW_OFFSET(y + row)]; pxStart = (uint16_t)x; pxEnd = (uint16_t)(x + w); if (pxStart & 1u) { line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0xF0) | nibble); pxStart++; } midBytes = (uint16_t)((pxEnd - pxStart) >> 1); if (midBytes > 0u) { memset(&line[pxStart >> 1], doubled, (size_t)midBytes); pxStart = (uint16_t)(pxStart + (midBytes << 1)); } if (pxStart < pxEnd) { line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0x0F) | (nibble << 4)); } } } // Fill an axis-aligned span that the CALLER has already proven to be // fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <= // SURFACE_HEIGHT, w > 0, h > 0). Identical tail to jlFillRect but with // the 32-bit clip elided -- on an on-surface rect that clip is a // no-op, so callers that already hold the on-surface guarantee (rect // outlines, circle scanlines, axis-aligned lines) avoid re-deriving // it per span. Pixel output and dirty-mark word bands are bit-for-bit // identical to routing the same span through jlFillRect. static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) { fillRectClipped(s, x, y, w, h, colorIndex); } halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex); surfaceMarkDirtyRect(s, x, y, w, h); } // Smith's scanline flood fill. Implements both the unbounded and the // boundary-stopped variants in one pass: the matching predicate is // (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill, // matchColor is the original seed color) or (pixel != matchColor) // when matchEqual is false (jlFloodFillBounded, matchColor is the // boundary that stops the fill). // // Algorithm: // 1. Push seed (x, y) on stack. // 2. Pop a seed; skip if its pixel no longer matches (already // filled by an earlier span overlap). // 3. Scan left and right from the seed to find the longest run of // matching pixels containing it -- this is the current span. // 4. Fill the span with newColor. // 5. Walk the row above and the row below, scanning the columns // that overlap the just-filled span; for each contiguous run of // matching pixels, push the rightmost x of that run as a new // seed (so popping that seed next will scan the same run). // 6. Repeat until the stack drains. // // Stack overflow truncates the fill rather than crashing; for vector // art (Sierra-style picture playback) the input is well-behaved and // 512 entries is plenty. static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual) { static int16_t stackX[FLOOD_STACK_SIZE]; static int16_t stackY[FLOOD_STACK_SIZE]; static uint8_t floodMarkBuf[SURFACE_WIDTH]; int16_t sp; int16_t x; int16_t y; int16_t leftX; int16_t rightX; uint8_t *row; uint8_t pix; bool pixMatch; uint8_t newNibble; newNibble = (uint8_t)(newColor & 0x0F); matchColor = (uint8_t)(matchColor & 0x0F); sp = 0; stackX[sp] = startX; stackY[sp] = startY; sp++; while (sp > 0) { sp--; x = stackX[sp]; y = stackY[sp]; if (y < 0 || y >= SURFACE_HEIGHT || x < 0 || x >= SURFACE_WIDTH) { continue; } /* Phase 9: planar ports have NULL s->pixels and the asm fast * paths take a chunky-row pointer. Skip them on planar; the C * fallback below uses halSamplePixel which works on both * storage layouts. */ if (s->pixels != NULL) { // Highest-tier asm fast path: seed-test + walk-left + walk-right // + 1-row fill + scan-above + scan-below + push, all in one // cross-segment call. The asm caches row addr / match decoder // across every sub-operation. C just pops and dispatches; this // path completes the entire per-seed work and computes the row // address itself, so we don't pay y*160 in C unless we fall back. bool seedMatched; if (halFastFloodWalkAndScans(s->pixels, x, y, matchColor, newNibble, matchEqual, stackX, stackY, &sp, FLOOD_STACK_SIZE, &seedMatched, &leftX, &rightX)) { // The asm filled the span internally but does not mark // dirty -- that is the C wrapper's job. Mark the just- // filled run so jlStagePresent blits it. if (seedMatched) { surfaceMarkDirtyRect(s, leftX, y, (int16_t)(rightX - leftX + 1), 1); } continue; } } /* Fallback path: compute row only if chunky; halFastFloodWalk * needs it but isn't implemented on Amiga. */ row = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(y)] : NULL; // Tier-2 asm fast path: combined seed test + walk-left + // walk-right in one cross-segment call. Falls back to the // pure-C walks below on ports without an asm implementation. { bool seedMatched; if (row != NULL && halFastFloodWalk(row, x, matchColor, newNibble, matchEqual, &seedMatched, &leftX, &rightX)) { if (!seedMatched) { continue; } } else if (halFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual, &seedMatched, &leftX, &rightX)) { if (!seedMatched) { continue; } } else { pix = halSamplePixel(s, x, y); pixMatch = (pix == matchColor); if (matchEqual) { if (!pixMatch) { continue; } } else { if (pixMatch || pix == newNibble) { continue; } } // Walk left to find the start of the matching run. leftX = x; while (leftX > 0) { pix = halSamplePixel(s, (int16_t)(leftX - 1), y); pixMatch = (pix == matchColor); if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { break; } leftX--; } // Walk right to find the end. rightX = x; while (rightX < SURFACE_WIDTH - 1) { pix = halSamplePixel(s, (int16_t)(rightX + 1), y); pixMatch = (pix == matchColor); if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { break; } rightX++; } } } // Fill the span. walk-out already guaranteed leftX/rightX are in // [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same // for y, so the span is provably on-surface -- route it through // the shared on-surface tail (chunky fill + planar dual-write + // dirty mark) rather than re-clipping through jlFillRect. The // planar dual-write here is load-bearing: without it // PLANAR_PRESENT builds (and, post-Phase-9, every build) display // flood-filled regions as the unfilled background. { int16_t spanW = (int16_t)(rightX - leftX + 1); fillRectOnSurface(s, leftX, y, spanW, 1, newNibble); } // Scan rows above and below for run boundaries. The hot // per-pixel match check goes through halFastFloodScanRow on // ports that have it (IIgs); fills markBuf[] with 1/0 per // pixel so the run-edge walk below is array-only -- no // function call, no nibble extract. { int16_t i; int16_t spanLen; uint8_t *scanRow; int16_t scanY; int16_t side; bool curHit; bool prevHit; spanLen = (int16_t)(rightX - leftX + 1); for (side = 0; side < 2; side++) { if (side == 0) { if (y <= 0) { continue; } scanY = (int16_t)(y - 1); } else { if (y >= SURFACE_HEIGHT - 1) { continue; } scanY = (int16_t)(y + 1); } scanRow = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(scanY)] : NULL; // Prefer the combined scan+push asm path (one call per // scan, no markBuf and no per-pixel C edge walk). Skip // the asm tiers if we don't have a chunky row pointer // (Phase 9 planar ports). if (scanRow == NULL || !halFastFloodScanAndPush(scanRow, leftX, rightX, matchColor, newNibble, matchEqual, scanY, stackX, stackY, &sp, FLOOD_STACK_SIZE)) { if ((scanRow == NULL || !halFastFloodScanRow(scanRow, leftX, rightX, matchColor, newNibble, matchEqual, floodMarkBuf)) && !halFloodScanRowPlanes(s, leftX, rightX, scanY, matchColor, newNibble, matchEqual, floodMarkBuf)) { // C fallback: fill markBuf the slow way. for (i = 0; i < spanLen; i++) { pix = halSamplePixel(s, (int16_t)(leftX + i), scanY); pixMatch = (pix == matchColor); floodMarkBuf[i] = (uint8_t)(matchEqual ? (pixMatch ? 1 : 0) : ((!pixMatch && pix != newNibble) ? 1 : 0)); } } // Walk markBuf for run-edge transitions. prevHit = false; for (i = 0; i < spanLen; i++) { curHit = floodMarkBuf[i] != 0; if (!curHit && prevHit) { if (sp < FLOOD_STACK_SIZE) { stackX[sp] = (int16_t)(leftX + i - 1); stackY[sp] = scanY; sp++; } } prevHit = curHit; } if (prevHit) { if (sp < FLOOD_STACK_SIZE) { stackX[sp] = rightX; stackY[sp] = scanY; sp++; } } } } } } } // Plot a single clipped pixel WITHOUT marking the dirty rect. Shared by // the public jlDrawPixel (which marks per call afterward) and the // Bresenham line/circle fallbacks (which accumulate one bounding box and // mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds // check, halFastDrawPixel dispatch, and chunky nibble RMW fallback. static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { uint8_t *byte; uint8_t nibble; if (s == NULL) { return; } if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { return; } if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) { /* Cast to uint16_t before shift -- already validated x >= 0, * so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */ byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)]; nibble = colorIndex & 0x0F; if (x & 1) { *byte = (uint8_t)((*byte & 0xF0) | nibble); } else { *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); } } } // ----- Public API (alphabetical) ----- void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { int16_t x; int16_t y; int16_t err; int16_t ir; int16_t minX; int16_t minY; int16_t maxX; int16_t maxY; if (s == NULL) { return; } if (r == 0) { jlDrawPixel(s, cx, cy, colorIndex); return; } // Fast path: when the bounding circle is fully on-surface we can // hand off to the port asm (no per-pixel bounds check needed in // the inner loop) and mark the bounding box dirty once. A circle // with r >= SURFACE_HEIGHT can never fit on-surface, and casting // such an r to int16_t can wrap negative (r > 32767), so reject it // here before the cast feeds the fast path / dirty-rect extents. if (r < SURFACE_HEIGHT) { ir = (int16_t)r; if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH && cy - ir >= 0 && cy + ir < SURFACE_HEIGHT && halFastDrawCircle(s, cx, cy, r, colorIndex)) { surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); return; } } // Bresenham midpoint: maintain (x, y) on the perimeter, eight- // octant symmetry plots all 8 reflections each iteration. Plots // through plotPixelNoMark so off-surface pixels clip individually // (same per-pixel clip as jlDrawPixel) without paying the // cross-segment dirty mark per pixel; one accumulated bounding box // is marked after the loop instead. minX = DIRTY_BOX_MIN_INIT; minY = DIRTY_BOX_MIN_INIT; maxX = DIRTY_BOX_MAX_INIT; maxY = DIRTY_BOX_MAX_INIT; x = (int16_t)r; y = 0; err = (int16_t)(1 - x); while (x >= y) { // The 8 octants span columns cx +/- x and cx +/- y, and rows // cy +/- y and cy +/- x. Fold all reflections into the box in // one shot, then clamp once after the loop. if (cx + x > maxX) { maxX = (int16_t)(cx + x); } if (cx - x < minX) { minX = (int16_t)(cx - x); } if (cx + y > maxX) { maxX = (int16_t)(cx + y); } if (cx - y < minX) { minX = (int16_t)(cx - y); } if (cy + x > maxY) { maxY = (int16_t)(cy + x); } if (cy - x < minY) { minY = (int16_t)(cy - x); } if (cy + y > maxY) { maxY = (int16_t)(cy + y); } if (cy - y < minY) { minY = (int16_t)(cy - y); } plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex); plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex); plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex); plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex); plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex); plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex); plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex); plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex); y++; /* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits * a multiply helper -- two ADDs are unconditionally cheaper. */ if (err <= 0) { err = (int16_t)(err + y + y + 1); } else { x--; err = (int16_t)(err + y + y - x - x + 1); } } // Clamp the accumulated box to the surface and mark it once. If the // circle was entirely off-surface no pixel was plotted, so minX/minY // stay above maxX/maxY (after clamping) and nothing is marked. if (minX < 0) { minX = 0; } if (minY < 0) { minY = 0; } if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; } if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; } if (minX <= maxX && minY <= maxY) { surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1)); } } void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { int16_t dx; int16_t dy; int16_t sx; int16_t sy; int16_t err; int16_t e2; int16_t tmp; int16_t minX; int16_t minY; int16_t maxX; int16_t maxY; int32_t span; if (s == NULL) { return; } // Horizontal and vertical fast paths use jlFillRect; the general // case Bresenham plots per-pixel through plotPixelNoMark so per-pixel // off-surface clipping just works. if (y0 == y1) { if (x0 > x1) { tmp = x0; x0 = x1; x1 = tmp; } // Compute the span in 32-bit so far-apart off-surface endpoints // (e.g. x0=-20000, x1=20000) do not overflow the 16-bit int the // subtraction would otherwise promote to on a 16-bit-int target; // clamp to the surface width so the downstream uint16_t narrowing // is safe. span = (int32_t)x1 - (int32_t)x0 + 1; if (span > SURFACE_WIDTH) { span = SURFACE_WIDTH; } // On-surface (the common case) skips jlFillRect's 32-bit clip: // span is already <= SURFACE_WIDTH and x0 <= x1, so the row is // fully on-surface exactly when its left edge and y are. Off- // surface still routes through jlFillRect for correct clipping. if (x0 >= 0 && y0 >= 0 && y0 < SURFACE_HEIGHT && (int32_t)x0 + span <= SURFACE_WIDTH) { fillRectOnSurface(s, x0, y0, (int16_t)span, 1, colorIndex); } else { jlFillRect(s, x0, y0, (uint16_t)span, 1, colorIndex); } return; } if (x0 == x1) { if (y0 > y1) { tmp = y0; y0 = y1; y1 = tmp; } span = (int32_t)y1 - (int32_t)y0 + 1; if (span > SURFACE_HEIGHT) { span = SURFACE_HEIGHT; } if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && (int32_t)y0 + span <= SURFACE_HEIGHT) { fillRectOnSurface(s, x0, y0, 1, (int16_t)span, colorIndex); } else { jlFillRect(s, x0, y0, 1, (uint16_t)span, colorIndex); } return; } // Diagonal: if both endpoints are on-surface, the inner Bresenham // can run without per-pixel bound checks. Hand off to the port // fast path; bounding-box dirty marking happens here in C either // way. if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT && halFastDrawLine(s, x0, y0, x1, y1, colorIndex)) { int16_t bbx = (x0 < x1) ? x0 : x1; int16_t bby = (y0 < y1) ? y0 : y1; int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1); int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1); surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh); return; } // Diagonal fallback: plot through plotPixelNoMark so each pixel still // clips off-surface individually (matching jlDrawPixel), but skip the // per-pixel cross-segment dirty mark. Accumulate one bounding box over // the plotted points and mark it once after the loop. minX = DIRTY_BOX_MIN_INIT; minY = DIRTY_BOX_MIN_INIT; maxX = DIRTY_BOX_MAX_INIT; maxY = DIRTY_BOX_MAX_INIT; dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1)); dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1))); sx = (int16_t)((x0 < x1) ? 1 : -1); sy = (int16_t)((y0 < y1) ? 1 : -1); err = (int16_t)(dx + dy); while (1) { if (x0 < minX) { minX = x0; } if (x0 > maxX) { maxX = x0; } if (y0 < minY) { minY = y0; } if (y0 > maxY) { maxY = y0; } plotPixelNoMark(s, x0, y0, colorIndex); if (x0 == x1 && y0 == y1) { break; } e2 = (int16_t)(2 * err); if (e2 >= dy) { err = (int16_t)(err + dy); x0 = (int16_t)(x0 + sx); } if (e2 <= dx) { err = (int16_t)(err + dx); y0 = (int16_t)(y0 + sy); } } // Clamp the accumulated box to the surface and mark it once. A line // entirely off-surface still ran the loop (so the box holds its raw // extents); clamping collapses it to a no-op when nothing landed on // the surface. if (minX < 0) { minX = 0; } if (minY < 0) { minY = 0; } if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; } if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; } if (minX <= maxX && minY <= maxY) { surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1)); } } void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { if (s == NULL) { return; } #ifdef JOEYLIB_PLATFORM_IIGS // Hot per-pixel path: the asm plot (halFastDrawPixel -> iigsDrawPixelInner) // plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect // call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table, // no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) -- // the far-pointer code for s->pixels[...] costs more than the // cross-segment JSL into the hand-tuned asm, so the asm plot stays. The // single-row dirty mark is widened inline, only for the stage. if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) { return; } (void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); if (s == jlStageGet()) { uint8_t word = SURFACE_WORD_INDEX(x); if (word < gStageMinWord[y]) { gStageMinWord[y] = word; } if (word > gStageMaxWord[y]) { gStageMaxWord[y] = word; } } #else plotPixelNoMark(s, x, y, colorIndex); // Public single-pixel path keeps the original per-call dirty mark // with the raw (x, y); only the Bresenham fallbacks batch the mark // into one accumulated box. plotPixelNoMark already no-op'd the // pixel itself if it was off-surface. surfaceMarkDirtyRect(s, x, y, 1, 1); #endif } void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { if (s == NULL) { return; } if (w == 0 || h == 0) { return; } // Degenerate dimensions: a 1xN or Nx1 rect IS a line, and a 1x1 // rect is a single pixel. jlFillRect handles both correctly so we // don't need to fork the inner logic. if (h == 1 || w == 1) { jlFillRect(s, x, y, w, h, colorIndex); return; } // Fast path: when the OUTER rect is fully on-surface every one of // the four edge spans is also fully on-surface, so each can skip // jlFillRect's 32-bit clip and route straight through the shared // on-surface tail. Test in 32-bit -- w/h are uint16_t, so x + w can // exceed int16_t range. Output is identical because on an // on-surface span jlFillRect's clip is a no-op. if (x >= 0 && y >= 0 && (int32_t)x + (int32_t)w <= SURFACE_WIDTH && (int32_t)y + (int32_t)h <= SURFACE_HEIGHT) { // Top edge. fillRectOnSurface(s, x, y, (int16_t)w, 1, colorIndex); // Bottom edge. fillRectOnSurface(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex); // Left edge (interior only -- top and bottom corners already drawn). fillRectOnSurface(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex); // Right edge (interior only). fillRectOnSurface(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex); return; } // General (possibly clipped) case: let jlFillRect clip each edge. // Top edge. jlFillRect(s, x, y, w, 1, colorIndex); // Bottom edge. jlFillRect(s, x, (int16_t)(y + (int16_t)h - 1), w, 1, colorIndex); // Left edge (interior only -- top and bottom corners already drawn). jlFillRect(s, x, (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex); // Right edge (interior only). jlFillRect(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex); } void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { int16_t y; int16_t x; int16_t ir; uint32_t xx; uint32_t yy; uint32_t r2; bool onSurface; if (s == NULL) { return; } if (r == 0) { jlDrawPixel(s, cx, cy, colorIndex); return; } // Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully // on-surface, and casting such an r to int16_t can wrap negative // (r > 32767), spuriously passing the bounds test and feeding the // asm / dirty-rect garbage. Reject it here before the cast. onSurface = false; if (r < SURFACE_HEIGHT) { ir = (int16_t)r; if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH && cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) { // The whole bounding circle is on-surface, so every scanline // span below is too. Remember this so the C fallback can // route spans through the lighter on-surface fill instead of // re-clipping each one through jlFillRect. onSurface = true; if (halFastFillCircle(s, cx, cy, r, colorIndex)) { surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); return; } } } // For each y from 0 to r, find the largest x such that x*x + y*y // <= r*r and emit a horizontal span. Maintain xx=x*x, yy=y*y // incrementally so the hot loop never does a 32-bit multiply -- // critical on 65816 / 68000 / 286 where mul is slow or absent. // (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are // uint32_t so r*r does not wrap for r >= 256 (a uint16_t product // overflows at r == 256 -> r2 == 0). /* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit * multiply helpers for the `2 * ...` constants. spanWidth is hoisted * because both jlFillRect calls in the body need it. */ xx = (uint32_t)r * (uint32_t)r; r2 = xx; yy = 0; x = (int16_t)r; for (y = 0; y <= (int16_t)r; y++) { uint16_t spanWidth; while (xx + yy > r2) { xx = xx - (uint32_t)((uint16_t)x + (uint16_t)x - 1u); x--; } spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u); if (onSurface) { // 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and // 0 <= cx - x ... cx + x < SURFACE_WIDTH for every span, so // jlFillRect's clip would be a no-op -- skip it. fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy + y), (int16_t)spanWidth, 1, colorIndex); if (y > 0) { fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy - y), (int16_t)spanWidth, 1, colorIndex); } } else { jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex); if (y > 0) { jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex); } } yy = yy + (uint32_t)((uint16_t)y + (uint16_t)y + 1u); } } void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { int16_t sx; int16_t sy; int16_t sw; int16_t sh; int32_t left; int32_t top; int32_t right; int32_t bottom; if (s == NULL) { return; } // Clip in 32-bit so any int16_t x/y combined with any uint16_t w/h // is handled without overflow (a uint16_t w > 32767 would wrap // negative if narrowed to int16_t before clipping, and a negative x // with a large w must still fill out to the right surface edge). // The surface is only 320x200, so the clipped result always fits // int16_t. left = (int32_t)x; top = (int32_t)y; right = left + (int32_t)w; bottom = top + (int32_t)h; if (left < 0) { left = 0; } if (top < 0) { top = 0; } if (right > SURFACE_WIDTH) { right = SURFACE_WIDTH; } if (bottom > SURFACE_HEIGHT) { bottom = SURFACE_HEIGHT; } if (right <= left || bottom <= top) { return; } sx = (int16_t)left; sy = (int16_t)top; sw = (int16_t)(right - left); sh = (int16_t)(bottom - top); // The clipped rect is now provably on-surface; share the fill + // planar dual-write + dirty-mark tail with the other on-surface // span emitters so there is one source of truth for it. fillRectOnSurface(s, sx, sy, sw, sh, colorIndex); } void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor) { uint8_t seedColor; if (s == NULL) { return; } if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { return; } /* halSamplePixel reads from whichever storage the port uses -- * works on both chunky (s->pixels) and planar (s->portData) ports. */ seedColor = halSamplePixel(s, x, y); if ((seedColor & 0x0F) == (newColor & 0x0F)) { return; } floodFillInternal(s, x, y, newColor, seedColor, true); } void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor) { uint8_t pix; if (s == NULL) { return; } if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { return; } pix = halSamplePixel(s, x, y); // Starting on a boundary pixel or already-filled pixel: nothing // to do. if ((pix & 0x0F) == (boundaryColor & 0x0F)) { return; } if ((pix & 0x0F) == (newColor & 0x0F)) { return; } floodFillInternal(s, x, y, newColor, boundaryColor, false); } uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { if (s == NULL) { return 0; } if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { return 0; } /* halSamplePixel reads from whichever storage the port uses -- * chunky ports return a nibble extracted from s->pixels; planar * ports read 4 plane bits and assemble the nibble. */ return halSamplePixel(s, x, y); } void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) { uint8_t nibble; uint8_t doubled; if (s == NULL) { return; } nibble = colorIndex & 0x0F; doubled = (uint8_t)((nibble << 4) | nibble); if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) { memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); } surfaceMarkDirtyAll(s); }