joeylib2/src/core/draw.c

856 lines
33 KiB
C

// 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 <stddef.h>
#include <string.h>
#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);
}