joeylib2/src/atarist/hal.c

2352 lines
96 KiB
C

// Atari ST HAL for M2 + M2.5.
//
// M2 scope:
// * XBIOS Setscreen to ST low-res (320x200x16, mode 0).
// * Word-interleaved ST planar buffer copied to the screen at present.
//
// M2.5 scope (per-band palette / SCB emulation):
// * jlpPresent scans the jlSurfaceT's SCB array and builds a compact
// transitions table: each entry is (start_line, palette_index)
// for a new palette region. For pattern.c's 8 uniform bands this
// is 8 entries; in the worst case it is 200 (one per scanline).
// * VBL ISR pre-loads the first band's palette, then programs
// MFP Timer B (event-count mode, TBDR = HBL delta to first
// transition) to fire at the END of the last scanline before
// the next band starts.
// * Timer B ISR writes the current band's palette, advances the
// transition index, and (stop/reload TBDR/restart) reprograms
// Timer B to fire at the next transition. With 8 transitions per
// frame the ISR runs 8 times instead of 313 -- well under the
// ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on
// event-count mode, and ~0.2% CPU overhead vs ~60% for per-HBL.
// * gLinePalettes is a flat pre-quantized (line, color)->$0RGB
// table built in jlpPresent by flattenScbPalettes; the ISR uses
// its first row per band as the source of 16 shifter writes.
//
// Deferred:
// * Takeover mode (direct shifter programming without TOS).
// * STE's extended palette bits (we drop to STF 9-bit for now).
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mint/osbind.h>
#include "port.h"
#include "surfaceInternal.h"
#include "spriteInternal.h"
#include "joey/tile.h"
// ----- Constants -----
// Word-interleaved ST planar uses the same 160 bytes/scanline as our
// chunky source, but organized as 20 groups of 4 words per scanline,
// with each word holding the 16 one-bit samples for one bitplane.
#define ST_BYTES_PER_ROW 160
#define ST_GROUPS_PER_ROW 20
#define ST_BYTES_PER_GROUP 8 // 4 plane words back-to-back
#define ST_PLANE_OFF_BYTES 2 // step between adjacent plane words within a group
#define ST_BITPLANES 4
#define ST_PLANAR_SIZE (ST_BYTES_PER_ROW * SURFACE_HEIGHT)
#define ST_SCREEN_ALIGN 256
// ----- Per-surface planar storage (project_planar_68k_plan, ST Phase 2) -----
//
// StPlanarT and the inline portData accessor moved to stPlanar.h
// (NATIVE-PERF Phase 2) so spriteCompile.c's dispatchers can read the
// buffer base without a cross-TU call.
#include "stPlanar.h"
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
// Shifter resolution ($FF8260, low 2 bits: 0=low 1=med 2=high/mono) and
// sync-mode ($FF820A, bit 1: set=50 Hz PAL, clear=60 Hz NTSC) registers,
// read by jlpFrameHz to report the true refresh instead of assuming PAL.
#define ST_SHIFTER_RES ((volatile uint8_t *)0xFFFF8260L)
#define ST_SYNC_MODE ((volatile uint8_t *)0xFFFF820AL)
#define ST_RES_MONO 0x02u // $FF8260 low-2-bits value for high res (SM124)
#define ST_SYNC_50HZ_BIT 0x02u // $FF820A bit 1: set = 50 Hz
#define ST_HZ_MONO 71u // SM124 mono refresh
#define ST_HZ_PAL 50u
#define ST_HZ_NTSC 60u
// Shifter video base address registers. The shifter latches the base
// at the start of each frame's display (during vblank), so a write
// here mid-visible-frame takes effect on the NEXT frame -- a deferred,
// tear-free page flip with no TOS involvement. STF exposes only the
// high and mid bytes; there is no low byte, which is why the ST screen
// must be 256-byte aligned (the low 8 bits of the base are hardwired
// to zero). Both bytes are odd-addressed byte registers; byte access
// to an odd address is legal on 68000 (only word/long faults).
#define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16
#define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8
// MFP hardware addresses.
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data
#define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL) // In-service A
#define MFP_TBCR_STOP 0x00
#define MFP_TBCR_EVENT 0x08
#define MFP_TB_CLEAR 0xFE // clear bit 0 of ISRA (Timer B)
// Exception-vector numbers passed to Setexc (= vector offset / 4).
#define VEC_VBL (0x70 / 4) // 68k autovector IRQ 4
#define VEC_MFP_TB (0x120 / 4) // MFP Timer B
#define INT_TIMER_B 8
// ----- Prototypes -----
// Phase 10: planar primitive helpers must be visible everywhere they
// could inline. Defined up here (between StPlanarT and the rest of
// the prototype block) so every jlp<Op> / fillSpan / circle walker
// can fold the 4-plane RMW directly into its body. always_inline
// hammers the point home for gcc-mint's conservative inliner.
static inline __attribute__((always_inline)) void stApplyMaskToGroup(uint8_t *groupBase, uint16_t mask, uint8_t color) {
uint16_t notMask = (uint16_t)~mask;
uint16_t *pw = (uint16_t *)groupBase;
if (color & 1u) { pw[0] = (uint16_t)(pw[0] | mask); } else { pw[0] = (uint16_t)(pw[0] & notMask); }
if (color & 2u) { pw[1] = (uint16_t)(pw[1] | mask); } else { pw[1] = (uint16_t)(pw[1] & notMask); }
if (color & 4u) { pw[2] = (uint16_t)(pw[2] | mask); } else { pw[2] = (uint16_t)(pw[2] & notMask); }
if (color & 8u) { pw[3] = (uint16_t)(pw[3] | mask); } else { pw[3] = (uint16_t)(pw[3] & notMask); }
}
static inline __attribute__((always_inline)) void stPlanarSetPixel(StPlanarT *pd, int16_t x, int16_t y, uint8_t color) {
uint16_t group = (uint16_t)((uint16_t)x >> 4);
uint16_t bitMask = (uint16_t)(1u << (15u - ((uint16_t)x & 15u)));
uint16_t notMask = (uint16_t)~bitMask;
uint16_t *pw = (uint16_t *)(pd->base
+ (uint16_t)y * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP);
if (color & 1u) { pw[0] = (uint16_t)(pw[0] | bitMask); } else { pw[0] = (uint16_t)(pw[0] & notMask); }
if (color & 2u) { pw[1] = (uint16_t)(pw[1] | bitMask); } else { pw[1] = (uint16_t)(pw[1] & notMask); }
if (color & 4u) { pw[2] = (uint16_t)(pw[2] | bitMask); } else { pw[2] = (uint16_t)(pw[2] & notMask); }
if (color & 8u) { pw[3] = (uint16_t)(pw[3] | bitMask); } else { pw[3] = (uint16_t)(pw[3] & notMask); }
}
static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPlanarT *pd, int16_t x, int16_t y) {
uint16_t group = (uint16_t)((uint16_t)x >> 4);
uint16_t bitMask = (uint16_t)(1u << (15u - ((uint16_t)x & 15u)));
const uint16_t *pw = (const uint16_t *)(pd->base
+ (uint16_t)y * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP);
uint8_t c = 0u;
if (pw[0] & bitMask) { c = (uint8_t)(c | 1u); }
if (pw[1] & bitMask) { c = (uint8_t)(c | 2u); }
if (pw[2] & bitMask) { c = (uint8_t)(c | 4u); }
if (pw[3] & bitMask) { c = (uint8_t)(c | 8u); }
return c;
}
// Flood-fill group decoder (PERF-AUDIT #4). For one 16-pixel group (4
// interleaved plane words) build the 16-bit mask whose bit
// (15 - (x & 15)) is set exactly where pixel x equals color: invert
// each plane word whose color bit is 0, then AND the four words. Four
// word reads decode 16 pixels, vs 4 reads PER pixel through
// stPlanarGetPixel.
static inline __attribute__((always_inline)) uint16_t stGroupEqBits(const uint16_t *gp, uint8_t color) {
uint16_t p0 = gp[0];
uint16_t p1 = gp[1];
uint16_t p2 = gp[2];
uint16_t p3 = gp[3];
if ((color & 1u) == 0u) { p0 = (uint16_t)~p0; }
if ((color & 2u) == 0u) { p1 = (uint16_t)~p1; }
if ((color & 4u) == 0u) { p2 = (uint16_t)~p2; }
if ((color & 8u) == 0u) { p3 = (uint16_t)~p3; }
return (uint16_t)(p0 & p1 & p2 & p3);
}
// Flood walk-out stop mask for one group: bit set where the walk must
// STOP (pixel is not fillable). matchEqual (jlFloodFill): stop where
// pix != matchColor. Bounded (jlFloodFillBounded): stop where
// pix == matchColor (boundary) or pix == newColor (already filled).
static inline __attribute__((always_inline)) uint16_t stFloodStopBits(const uint8_t *rowBase, uint16_t group, uint8_t matchColor, uint8_t newColor, bool matchEqual) {
const uint16_t *gp = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
if (matchEqual) {
return (uint16_t)~stGroupEqBits(gp, matchColor);
}
return (uint16_t)(stGroupEqBits(gp, matchColor) | stGroupEqBits(gp, newColor));
}
static uint16_t quantizeColorToSt(uint16_t orgb);
static void flattenScbPalettes(const jlSurfaceT *src);
static void setShifterVideoBase(const uint8_t *page);
static void writeDiagnostics(void);
static long writePrevPaletteRegs(void);
static __attribute__((interrupt_handler)) void timerBIsr(void);
static __attribute__((interrupt_handler)) void vblIsr(void);
static void buildTransitions(const jlSurfaceT *src);
static bool paletteOrScbChanged(const jlSurfaceT *src);
static void refreshPaletteStateIfNeeded(const jlSurfaceT *src);
// ----- Module state -----
// Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
// padding for runtime 256-byte alignment. TOS .PRG format only supports
// 2-byte object-file alignment, so we overallocate and align the first
// page pointer manually in jlpInit; the second page sits exactly one
// page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after
// the first, so it stays 256-byte aligned as the shifter requires.
static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN];
// Two physical screen pages. gFrontIdx selects the one the shifter is
// currently displaying; the OFF-screen page (gFrontIdx ^ 1) is the
// flip target written by the next present. Drawing primitives target
// the stage's own shadow planar buffer (StPlanarT.base), NOT a page,
// so the union(prev,cur) dirty copy at present brings the off-screen
// page fully up to date before the flip -- mirrors the Amiga 2-buffer
// gPrevStage* staleness pattern (shadow -> off-screen buffer -> flip).
static uint8_t *gScreenPage[2] = { NULL, NULL };
static uint8_t gFrontIdx = 0;
static void *gPrevPhysbase = NULL;
static void *gPrevLogbase = NULL;
static int16_t gPrevRez = 0;
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
static bool gModeSet = false;
// Supervisor-stack token returned by Super(0L) in jlpInit, used to drop
// back to user mode in jlpShutdown. The ST maps the entire $FF8000..
// $FFFFFF I/O region (shifter video-base, palette, MFP) as supervisor-
// only; a GEMDOS .PRG starts in user mode, so the direct register pokes
// in jlpPresent (setShifterVideoBase) bus-error until we enter
// supervisor for the program's lifetime.
static void *gSavedSsp = NULL;
static bool gSuperEntered = false;
// Snapshot of the previous frame's dirty bands. With two physical
// pages the off-screen page holds content from TWO frames ago, so each
// present must refresh the union(prev, current) of dirty rows from the
// shadow -- not just this frame's dirty rows -- or stale streaks from
// two frames back survive on the page we are about to display. Seeded
// to "all dirty" so the first two presents do full-screen refreshes
// and converge both pages to the current shadow. After that we pay
// only the small per-row union(prev,current) copy.
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// "The previous frame's snapshot recorded at least one dirty row."
// Lets jlpPresent skip the whole 200-row union scan + 200-row snapshot
// copy on consecutive idle frames: if neither the previous frame nor
// the current frame touched any row, union(prev,current) is empty, the
// off-screen page already matches the displayed page (the settling
// flip happened on the first idle frame), and the snapshot would only
// copy clean->clean. Seeded true to match the "all dirty" bootstrap of
// gPrevStage* above so the first two presents still run.
static bool gPrevAnyDirty = true;
// Per-scanline pre-quantized palette table. Indexed by display line;
// each row is a 16-word palette ready to be copied straight into the
// shifter registers. Written at present() time, read by the Timer B
// ISR with no CPU-side math beyond a counter subtract.
static uint16_t gLinePalettes[SURFACE_HEIGHT][SURFACE_COLORS_PER_PALETTE];
// Band-transition table. Each entry is one palette change: at
// display line gBandStart[i], load palette indexed by gBandPalIdx[i].
// Built once per jlpPresent from the jlSurfaceT's SCB array.
#define MAX_BANDS SURFACE_HEIGHT
static uint16_t gBandStart [MAX_BANDS];
static uint8_t gBandPalIdx[MAX_BANDS];
static uint16_t gBandCount = 0;
// Index of the band the Timer B ISR is currently scheduling TO. At
// VBL this is 0 (band 0 palette pre-loaded, Timer B scheduled to
// fire when it's time to transition to band 1). Each ISR fire writes
// the palette for gCurrentBand and advances to the next.
static volatile uint16_t gCurrentBand = 0;
// Diagnostic captures.
static volatile int16_t gFrameCount = 0;
static volatile uint16_t gLastBandCount = 0;
// Saved exception vectors for restore on shutdown.
static void (*gOldVblVec)(void) = NULL;
static void (*gOldTimerBVec)(void) = NULL;
// Cached SCB + palette from the last present. flattenScbPalettes runs
// 200 * 16 quantize conversions and buildTransitions rescans the full
// SCB; neither is cheap on a 7 MHz 68000. In the typical game loop
// (and every frame of the keys demo after the initial paint) SCB and
// palette never change, so caching and skipping those passes keeps
// rect presents down to just the screen blit.
static uint8_t gCachedScb [SURFACE_HEIGHT];
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false;
// ----- Internal helpers (alphabetical) -----
// Scan the surface's SCB and record one transition entry for each
// run of the same palette index. gBandCount is the number of
// distinct bands; gBandStart[i] is the display line where band i
// begins; gBandPalIdx[i] is the palette index that band uses.
static void buildTransitions(const jlSurfaceT *src) {
uint16_t line;
uint8_t idx;
uint8_t prev;
gBandCount = 0;
prev = 0xFF;
for (line = 0; line < SURFACE_HEIGHT; line++) {
idx = src->scb[line];
if (idx >= SURFACE_PALETTE_COUNT) {
idx = 0;
}
if (idx != prev) {
if (gBandCount < MAX_BANDS) {
gBandStart [gBandCount] = line;
gBandPalIdx[gBandCount] = idx;
gBandCount++;
}
prev = idx;
}
}
gLastBandCount = gBandCount;
}
// Pre-quantize every palette row indexed by scanline through the SCB
// into gLinePalettes, so the Timer B ISR can do a flat indexed copy
// without any surface-level lookups. Called once per jlpPresent.
static void flattenScbPalettes(const jlSurfaceT *src) {
uint16_t line;
uint16_t col;
uint8_t idx;
for (line = 0; line < SURFACE_HEIGHT; line++) {
idx = src->scb[line];
if (idx >= SURFACE_PALETTE_COUNT) {
idx = 0;
}
for (col = 0; col < SURFACE_COLORS_PER_PALETTE; col++) {
gLinePalettes[line][col] = quantizeColorToSt(src->palette[idx][col]);
}
}
}
// Returns true if SCB or palette values differ from the last present.
static bool paletteOrScbChanged(const jlSurfaceT *src) {
if (!gCacheValid) {
return true;
}
if (memcmp(gCachedScb, src->scb, sizeof(gCachedScb)) != 0) {
return true;
}
if (memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) != 0) {
return true;
}
return false;
}
// Rebuild the per-line palette table and band-transition table only
// when the SCB/palette state has actually changed. Both are hot -- the
// flatten pass runs 3200 palette entries through quantization -- so
// skipping them on clean frames dominates rect-present timing.
static void refreshPaletteStateIfNeeded(const jlSurfaceT *src) {
if (!paletteOrScbChanged(src)) {
return;
}
flattenScbPalettes(src);
buildTransitions(src);
memcpy(gCachedScb, src->scb, sizeof(gCachedScb));
memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette));
gCacheValid = true;
}
// 12-bit $0RGB to STF 9-bit palette register (drops the low bit of
// each 4-bit channel).
static uint16_t quantizeColorToSt(uint16_t orgb) {
uint16_t r;
uint16_t g;
uint16_t b;
r = (orgb >> 8) & 0x0F;
g = (orgb >> 4) & 0x0F;
b = orgb & 0x0F;
r = r >> 1;
g = g >> 1;
b = b >> 1;
return (uint16_t)((r << 8) | (g << 4) | b);
}
// Timer B interrupt handler. Fires once at each band transition;
// writes the band's palette to the shifter and lets Timer B's
// auto-reload keep counting for the next fire. We deliberately do
// NOT stop/reload/restart the timer here: that sequence would cost
// 1-2 HBL edges each fire, and those losses compound across 7+
// transitions into a visible "last band short" drift.
//
// MFP event-count auto-reload latches the CURRENT TBDR at each
// underflow, so a value written here does not take effect on the
// NEXT fire (that interval was already reloaded at the underflow
// that triggered this ISR) but on the fire AFTER next. To time
// variable-height bands correctly we therefore program one fire
// ahead: in the ISR that transitions to `band` we load TBDR with
// the band+1 -> band+2 delta (the interval that the band+1
// underflow will reload and use to time the band+2 transition).
// Uniform bands (like pattern.c) have all deltas equal, so the
// one-ahead value is identical and they stay perfectly aligned.
static void timerBIsr(void) {
uint16_t band;
uint8_t palIdx;
const uint16_t *src;
volatile uint16_t *dst;
uint16_t nextDelta;
band = gCurrentBand + 1;
gCurrentBand = band;
if (band < gBandCount) {
palIdx = gBandPalIdx[band];
if (palIdx >= SURFACE_PALETTE_COUNT) {
palIdx = 0;
}
src = &gLinePalettes[gBandStart[band]][0];
dst = ST_PALETTE_REGS;
dst[ 0] = src[ 0];
dst[ 1] = src[ 1];
dst[ 2] = src[ 2];
dst[ 3] = src[ 3];
dst[ 4] = src[ 4];
dst[ 5] = src[ 5];
dst[ 6] = src[ 6];
dst[ 7] = src[ 7];
dst[ 8] = src[ 8];
dst[ 9] = src[ 9];
dst[10] = src[10];
dst[11] = src[11];
dst[12] = src[12];
dst[13] = src[13];
dst[14] = src[14];
dst[15] = src[15];
if (band + 1 < gBandCount) {
// A transition to band+1 is still pending; keep Timer B
// running. The band+1 underflow has already had its reload
// value latched (set one fire earlier), so program TBDR
// now with the band+1 -> band+2 delta, which the band+1
// underflow will reload and use to time band+2. Don't
// stop the timer.
if (band + 2 < gBandCount) {
nextDelta = gBandStart[band + 2] - gBandStart[band + 1];
if (nextDelta == 0 || nextDelta > 255) {
nextDelta = 1;
}
*ST_MFP_TBDR = (uint8_t)nextDelta;
}
*ST_MFP_ISRA = MFP_TB_CLEAR;
return;
}
}
// No further transitions this frame; stopping Timer B here only
// affects the (never-used) next fire, not timing of any band
// we've already scheduled.
*ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_ISRA = MFP_TB_CLEAR;
}
// Vertical blank handler. Pre-loads band 0's palette so the first
// visible scanline is correct, then programs Timer B to fire at
// the HBL delta to the next band transition (if any).
static void vblIsr(void) {
uint16_t delta;
const uint16_t *src;
volatile uint16_t *dst;
gFrameCount = gFrameCount + 1;
gCurrentBand = 0;
if (gBandCount == 0) {
return;
}
// Stage band 0's palette into the shifter registers.
src = &gLinePalettes[gBandStart[0]][0];
dst = ST_PALETTE_REGS;
dst[ 0] = src[ 0];
dst[ 1] = src[ 1];
dst[ 2] = src[ 2];
dst[ 3] = src[ 3];
dst[ 4] = src[ 4];
dst[ 5] = src[ 5];
dst[ 6] = src[ 6];
dst[ 7] = src[ 7];
dst[ 8] = src[ 8];
dst[ 9] = src[ 9];
dst[10] = src[10];
dst[11] = src[11];
dst[12] = src[12];
dst[13] = src[13];
dst[14] = src[14];
dst[15] = src[15];
// Program Timer B for the next band transition.
if (gBandCount > 1) {
delta = gBandStart[1] - gBandStart[0];
if (delta == 0 || delta > 255) {
delta = 1;
}
*ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_TBDR = (uint8_t)delta;
*ST_MFP_ISRA = MFP_TB_CLEAR;
*ST_MFP_TBCR = MFP_TBCR_EVENT;
// The restart above loads the counter from TBDR (= the band
// 0 -> 1 delta), so the first fire is correctly timed. Now
// pre-program TBDR with the band 1 -> 2 delta so the band-1
// underflow auto-reloads with the right interval (one fire
// ahead, matching timerBIsr). Writing TBDR while counting
// only changes the next reload, not the in-flight count.
if (gBandCount > 2) {
delta = gBandStart[2] - gBandStart[1];
if (delta == 0 || delta > 255) {
delta = 1;
}
*ST_MFP_TBDR = (uint8_t)delta;
}
} else {
*ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_ISRA = MFP_TB_CLEAR;
}
}
// Point the shifter at a screen page by writing the high and mid base
// bytes. The low byte does not exist on STF (base is 256-aligned), so
// two byte writes fully specify the address. The shifter latches the
// base at the next frame start, so calling this mid-visible-frame (as
// jlpPresent does, after the app's jlWaitVBL) makes the flip take
// effect on the following frame with no tearing. No TOS / Setscreen
// involvement, which matters because our VBL ISR replaced TOS's and
// does not run TOS's Setscreen-latch path.
static void setShifterVideoBase(const uint8_t *page) {
uintptr_t addr;
addr = (uintptr_t)page;
*ST_VID_BASE_HI = (uint8_t)((addr >> 16) & 0xFFu);
*ST_VID_BASE_MID = (uint8_t)((addr >> 8) & 0xFFu);
}
static long writePrevPaletteRegs(void) {
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
ST_PALETTE_REGS[i] = gPrevPalette[i];
}
return 0;
}
static void writeDiagnostics(void) {
FILE *fp;
uint16_t i;
fp = fopen("diag.txt", "w");
if (fp == NULL) {
return;
}
fprintf(fp, "frames observed: %d\n", (int)gFrameCount);
fprintf(fp, "band count: %d\n", (int)gLastBandCount);
for (i = 0; i < gLastBandCount && i < 16; i++) {
fprintf(fp, " band %2d: start line %3d, palIdx %d\n",
(int)i, (int)gBandStart[i], (int)gBandPalIdx[i]);
}
fclose(fp);
}
// ----- HAL API (alphabetical) -----
bool jlpInit(const jlConfigT *config) {
uintptr_t addr;
uint16_t i;
(void)config;
// Enter supervisor mode for the program's lifetime. The shifter
// video-base, palette, and MFP registers live in the supervisor-only
// $FF8000..$FFFFFF I/O region; jlpPresent pokes the video-base bytes
// directly every flip, and a .PRG launched by GEMDOS starts in user
// mode, so without this the first present bus-errors. Staying super
// for the whole run (vs a per-present Supexec trap) also keeps the
// present hot path free of trap overhead. jlpShutdown restores user
// mode via the saved stack token. GEMDOS/XBIOS/BIOS traps (including
// the stdio used by joeylog) work fine from supervisor.
if (!gSuperEntered) {
gSavedSsp = (void *)Super(0L);
gSuperEntered = true;
}
// Align page 0 to 256 bytes inside the static storage; page 1
// follows exactly one 32000-byte (256-aligned) page later, so it
// is 256-aligned too. The shifter only displays 256-byte-aligned
// bases, so both pages must satisfy that.
addr = (uintptr_t)gScreenBuffer;
addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
gScreenPage[0] = (uint8_t *)addr;
gScreenPage[1] = gScreenPage[0] + SURFACE_PIXELS_SIZE;
gFrontIdx = 0;
memset(gScreenPage[0], 0, SURFACE_PIXELS_SIZE);
memset(gScreenPage[1], 0, SURFACE_PIXELS_SIZE);
// Bootstrap: pretend the previous frame dirtied EVERY row so the
// first two presents do full-page refreshes and bring both pages
// into sync with the shadow. After that we converge to the small
// per-row union(prev,current) cost.
for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = 0u;
gPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX;
}
gPrevPhysbase = Physbase();
gPrevLogbase = Logbase();
gPrevRez = Getrez();
// Capture current palette so we can restore exactly on shutdown.
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
}
// Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
// jlpPresent flips to the off-screen page via the shifter base
// registers from here on.
Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0);
gModeSet = true;
// Force hardware palette entry 0 to black so the overscan border
// (which the ST shows in palette[0]) stays black until the app's
// first refreshPaletteStateIfNeeded uploads its own palette.
Setcolor(0, 0x000);
// Save previous VBL + Timer B vectors, install ours. Timer B
// is at MFP vector $120; vector installed by Xbtimer below.
gOldVblVec = (void (*)(void))Setexc(VEC_VBL, -1L);
gOldTimerBVec = (void (*)(void))Setexc(VEC_MFP_TB, -1L);
(void)Setexc(VEC_VBL, (long)vblIsr);
// Program MFP Timer B: event-count (HBL) mode, initial TBDR=1
// (a placeholder -- VBL ISR reprograms it for the first real
// transition per frame), vector=timerBIsr, then enable in IMRA.
Xbtimer(1, MFP_TBCR_EVENT, 1, timerBIsr);
Jenabint(INT_TIMER_B);
return true;
}
void jlpPresent(const jlSurfaceT *src) {
StPlanarT *pd;
uint8_t *backPage;
uint8_t backIdx;
int16_t y;
int16_t firstDirty;
int16_t lastDirty;
int16_t runStart;
uint8_t curMin;
uint8_t curMax;
uint8_t prvMin;
uint8_t prvMax;
uint8_t minWord;
uint8_t maxWord;
uint16_t groupStart;
uint16_t groupEnd;
uint16_t byteStart;
uint16_t byteLen;
bool fullWidth;
if (src == NULL || !gModeSet) {
return;
}
pd = (StPlanarT *)src->portData;
if (pd == NULL) {
return;
}
refreshPaletteStateIfNeeded(src);
// Idle-frame early-out. When the previous frame's snapshot recorded
// no dirty rows (gPrevAnyDirty == false) AND the current frame
// touched no row, union(prev,current) is empty: both pages already
// match the shadow (the one settling flip after the last activity
// ran on the first idle frame), so there is nothing to copy, no
// page to flip, and the snapshot below would only copy clean->clean.
// Skip the whole 200-row union scan and the 200-row snapshot copy.
// A row is dirty iff its min word stayed at or below its max word;
// a single byte test per row with early exit on the first dirty row
// suffices. Palette/SCB updates already ran above, so display state
// stays correct.
if (!gPrevAnyDirty) {
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (!STAGE_DIRTY_ROW_CLEAN(y)) {
anyDirty = true;
break;
}
}
if (!anyDirty) {
return;
}
}
// PAGE FLIP: draw into the stage's shadow (pd->base), bring the
// OFF-screen page up to date with the union(prev,current) dirty
// rows, then point the shifter at it. The off-screen page holds
// content from TWO frames ago (strict A/B alternation), so any row
// dirtied in either of the last two frames may differ from the
// current shadow -- the union refresh covers exactly those rows.
// The displayed (front) page is never touched, so there is no
// tearing regardless of copy duration.
backIdx = (uint8_t)(gFrontIdx ^ 1u);
backPage = gScreenPage[backIdx];
// WIN 2: bound the present to the union dirty row range. The vast
// majority of game frames touch a small vertical band; finding the
// first/last union-dirty row up front lets the copy loop skip the
// huge clean head/tail of the page, and lets us bail the whole copy
// body when nothing is dirty (steady-state idle frames). The scan
// is 200 cheap byte compares -- far less than 200 memcpy setups.
firstDirty = -1;
lastDirty = -1;
for (y = 0; y < SURFACE_HEIGHT; y++) {
curMin = gStageMinWord[y];
curMax = gStageMaxWord[y];
prvMin = gPrevStageMinWord[y];
prvMax = gPrevStageMaxWord[y];
minWord = (curMin < prvMin) ? curMin : prvMin;
maxWord = (curMax > prvMax) ? curMax : prvMax;
if (minWord <= maxWord) {
if (firstDirty < 0) {
firstDirty = y;
}
lastDirty = y;
}
}
if (firstDirty >= 0) {
// Shadow -> off-screen page. Each dirty word covers 4 pixels
// (a quarter of an 8-byte group). Round to whole groups for a
// simple aligned memcpy, since planar groups are the natural
// copy unit.
//
// WIN 1: backPage and pd->base share the 160-byte row stride and
// are both contiguous, so a run of consecutive FULL-WIDTH dirty
// rows is one contiguous span of (rows * 160) bytes. Coalesce
// such runs into a single memcpy: mintlib memcpy bursts long-
// aligned blocks with movem.l/move.l, so one big copy beats many
// tiny per-row ones (fewer call setups, longer inner burst).
// Partial-width rows keep their own per-row group-aligned memcpy,
// so narrow dirty bands stay correct and cheap. runStart < 0
// means "no full-width run is open". The per-row band is the
// union(prev,current) band so a row stale on the off-screen page
// from two frames ago is fully refreshed, not just this frame's
// sub-span.
runStart = -1;
for (y = firstDirty; y <= lastDirty; y++) {
curMin = gStageMinWord[y];
curMax = gStageMaxWord[y];
prvMin = gPrevStageMinWord[y];
prvMax = gPrevStageMaxWord[y];
minWord = (curMin < prvMin) ? curMin : prvMin;
maxWord = (curMax > prvMax) ? curMax : prvMax;
if (minWord > maxWord) {
// Clean row breaks any open full-width run.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
runStart = -1;
}
continue;
}
// Defense-in-depth: an upstream clipping bug could leave a
// maxWord past the row; clamp so the groupEnd-driven byteLen
// can never push the memcpy into the next scanline.
if (maxWord >= SURFACE_WORDS_PER_ROW) {
maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
}
groupStart = (uint16_t)(minWord >> 2);
groupEnd = (uint16_t)((maxWord >> 2) + 1);
byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
fullWidth = (groupStart == 0 && byteLen == (uint16_t)ST_BYTES_PER_ROW);
if (fullWidth) {
// Extend (or open) the contiguous full-width run; defer
// the copy until a non-full-width row or the range end
// flushes it.
if (runStart < 0) {
runStart = y;
}
continue;
}
// Partial-width row: flush any open full-width run first so
// the page stays in row order, then copy just this row's band.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
runStart = -1;
}
memcpy(&backPage [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
&pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
byteLen);
}
// Flush a full-width run that reached the end of the dirty range.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(lastDirty + 1 - runStart) * (uint16_t)ST_BYTES_PER_ROW);
}
// The off-screen page now holds the complete current image.
// Point the shifter at it; the latch happens at the next frame
// start (we are mid-visible-frame here, after the app's
// jlWaitVBL), so the swap is tear-free. Only flip when there was
// something to refresh -- flipping on a fully idle frame would
// oscillate the display between the two pages with no benefit.
setShifterVideoBase(backPage);
gFrontIdx = backIdx;
}
// Snapshot this frame's dirty bands as "previous" for the next
// present. jlStagePresent clears the live bands right after
// jlpPresent returns, so we must grab them now. Next present unions
// these with that frame's dirty rows to refresh whatever the other
// page is stale on. Record whether any row was dirty so the next
// present can take the idle-frame early-out above.
{
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
gPrevStageMinWord[y] = gStageMinWord[y];
gPrevStageMaxWord[y] = gStageMaxWord[y];
if (!STAGE_DIRTY_ROW_CLEAN(y)) {
anyDirty = true;
}
}
gPrevAnyDirty = anyDirty;
}
}
// Vsync() is XBIOS opcode 37; mintlib exposes it directly. It blocks
// until the next 50 Hz (PAL) or 60 Hz (NTSC) vertical blank.
void jlpWaitVBL(void) {
int16_t before;
// Can't use Vsync(): TOS's Vsync increments _vblsem inside its
// own VBL ISR, which we replaced (Setexc(VEC_VBL, vblIsr)) with
// our SCB-emulating ISR that doesn't chain to the original.
// Spin on gFrameCount instead -- it's volatile and bumped every
// VBL by our ISR.
before = gFrameCount;
while (gFrameCount == before) {
// wait
}
}
// gFrameCount is already maintained by our VBL ISR; just narrow to
// uint16_t for the cross-port HAL contract.
uint16_t jlpFrameCount(void) {
return (uint16_t)gFrameCount;
}
uint16_t jlpFrameHz(void) {
/* Report the REAL refresh, not a hardcoded 50. The old assumption
* silently under-reported ops/sec ~17% under Hatari (which runs the
* ST at 60 Hz) and would be wrong on any NTSC/mono machine. The
* shifter resolution register selects mono vs color; for color the
* sync-mode register's 50/60 bit is authoritative. The program holds
* supervisor mode for its lifetime (jlpInit Super(0L)), so these
* $FFFF82xx I/O reads are legal. */
if ((*ST_SHIFTER_RES & 0x03u) == ST_RES_MONO) {
return ST_HZ_MONO;
}
if (*ST_SYNC_MODE & ST_SYNC_50HZ_BIT) {
return ST_HZ_PAL;
}
return ST_HZ_NTSC;
}
// TOS maintains a 200 Hz long counter at $4BA, incremented by the
// Timer C VBL chain. Read in supervisor mode (Supexec) so we don't
// fault on user-mode access. Resolution is 5 ms, plenty for sound
// scheduling at 60 Hz AGI ticks.
static volatile uint32_t gMillisBase200 = 0;
static volatile bool gMillisBaseSet = false;
static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL);
}
uint32_t jlpMillisElapsed(void) {
uint32_t ticks;
ticks = (uint32_t)Supexec(readHz200);
// 0 is a legal counter value (power-on tick 0 and every ~248-day
// wrap), so use an explicit one-shot flag instead of a value-0
// sentinel to capture the base.
if (!gMillisBaseSet) {
gMillisBase200 = ticks;
gMillisBaseSet = true;
return 0u;
}
// 200 Hz counter -> ms = ticks * 5. Unsigned subtraction is wrap-safe.
return (uint32_t)((ticks - gMillisBase200) * 5u);
}
void jlpShutdown(void) {
if (!gModeSet) {
return;
}
// Stop the audio Timer A first. The audio HAL has its own
// jlpAudioShutdown that disables Timer A and restores the vector,
// but cross-platform jlShutdown doesn't call it -- if a sketch
// forgets jlAudioShutdown(), Timer A keeps firing after our
// code unloads and TOS panics on the first dangling vector hit.
// Calling jlpAudioShutdown here is idempotent (gReady guard),
// so explicit-shutdown sketches still work.
jlpAudioShutdown();
// Disable MFP Timer B and restore the exception vectors before
// changing the screen -- a late ISR firing mid-Setscreen would
// write palette into whatever buffer TOS remapped.
Jdisint(INT_TIMER_B);
if (gOldTimerBVec != NULL) {
(void)Setexc(VEC_MFP_TB, (long)gOldTimerBVec);
}
if (gOldVblVec != NULL) {
(void)Setexc(VEC_VBL, (long)gOldVblVec);
}
Setscreen((long)gPrevLogbase, (long)gPrevPhysbase, gPrevRez);
Supexec(writePrevPaletteRegs);
writeDiagnostics();
gModeSet = false;
// Drop back to user mode so control returns to GEMDOS the way it
// launched us (mirrors the Super(0L) in jlpInit).
if (gSuperEntered) {
(void)Super(gSavedSsp);
gSuperEntered = false;
}
}
// ST word-interleaved planar 68k fast paths. The surface68kSt*
// primitives are ST-specific (plane-major group layout); they live in
// src/shared68k/surface68k.s alongside the Amiga-only planar helpers.
extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
extern void surface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color);
extern void surface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color);
extern void surface68kStFillCircle(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
extern void surface68kStFillRectSingleGroup(uint8_t *firstGroupPtr, uint16_t mask, uint16_t h, uint8_t color);
extern void surface68kStFillRectMulti(uint8_t *base, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t color);
extern void surface68kStLongFill(uint8_t *dst, uint16_t numGroups, uint32_t loLong, uint32_t hiLong);
extern void surface68kStTileFill8x8(uint8_t *firstGroupPtr, uint16_t mask, uint8_t color);
extern void surface68kStSprite16x16Save(uint8_t *base, uint16_t x, uint16_t y, uint8_t *dstBuf);
extern void surface68kStSprite16x16Restore(uint8_t *base, uint16_t x, uint16_t y, const uint8_t *srcBuf);
extern void surface68kStSpriteSaveByteAligned(uint8_t *base, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes);
extern void surface68kStSpriteRestoreByteAligned(uint8_t *base, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes);
// Phase 9: clear the entire planar buffer to a 4-bit color. Build an
// 8-byte group template (4 plane words: 0xFFFF or 0x0000 each by
// color bit) then stream it across all 4000 groups via long stores.
void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
StPlanarT *pd;
uint8_t color;
uint32_t loLong;
uint32_t hiLong;
if (s->portData == NULL) {
jlpGenericSurfaceClear(s, doubled);
return;
}
pd = (StPlanarT *)s->portData;
color = (uint8_t)(doubled & 0x0Fu);
/* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs.
* loLong = (p0_word << 16) | p1_word; hiLong = (p2_word << 16) | p3_word. */
loLong = ((color & 1u) ? 0xFFFF0000ul : 0ul)
| ((color & 2u) ? 0x0000FFFFul : 0ul);
hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul)
| ((color & 8u) ? 0x0000FFFFul : 0ul);
surface68kStLongFill(pd->base,
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
loLong, hiLong);
}
// Phase 9: planar-only. Chunky shadow is gone; only the planar buffer
// gets the pixel.
void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
StPlanarT *pd;
if (s->portData == NULL) {
jlpGenericDrawPixel(s, x, y, colorIndex);
return;
}
pd = (StPlanarT *)s->portData;
stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
}
// Phase 9 planar walkers. Same Bresenham as the cross-platform
// fallback, but writing to the planar buffer via stPlanarSetPixel.
// Mirror the Amiga amigaPlanarLine / amigaPlanarCircleOutline /
// amigaPlanarCircleFill structure. Phase 10 hand-rolled asm replaces
// these (jlDrawCircle.s already exists for Amiga; ST will get its own).
static void stPlanarLine(StPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
int16_t dx;
int16_t dy;
int16_t sx;
int16_t sy;
int16_t err;
int16_t e2;
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 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT) {
stPlanarSetPixel(pd, x0, y0, color);
}
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); }
}
}
static void stPlanarCircleOutline(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r, uint8_t color) {
int16_t bx = (int16_t)r;
int16_t by = 0;
int16_t err = (int16_t)(1 - bx);
int16_t px;
int16_t py;
while (bx >= by) {
/* 8 octants. Per-pixel clip since Bresenham can leave the
* surface for circles touching the edge. */
#define ST_PLOT(X, Y) do { px = (X); py = (Y); if (px >= 0 && px < SURFACE_WIDTH && py >= 0 && py < SURFACE_HEIGHT) { stPlanarSetPixel(pd, px, py, color); } } while (0)
ST_PLOT((int16_t)(cx + bx), (int16_t)(cy + by));
ST_PLOT((int16_t)(cx - bx), (int16_t)(cy + by));
ST_PLOT((int16_t)(cx + bx), (int16_t)(cy - by));
ST_PLOT((int16_t)(cx - bx), (int16_t)(cy - by));
ST_PLOT((int16_t)(cx + by), (int16_t)(cy + bx));
ST_PLOT((int16_t)(cx - by), (int16_t)(cy + bx));
ST_PLOT((int16_t)(cx + by), (int16_t)(cy - bx));
ST_PLOT((int16_t)(cx - by), (int16_t)(cy - bx));
#undef ST_PLOT
by++;
if (err > 0) {
bx--;
err = (int16_t)(err + 2 * (by - bx) + 1);
} else {
err = (int16_t)(err + 2 * by + 1);
}
}
}
// Phase 10: group-aware span fill -- the same leading-mask /
// full-group / trailing-mask decomposition jlpFillRect uses,
// but for one row. Replaces the per-pixel walk that gave jlFillCircle
// r=40 ~1 ops/sec.
static void stPlanarFillSpan(StPlanarT *pd, int16_t x0, int16_t x1, int16_t y, uint8_t color) {
int16_t left;
int16_t right;
if (y < 0 || y >= SURFACE_HEIGHT) {
return;
}
left = (x0 < x1) ? x0 : x1;
right = (x0 > x1) ? x0 : x1;
if (left < 0) { left = 0; }
if (right >= SURFACE_WIDTH) { right = SURFACE_WIDTH - 1; }
if (left > right) {
return;
}
surface68kStFillSpan(pd->base, left, right, y, color);
}
// Exact-disk span walk, same row coverage as src/core/draw.c
// jlFillCircle and the asm surface68kStFillCircle: for each y offset
// the largest x with x*x + y*y <= r*r, tracked incrementally. Defensive
// fallback only -- draw.c routes every off-surface circle through its
// own clipped span loop before jlpFillCircle is ever called, so keep r
// modest (< SURFACE_HEIGHT) if a future caller ever lands here.
static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r, uint8_t color) {
uint32_t r2 = (uint32_t)r * (uint32_t)r;
uint32_t xx = r2;
uint32_t yy = 0u;
int32_t x = (int32_t)r;
uint16_t y;
for (y = 0u; y <= r; y++) {
uint32_t xxLimit;
xxLimit = r2 - yy;
while (xx > xxLimit) {
xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
x--;
}
stPlanarFillSpan(pd, (int16_t)(cx - (int16_t)x), (int16_t)(cx + (int16_t)x), (int16_t)(cy + (int16_t)y), color);
if (y > 0u) {
stPlanarFillSpan(pd, (int16_t)(cx - (int16_t)x), (int16_t)(cx + (int16_t)x), (int16_t)(cy - (int16_t)y), color);
}
yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
}
}
void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
StPlanarT *pd;
if (s->portData == NULL) {
return;
}
pd = (StPlanarT *)s->portData;
// Asm walker assumes fully on-surface; partial-clip lines fall
// back to the C walker which clips per-pixel.
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
&& x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
} else {
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
}
void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
if (s->portData == NULL) {
return;
}
pd = (StPlanarT *)s->portData;
/* Off-surface circles fall back to the per-pixel C walker which
* does the clip per plot; the asm assumes fully-on-surface so it
* can drop the clip check from the inner loop. */
if ((int32_t)cx - (int32_t)r < 0
|| (int32_t)cx + (int32_t)r >= SURFACE_WIDTH
|| (int32_t)cy - (int32_t)r < 0
|| (int32_t)cy + (int32_t)r >= SURFACE_HEIGHT) {
stPlanarCircleOutline(pd, cx, cy, r, (uint8_t)(colorIndex & 0x0Fu));
} else {
surface68kStCircleOutline(pd->base, (uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu));
}
}
bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
// Off-surface bounding box falls back to the C span walker, which
// clips each span; the asm assumes the whole circle is on-surface.
if ((int32_t)cx - (int32_t)r < 0
|| (int32_t)cx + (int32_t)r >= SURFACE_WIDTH
|| (int32_t)cy - (int32_t)r < 0
|| (int32_t)cy + (int32_t)r >= SURFACE_HEIGHT) {
stPlanarCircleFill(pd, cx, cy, r, (uint8_t)(colorIndex & 0x0Fu));
} else {
surface68kStFillCircle(pd->base, (uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu));
}
return true;
}
// Phase 2: allocate a shadow word-interleaved planar buffer per
// surface. Both stage and non-stage get their own buffer; the two
// displayed pages (gScreenPage[0/1]) are the flip targets, kept
// separate from every surface's shadow.
//
// LONG alignment is required, not just word: the full-row long-fill
// path and circle.s both do `move.l` writes on this buffer, and
// 68000 address-errors on long access to a word-aligned-but-not-
// long-aligned destination. mintlib's malloc usually returns long-
// aligned blocks, but TOS heaps can land at odd offsets after a
// few allocations -- over-allocate by 4 bytes and align up here.
// Symptom of getting this wrong: intermittent return-to-desktop
// after the red startup paint as the first long write hits an
// odd-by-2 base.
void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
StPlanarT *pd;
uint8_t *raw;
uintptr_t addr;
(void)s;
(void)isStage;
pd = (StPlanarT *)calloc(1, sizeof(StPlanarT));
if (pd == NULL) {
return NULL;
}
raw = (uint8_t *)malloc(ST_PLANAR_SIZE + 4u);
if (raw == NULL) {
free(pd);
return NULL;
}
addr = (uintptr_t)raw;
addr = (addr + 3u) & ~(uintptr_t)3u; /* round up to long-aligned */
pd->raw = raw;
pd->base = (uint8_t *)addr;
pd->ownsBuffer = true;
memset(pd->base, 0, ST_PLANAR_SIZE);
return pd;
}
void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
StPlanarT *pd;
(void)s;
(void)isStage;
if (portData == NULL) {
return;
}
pd = (StPlanarT *)portData;
if (pd->ownsBuffer && pd->raw != NULL) {
free(pd->raw);
}
free(pd);
}
// Phase 3: dual-write to the word-interleaved planar shadow buffer.
// Chunky shadow (s->pixels) is still the source-of-truth for display
// (c2p at present); the planar buffer becomes authoritative at
// Phase 9 switch flip.
//
// Per row: split [x, x+w) into a leading partial group (bits
// 15..15-bitFirst within the leading word -> mask = (1<<(16-bitFirst))
// - 1), zero or more full groups, and a trailing partial group
// (bits 15..15-bitLast -> mask = ~((1<<(15-bitLast)) - 1)). For each
// of the 4 plane words within a group, the bit value of the color
// index controls OR-with-mask (set) vs AND-with-not-mask (clear).
// Single-group case (groupFirst == groupLast) collapses to one word
// RMW per plane with the combined mask.
// (stApplyMaskToGroup is defined inline near the top of the file.)
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
StPlanarT *pd;
uint16_t groupFirst;
uint16_t groupLast;
uint8_t *rowBase;
if (s->portData == NULL) {
jlpGenericFillRect(s, x, y, w, h, colorIndex);
return;
}
// Callers guarantee s != NULL and positive w/h: the jlFillRect
// wrapper validates + clips before fillRectOnSurface, and the
// fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1.
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return;
}
/* Phase 10 fast path: x == 0 AND w == SURFACE_WIDTH means the rect
* spans every group on every row with no edge masks. movem.l-based
* asm long-fill batches 24 bytes per call. UBER jlFillRect 320x200
* lands here. */
if (x == 0 && w == SURFACE_WIDTH) {
uint32_t loLong = ((colorIndex & 1u) ? 0xFFFF0000ul : 0ul)
| ((colorIndex & 2u) ? 0x0000FFFFul : 0ul);
uint32_t hiLong = ((colorIndex & 4u) ? 0xFFFF0000ul : 0ul)
| ((colorIndex & 8u) ? 0x0000FFFFul : 0ul);
surface68kStLongFill(pd->base + (uint16_t)y * ST_BYTES_PER_ROW,
(uint16_t)((uint16_t)h * ST_GROUPS_PER_ROW),
loLong, hiLong);
return;
}
groupFirst = (uint16_t)((uint16_t)x >> 4);
groupLast = (uint16_t)(((uint16_t)x + w - 1u) >> 4);
if (groupFirst == groupLast) {
uint16_t bitFirst = (uint16_t)((uint16_t)x & 15u);
uint16_t bitLast = (uint16_t)(((uint16_t)x + w - 1u) & 15u);
uint16_t leftMask = (uint16_t)((1ul << (16u - bitFirst)) - 1ul);
uint16_t rightMask = (uint16_t)~((1ul << (15u - bitLast)) - 1ul);
uint16_t mask = (uint16_t)(leftMask & rightMask);
rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
surface68kStFillRectSingleGroup(rowBase + groupFirst * ST_BYTES_PER_GROUP,
mask, h, colorIndex);
return;
}
/* Phase 10.5: multi-group case (groupFirst != groupLast) handled
* by 16-way-color-dispatched asm with hoisted mask state. ~3-5x
* faster than the C loop with inlined stApplyMaskToGroup. */
surface68kStFillRectMulti(pd->base, x, y, w, h, colorIndex);
}
void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
StPlanarT *dstPd;
StPlanarT *srcPd;
if (dst == NULL || src == NULL) {
return;
}
dstPd = (StPlanarT *)dst->portData;
srcPd = (StPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
return;
}
memcpy(dstPd->base, srcPd->base, ST_PLANAR_SIZE);
}
// ----- Phases 4-7: per-pixel / tile / sprite / blit planar primitives -----
//
// These implementations dual-write the planar shadow alongside the
// chunky shadow that cross-platform code maintains. They use simple
// per-pixel walks for clarity and correctness; Phase 10 will replace
// the hot ones (jlFillRect, jlDrawPixel, sprite codegen) with hand-rolled
// asm. The emphasis here is "correct first, fast later" -- Phase 9
// flips the read source from chunky to planar and we'll see immediately
// (DRAW hash vs IIgs reference) whether each primitive landed bits
// in the right place.
// stPlanarSetPixel and stPlanarGetPixel are defined inline near the
// top of the file (between StPlanarT and the prototype block) so
// every callsite folds the 4-plane RMW into its body.
// Phase 5 tile ops. 8x8 tiles at byte position (bx, by) start at
// pixel (bx*8, by*8). 8 pixels wide always covers exactly half a
// 16-pixel group: high half (bits 15..8) when bx is even, low half
// (bits 7..0) when bx is odd. Per-row work is 4 plane half-word RMWs.
void jlpTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
StPlanarT *pd;
uint16_t group;
uint16_t halfMask;
uint8_t *gp;
// Core jlTileFill already proved s != NULL and clipped bx/by; only
// the non-planar (offscreen, no portData) fallback needs handling.
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
jlpGenericTileFill(s, bx, by, colorIndex);
return;
}
group = (uint16_t)((uint16_t)bx >> 1);
halfMask = ((bx & 1u) == 0u) ? 0xFF00u : 0x00FFu;
gp = pd->base + (uint16_t)by * 8u * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP;
/* Phase 10 final: specialized 8x8 unrolled tile-fill skips the
* generic FRG_LOOP's per-row subq+bne overhead. */
surface68kStTileFill8x8(gp, halfMask, colorIndex);
}
// Phase 10: tile paste/snap reuse the asm sprite save/restore
// helpers -- identical per-row work patterns at byte-aligned
// positions. Width 8 = single tile column = single half-group
// write per plane. The asm walker handles 8 rows just as well
// as a sprite's variable height.
void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
StPlanarT *pd;
uint16_t group;
uint8_t *dstAddr;
int16_t row;
if (dst->portData == NULL) {
jlpGenericTilePaste(dst, bx, by, chunkyTile);
return;
}
pd = (StPlanarT *)dst->portData;
/* Phase 10.5: jlTileT.pixels holds plane-major bytes (4 plane bytes
* per row * 8 rows = 32 bytes). Direct byte copy to the planar
* buffer; no chunky <-> planar conversion. Mirrors the sibling
* jlpTileCopy pattern but reads from the contiguous tile
* buffer. Drops the asm-walker entry/exit overhead. */
group = (uint16_t)((uint16_t)bx >> 1);
dstAddr = pd->base
+ (uint16_t)by * 8u * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP
+ (uint16_t)(bx & 1u);
(void)row;
#define ST_TILE_PASTE_ROW \
do { \
dstAddr[0] = chunkyTile[0]; \
dstAddr[2] = chunkyTile[1]; \
dstAddr[4] = chunkyTile[2]; \
dstAddr[6] = chunkyTile[3]; \
dstAddr += ST_BYTES_PER_ROW; \
chunkyTile += TILE_BYTES_PER_ROW; \
} while (0)
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW;
#undef ST_TILE_PASTE_ROW
}
// Planar monochrome paste. monoTile follows the cross-port mono
// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky
// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two
// pixels per byte. A pixel renders fgColor when its source nibble is
// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each
// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = leftmost
// pixel, matching the planar bit order), then each plane k writes
// outPlaneK = (shape & maskFgK) | (~shape & maskBgK)
// at dstAddr + k*2 inside the 16-pixel group (4 plane words = 8 bytes
// per group). bx odd selects the second byte of each plane word (the
// low / right 8 pixels of the group).
void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
StPlanarT *pd;
uint16_t group;
uint8_t *dstAddr;
uint8_t row;
uint8_t col;
uint8_t plane;
uint8_t masksFg[4];
uint8_t masksBg[4];
if (dst->portData == NULL) {
jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor);
return;
}
pd = (StPlanarT *)dst->portData;
for (plane = 0u; plane < 4u; plane++) {
masksFg[plane] = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u);
masksBg[plane] = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u);
}
group = (uint16_t)((uint16_t)bx >> 1);
dstAddr = pd->base
+ (uint16_t)by * 8u * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP
+ (uint16_t)(bx & 1u);
for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) {
uint8_t shape = 0u;
for (col = 0u; col < TILE_BYTES_PER_ROW; col++) {
uint8_t srcByte = *monoTile++;
shape = (uint8_t)(shape << 2);
if (srcByte & 0xF0u) {
shape = (uint8_t)(shape | 0x02u);
}
if (srcByte & 0x0Fu) {
shape = (uint8_t)(shape | 0x01u);
}
}
dstAddr[0] = (uint8_t)((uint8_t)(shape & masksFg[0]) |
(uint8_t)((uint8_t)(~shape) & masksBg[0]));
dstAddr[2] = (uint8_t)((uint8_t)(shape & masksFg[1]) |
(uint8_t)((uint8_t)(~shape) & masksBg[1]));
dstAddr[4] = (uint8_t)((uint8_t)(shape & masksFg[2]) |
(uint8_t)((uint8_t)(~shape) & masksBg[2]));
dstAddr[6] = (uint8_t)((uint8_t)(shape & masksFg[3]) |
(uint8_t)((uint8_t)(~shape) & masksBg[3]));
dstAddr += ST_BYTES_PER_ROW;
}
}
void jlpTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) {
const StPlanarT *pd;
uint16_t group;
const uint8_t *srcAddr;
int16_t row;
if (src->portData == NULL) {
jlpGenericTileSnap(src, bx, by, chunkyOut);
return;
}
pd = (const StPlanarT *)src->portData;
/* Phase 10.5: write plane-major bytes to jlTileT (4 per row * 8 rows). */
group = (uint16_t)((uint16_t)bx >> 1);
srcAddr = pd->base
+ (uint16_t)by * 8u * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP
+ (uint16_t)(bx & 1u);
(void)row;
#define ST_TILE_SNAP_ROW \
do { \
chunkyOut[0] = srcAddr[0]; \
chunkyOut[1] = srcAddr[2]; \
chunkyOut[2] = srcAddr[4]; \
chunkyOut[3] = srcAddr[6]; \
srcAddr += ST_BYTES_PER_ROW; \
chunkyOut += TILE_BYTES_PER_ROW; \
} while (0)
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW;
#undef ST_TILE_SNAP_ROW
}
// Phase 10: direct planar->planar tile copy. Each tile occupies one
// half-byte of one plane word per plane per row (8 rows total).
// We just byte-copy 4 plane bytes per row -- no chunky scratch, no
// bit transpose, no LUT. ~640 cyc per tile vs ~5000 cyc for the
// snap+paste path.
void jlpTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
StPlanarT *dstPd;
const StPlanarT *srcPd;
uint8_t *dstAddr;
const uint8_t *srcAddr;
uint16_t srcGroup;
uint16_t dstGroup;
int16_t row;
if (dst->portData == NULL || src->portData == NULL) {
jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
return;
}
dstPd = (StPlanarT *)dst->portData;
srcPd = (const StPlanarT *)src->portData;
srcGroup = (uint16_t)((uint16_t)srcBx >> 1);
dstGroup = (uint16_t)((uint16_t)dstBx >> 1);
srcAddr = srcPd->base
+ (uint16_t)srcBy * 8u * ST_BYTES_PER_ROW
+ srcGroup * ST_BYTES_PER_GROUP
+ (uint16_t)(srcBx & 1u);
dstAddr = dstPd->base
+ (uint16_t)dstBy * 8u * ST_BYTES_PER_ROW
+ dstGroup * ST_BYTES_PER_GROUP
+ (uint16_t)(dstBx & 1u);
/* gcc-mint -O2 does NOT unroll the 8-iter byte-copy loop,
* leaving cmpl + bnes loop overhead per row. Manual unroll
* drops ~150 cyc/call. (void)row keeps the unused decl quiet. */
(void)row;
#define ST_TILE_COPY_ROW \
do { \
dstAddr[0] = srcAddr[0]; \
dstAddr[2] = srcAddr[2]; \
dstAddr[4] = srcAddr[4]; \
dstAddr[6] = srcAddr[6]; \
srcAddr += ST_BYTES_PER_ROW; \
dstAddr += ST_BYTES_PER_ROW; \
} while (0)
ST_TILE_COPY_ROW; /* row 0 */
ST_TILE_COPY_ROW; /* row 1 */
ST_TILE_COPY_ROW; /* row 2 */
ST_TILE_COPY_ROW; /* row 3 */
ST_TILE_COPY_ROW; /* row 4 */
ST_TILE_COPY_ROW; /* row 5 */
ST_TILE_COPY_ROW; /* row 6 */
ST_TILE_COPY_ROW; /* row 7 */
#undef ST_TILE_COPY_ROW
}
void jlpTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) {
StPlanarT *dstPd;
uint8_t scratch[TILE_BYTES];
int16_t row;
uint16_t dstX0;
uint16_t dstY0;
uint16_t group;
uint16_t halfOff;
uint8_t *dstByte;
uint8_t p0;
uint8_t p1;
uint8_t p2;
uint8_t p3;
uint8_t xK0;
uint8_t xK1;
uint8_t xK2;
uint8_t xK3;
uint8_t mask;
uint8_t notMask;
if (dst->portData == NULL) {
jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent);
return;
}
dstPd = (StPlanarT *)dst->portData;
/* Phase 10.5: bulk-plane fast path. scratch holds plane-major bytes
* (4 plane bytes per row * 8 rows). For each row, build a "non-
* transparent" mask = OR of (plane_byte XOR replicated transparent
* bit) -- 1s where the source pixel != transparent. Then 4 byte
* RMWs (one per plane) write the row at byte-aligned dst.
*
* For transparent=0 this collapses to mask = p0|p1|p2|p3.
* Replaces the prior 64-iteration per-pixel SetPixel walker. */
jlpTileSnap(src, srcBx, srcBy, scratch);
dstX0 = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstY0 = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
group = (uint16_t)(dstX0 >> 4);
halfOff = (uint16_t)((dstX0 & 8u) >> 3u);
dstByte = dstPd->base + dstY0 * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP + halfOff;
xK0 = (transparent & 1u) ? 0xFFu : 0u;
xK1 = (transparent & 2u) ? 0xFFu : 0u;
xK2 = (transparent & 4u) ? 0xFFu : 0u;
xK3 = (transparent & 8u) ? 0xFFu : 0u;
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
p0 = scratch[row * 4 + 0];
p1 = scratch[row * 4 + 1];
p2 = scratch[row * 4 + 2];
p3 = scratch[row * 4 + 3];
mask = (uint8_t)((p0 ^ xK0) | (p1 ^ xK1) | (p2 ^ xK2) | (p3 ^ xK3));
if (mask != 0u) {
notMask = (uint8_t)~mask;
dstByte[0] = (uint8_t)((dstByte[0] & notMask) | (p0 & mask));
dstByte[2] = (uint8_t)((dstByte[2] & notMask) | (p1 & mask));
dstByte[4] = (uint8_t)((dstByte[4] & notMask) | (p2 & mask));
dstByte[6] = (uint8_t)((dstByte[6] & notMask) | (p3 & mask));
}
dstByte += ST_BYTES_PER_ROW;
}
}
// Phase 10 fast path: byte-aligned, fully-on-surface sprite draw.
// Builds 4 plane bytes + 1 opacity byte from each tile-column row
// in one pass, then does 4 word RMWs per group half. ~7x faster
// than the per-pixel walker for the typical (byte-aligned) case.
//
// Per row of a tile column: 4 chunky bytes -> 8 nibbles -> {plane0
// byte, plane1 byte, plane2 byte, plane3 byte, opacity byte}. The
// opacity byte has bits set where the sprite pixel is non-zero;
// transparent pixels (color 0) leave the destination plane bits
// alone via the (word AND ~opMask) | (planeBits AND opMask) RMW.
//
// 8 pixels at byte-aligned x always cover exactly one half of one
// group: high half if (x mod 16) == 0, low half if (x mod 16) == 8.
// We branch once per tile column on (dstX & 8).
static void stSpriteDrawByteAligned(StPlanarT *pd, const jlSpriteT *sp, int16_t x, int16_t y) {
uint16_t wTiles = sp->widthTiles;
int16_t srcH = (int16_t)(sp->heightTiles * 8);
uint8_t *rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
int16_t row;
for (row = 0; row < srcH; row++) {
int16_t tileY = (int16_t)(row >> 3);
int16_t inTileY = (int16_t)(row & 7);
const uint8_t *tileRowBase = sp->tileData + (uint32_t)tileY * wTiles * 32u + (uint32_t)inTileY * 4u;
int16_t tileCol;
for (tileCol = 0; tileCol < (int16_t)wTiles; tileCol++) {
const uint8_t *trp = tileRowBase + (uint32_t)tileCol * 32u;
uint8_t b0 = trp[0];
uint8_t b1 = trp[1];
uint8_t b2 = trp[2];
uint8_t b3 = trp[3];
uint8_t pb0 = 0u;
uint8_t pb1 = 0u;
uint8_t pb2 = 0u;
uint8_t pb3 = 0u;
uint8_t pop = 0u;
uint8_t c;
/* 8 pixels per tile column: hi(b0),lo(b0),hi(b1),lo(b1),
* hi(b2),lo(b2),hi(b3),lo(b3) at bit positions 7..0
* within the eventual plane byte. Walk inline -- no LUT
* loop overhead. */
c = (uint8_t)(b0 >> 4); if (c) { pop = (uint8_t)(pop | 0x80u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x80u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x80u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x80u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x80u); }
c = (uint8_t)(b0 & 0x0Fu); if (c) { pop = (uint8_t)(pop | 0x40u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x40u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x40u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x40u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x40u); }
c = (uint8_t)(b1 >> 4); if (c) { pop = (uint8_t)(pop | 0x20u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x20u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x20u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x20u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x20u); }
c = (uint8_t)(b1 & 0x0Fu); if (c) { pop = (uint8_t)(pop | 0x10u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x10u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x10u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x10u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x10u); }
c = (uint8_t)(b2 >> 4); if (c) { pop = (uint8_t)(pop | 0x08u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x08u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x08u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x08u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x08u); }
c = (uint8_t)(b2 & 0x0Fu); if (c) { pop = (uint8_t)(pop | 0x04u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x04u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x04u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x04u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x04u); }
c = (uint8_t)(b3 >> 4); if (c) { pop = (uint8_t)(pop | 0x02u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x02u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x02u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x02u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x02u); }
c = (uint8_t)(b3 & 0x0Fu); if (c) { pop = (uint8_t)(pop | 0x01u); if (c & 1u) pb0 = (uint8_t)(pb0 | 0x01u); if (c & 2u) pb1 = (uint8_t)(pb1 | 0x01u); if (c & 4u) pb2 = (uint8_t)(pb2 | 0x01u); if (c & 8u) pb3 = (uint8_t)(pb3 | 0x01u); }
if (pop != 0u) {
int16_t dstX = (int16_t)(x + tileCol * 8);
uint16_t group = (uint16_t)((uint16_t)dstX >> 4);
uint16_t *pw = (uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
uint16_t opMask;
uint16_t notOpMask;
uint16_t pv0;
uint16_t pv1;
uint16_t pv2;
uint16_t pv3;
if ((dstX & 8) == 0) {
opMask = (uint16_t)((uint16_t)pop << 8);
pv0 = (uint16_t)((uint16_t)pb0 << 8);
pv1 = (uint16_t)((uint16_t)pb1 << 8);
pv2 = (uint16_t)((uint16_t)pb2 << 8);
pv3 = (uint16_t)((uint16_t)pb3 << 8);
} else {
opMask = (uint16_t)pop;
pv0 = (uint16_t)pb0;
pv1 = (uint16_t)pb1;
pv2 = (uint16_t)pb2;
pv3 = (uint16_t)pb3;
}
notOpMask = (uint16_t)~opMask;
pw[0] = (uint16_t)((pw[0] & notOpMask) | pv0);
pw[1] = (uint16_t)((pw[1] & notOpMask) | pv1);
pw[2] = (uint16_t)((pw[2] & notOpMask) | pv2);
pw[3] = (uint16_t)((pw[3] & notOpMask) | pv3);
}
}
rowBase += ST_BYTES_PER_ROW;
}
}
// Phase 10: sprite walker with hoisted state. rowBase advances by
// 160 per row instead of recomputing y*160 per pixel; tile-row
// pointer is advanced once per tile column (8 cols) instead of
// recomputed per pixel; the per-pixel inner block is the inlined
// stPlanarSetPixel body so there's no nested function entry / y*160
// re-derivation. Major rewrite of the dispatcher path that drove
// the 0.06x sprite gap before this commit.
void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
StPlanarT *pd;
int16_t spritePxStart;
int16_t spritePyStart;
int16_t srcW;
int16_t srcH;
int16_t sx;
int16_t sy;
int16_t row;
int16_t col;
uint16_t wTiles;
uint8_t *rowBase;
if (s == NULL || sp == NULL || sp->tileData == NULL) {
return;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return;
}
wTiles = sp->widthTiles;
srcW = (int16_t)(wTiles * 8);
srcH = (int16_t)(sp->heightTiles * 8);
spritePxStart = x;
spritePyStart = y;
/* Phase 10 fast path: byte-aligned x AND fully on-surface lets
* us bulk-write 8 pixels into one group half per tile column,
* skipping the per-pixel walker entirely. UBER's jlSpriteDraw test
* at (160, 100) lands here. */
if ((x & 7) == 0
&& x >= 0 && (x + srcW) <= SURFACE_WIDTH
&& y >= 0 && (y + srcH) <= SURFACE_HEIGHT) {
stSpriteDrawByteAligned(pd, sp, x, y);
return;
}
sx = 0;
sy = 0;
if (spritePxStart < 0) { sx = (int16_t)(-spritePxStart); srcW = (int16_t)(srcW - sx); spritePxStart = 0; }
if (spritePyStart < 0) { sy = (int16_t)(-spritePyStart); srcH = (int16_t)(srcH - sy); spritePyStart = 0; }
if (spritePxStart >= SURFACE_WIDTH || spritePyStart >= SURFACE_HEIGHT || srcW <= 0 || srcH <= 0) {
return;
}
if (spritePxStart + srcW > SURFACE_WIDTH) { srcW = (int16_t)(SURFACE_WIDTH - spritePxStart); }
if (spritePyStart + srcH > SURFACE_HEIGHT) { srcH = (int16_t)(SURFACE_HEIGHT - spritePyStart); }
/* Phase 11 (shared-walker): byte-wide shifted draw. For each 8-px
* column chunk of the sprite, build 4 plane bytes + a shape mask
* via c2p, shift left/right by (x & 7), and AND-mask + OR the
* results onto the dst plane bytes. ST's planar layout stores 4
* plane bytes interleaved at word granularity inside each 16-px
* group: byte address = base + group*8 + plane*2 + half_byte
* (where half_byte = dst_byte_col & 1). This replaces the old
* per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and
* murdered the frame rate on shifted sprites (passenger walk,
* flame, anything else not on an 8-px boundary).
*
* Phase / placement is driven from the ORIGINAL signed x so a
* sub-byte left clip (x negative and not a multiple of 8) keeps
* its true source-to-destination phase. shift = x & 7 picks the
* low 3 bits of x (correct two's-complement modulo-8 even when x
* is negative); startByteX = x >> 3 (arithmetic) is the signed
* destination byte column for source byte column 0. Each source
* byte column is walked in full and the per-byte guards drop the
* off-surface portions, so the left byte and its right-carry spill
* are clipped independently. */
{
uint8_t shift = (uint8_t)(x & 7);
int16_t startByteX = (int16_t)(x >> 3);
int16_t byteCol;
rowBase = pd->base + (uint16_t)spritePyStart * ST_BYTES_PER_ROW;
for (row = 0; row < srcH; row++) {
int16_t spritePy = (int16_t)(sy + row);
int16_t tileY = (int16_t)(spritePy >> 3);
int16_t inTileY = (int16_t)(spritePy & 7);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
const uint8_t *chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
uint8_t p0 = 0u, p1 = 0u, p2 = 0u, p3 = 0u, mask = 0u;
uint8_t bit;
for (bit = 0; bit < 8u; bit++) {
uint8_t nibble = (bit & 1u)
? (uint8_t)(chunky[bit >> 1] & 0x0Fu)
: (uint8_t)(chunky[bit >> 1] >> 4);
uint8_t bm = (uint8_t)(0x80u >> bit);
if (nibble != 0u) { mask |= bm; }
if (nibble & 1u) { p0 |= bm; }
if (nibble & 2u) { p1 |= bm; }
if (nibble & 4u) { p2 |= bm; }
if (nibble & 8u) { p3 |= bm; }
}
if (mask == 0u) {
continue;
}
/* Compute ST address for the dst byte at byte_col_dst.
* Plane bytes are interleaved word-wise inside groups of
* 16 pixels = 8 bytes (4 planes x 2 bytes). The left byte
* and the right-carry spill are guarded independently so
* a negative dstByteCol (sub-byte left clip) still lands
* its spill into on-surface byte dstByteCol+1. */
int16_t dstByteCol = (int16_t)(startByteX + byteCol);
{
uint8_t leftMask = (uint8_t)(mask >> shift);
if (leftMask != 0u
&& dstByteCol >= 0
&& dstByteCol < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t group = (uint16_t)(dstByteCol >> 1);
uint16_t half = (uint16_t)(dstByteCol & 1);
uint8_t notM = (uint8_t)(~leftMask);
uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 >> shift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 >> shift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 >> shift));
p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 >> shift));
}
if (shift != 0u) {
uint8_t rshift = (uint8_t)(8u - shift);
uint8_t rightMask = (uint8_t)(mask << rshift);
int16_t rdc = (int16_t)(dstByteCol + 1);
if (rightMask != 0u
&& rdc >= 0
&& rdc < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t rgroup = (uint16_t)(rdc >> 1);
uint16_t rhalf = (uint16_t)(rdc & 1);
uint8_t notM = (uint8_t)(~rightMask);
uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
}
}
}
}
rowBase += ST_BYTES_PER_ROW;
}
}
(void)col;
}
// Phase 10: hoist y*160 to per-row, fold setPixel/getPixel bodies
// inline. Each pixel's group address differs only in (x), so we
// can compute base+row*160 once per row and just do per-pixel
// (group, bitMask, 4 plane RMW). 2x speedup over the per-pixel
// stPlanarSetPixel form.
void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const StPlanarT *pd;
int16_t row;
int16_t pair;
int16_t pairs;
uint8_t *pp;
const uint8_t *rowBase;
if (s == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
pd = (const StPlanarT *)s->portData;
if (pd == NULL) {
return;
}
/* NATIVE-PERF Phase 1: group-aligned raw-span format. When x AND w
* are both 16-px multiples (the compiled save/restore window
* shape) each row is a CONTIGUOUS span of interleaved-group bytes,
* so the save is a straight per-row copy -- and, critically, the
* SAME byte format the compiled emitters read and write (backup
* format is a pure function of the recorded geometry; see
* spriteInternal.h). This branch must run BEFORE the byte-aligned
* branch: a group-aligned window previously took the byte-col asm,
* whose byte ORDER differs -- save and restore switch formats
* together, atomically, on the same predicate. */
if (((x | (int16_t)w) & 15) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
uint16_t spanBytes = (uint16_t)(w >> 1);
const uint8_t *src = pd->base + (uint16_t)y * ST_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP;
uint8_t *dst = dstPlaneBytes;
for (row = 0; row < (int16_t)h; row++) {
memcpy(dst, src, (size_t)spanBytes);
src += ST_BYTES_PER_ROW;
dst += spanBytes;
}
return;
}
/* Phase 10.5 fast path: byte-aligned, fully on-surface.
* Specialized 16x16 (the UBER ball-sprite size) skips the asm
* walker's per-row col-init + col-loop-check overhead. Group-
* aligned windows never reach here (raw-span above owns them);
* this serves the x % 16 == 8 byte-aligned case. */
if ((x & 7) == 0 && (w & 7) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
if (w == 16u && h == 16u) {
surface68kStSprite16x16Save(pd->base, (uint16_t)x, (uint16_t)y, dstPlaneBytes);
} else {
surface68kStSpriteSaveByteAligned(pd->base, (uint16_t)x, (uint16_t)y, w, h, dstPlaneBytes);
}
return;
}
pairs = (int16_t)(w >> 1);
rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
for (row = 0; row < (int16_t)h; row++) {
pp = &dstPlaneBytes[(uint16_t)row * (uint16_t)pairs];
for (pair = 0; pair < pairs; pair++) {
int16_t px;
uint16_t group;
uint16_t bitMask;
const uint16_t *pw;
uint8_t hi;
uint8_t lo;
px = (int16_t)(x + pair * 2);
group = (uint16_t)((uint16_t)px >> 4);
bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u)));
pw = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
hi = 0u;
if (pw[0] & bitMask) { hi = (uint8_t)(hi | 1u); }
if (pw[1] & bitMask) { hi = (uint8_t)(hi | 2u); }
if (pw[2] & bitMask) { hi = (uint8_t)(hi | 4u); }
if (pw[3] & bitMask) { hi = (uint8_t)(hi | 8u); }
px = (int16_t)(x + pair * 2 + 1);
group = (uint16_t)((uint16_t)px >> 4);
bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u)));
pw = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
lo = 0u;
if (pw[0] & bitMask) { lo = (uint8_t)(lo | 1u); }
if (pw[1] & bitMask) { lo = (uint8_t)(lo | 2u); }
if (pw[2] & bitMask) { lo = (uint8_t)(lo | 4u); }
if (pw[3] & bitMask) { lo = (uint8_t)(lo | 8u); }
pp[pair] = (uint8_t)((hi << 4) | lo);
}
rowBase += ST_BYTES_PER_ROW;
}
}
void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
StPlanarT *pd;
int16_t row;
int16_t pair;
int16_t pairs;
uint8_t b;
const uint8_t *pp;
uint8_t *rowBase;
if (s == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return;
}
/* Group-aligned raw-span restore: MUST mirror the save-side branch
* above exactly (same predicate, same byte order) -- see the save
* comment. A raw-span backup restored through the byte-col asm (or
* vice versa) silently scrambles planes. */
if (((x | (int16_t)w) & 15) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
uint16_t spanBytes = (uint16_t)(w >> 1);
uint8_t *dst = pd->base + (uint16_t)y * ST_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP;
const uint8_t *src = srcPlaneBytes;
for (row = 0; row < (int16_t)h; row++) {
memcpy(dst, src, (size_t)spanBytes);
dst += ST_BYTES_PER_ROW;
src += spanBytes;
}
return;
}
/* Phase 10.5 fast path: byte-aligned, fully on-surface.
* Specialized 16x16 (UBER ball-sprite) skips walker overhead.
* Serves only the x % 16 == 8 case (raw-span above owns group-
* aligned windows). */
if ((x & 7) == 0 && (w & 7) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
if (w == 16u && h == 16u) {
surface68kStSprite16x16Restore(pd->base, (uint16_t)x, (uint16_t)y, srcPlaneBytes);
} else {
surface68kStSpriteRestoreByteAligned(pd->base, (uint16_t)x, (uint16_t)y, w, h, srcPlaneBytes);
}
return;
}
pairs = (int16_t)(w >> 1);
rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
for (row = 0; row < (int16_t)h; row++) {
pp = &srcPlaneBytes[(uint16_t)row * (uint16_t)pairs];
for (pair = 0; pair < pairs; pair++) {
int16_t px;
uint16_t group;
uint16_t bitMask;
uint16_t notMask;
uint16_t *pw;
uint8_t color;
b = pp[pair];
px = (int16_t)(x + pair * 2);
color = (uint8_t)(b >> 4);
group = (uint16_t)((uint16_t)px >> 4);
bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u)));
notMask = (uint16_t)~bitMask;
pw = (uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
if (color & 1u) { pw[0] = (uint16_t)(pw[0] | bitMask); } else { pw[0] = (uint16_t)(pw[0] & notMask); }
if (color & 2u) { pw[1] = (uint16_t)(pw[1] | bitMask); } else { pw[1] = (uint16_t)(pw[1] & notMask); }
if (color & 4u) { pw[2] = (uint16_t)(pw[2] | bitMask); } else { pw[2] = (uint16_t)(pw[2] & notMask); }
if (color & 8u) { pw[3] = (uint16_t)(pw[3] | bitMask); } else { pw[3] = (uint16_t)(pw[3] & notMask); }
px = (int16_t)(x + pair * 2 + 1);
color = (uint8_t)(b & 0x0Fu);
group = (uint16_t)((uint16_t)px >> 4);
bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u)));
notMask = (uint16_t)~bitMask;
pw = (uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
if (color & 1u) { pw[0] = (uint16_t)(pw[0] | bitMask); } else { pw[0] = (uint16_t)(pw[0] & notMask); }
if (color & 2u) { pw[1] = (uint16_t)(pw[1] | bitMask); } else { pw[1] = (uint16_t)(pw[1] & notMask); }
if (color & 4u) { pw[2] = (uint16_t)(pw[2] | bitMask); } else { pw[2] = (uint16_t)(pw[2] & notMask); }
if (color & 8u) { pw[3] = (uint16_t)(pw[3] | bitMask); } else { pw[3] = (uint16_t)(pw[3] & notMask); }
}
rowBase += ST_BYTES_PER_ROW;
}
}
// ----- Flood-fill plane hooks (PERF-AUDIT #4) -----
//
// floodFillInternal's planar walk-out and row-scan tiers, modeled on
// the Amiga versions but against the word-interleaved layout. The
// semantics contract with the core C fallback (bit-for-bit):
// * walk: seed test, then extend left/right while pixels are
// fillable. matchEqual: fillable = (pix == matchColor); bounded:
// fillable = (pix != matchColor && pix != newColor).
// * scan: markBuf[i] = 1 where row pixel (leftX + i) is fillable,
// 0 otherwise, for i in [0 .. rightX - leftX].
// Both decode a 16-pixel group's 4 plane words ONCE into a stop/mark
// bit mask, then test single register bits per pixel -- and the walk
// skips a whole all-fillable group remainder in one step.
bool jlpFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
StPlanarT *pd;
const uint8_t *rowBase;
uint16_t seedGroup;
uint16_t seedStopBits;
uint16_t curGroup;
uint16_t stopBits;
uint16_t rem;
uint16_t g;
int16_t leftX;
int16_t rightX;
int16_t prevX;
int16_t nextX;
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return false;
}
matchColor = (uint8_t)(matchColor & 0x0Fu);
newColor = (uint8_t)(newColor & 0x0Fu);
rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
seedGroup = (uint16_t)((uint16_t)startX >> 4);
seedStopBits = stFloodStopBits(rowBase, seedGroup, matchColor, newColor, matchEqual);
if (seedStopBits & (uint16_t)(1u << (15u - ((uint16_t)startX & 15u)))) {
*seedMatched = false;
return true;
}
*seedMatched = true;
// Walk left. Pixel x sits at bit (15 - (x & 15)), so moving left
// means moving toward bit 15: shift the stop mask down so bit 0 is
// the candidate pixel, then consume clear bits upward.
leftX = startX;
curGroup = seedGroup;
stopBits = seedStopBits;
while (leftX > 0) {
prevX = (int16_t)(leftX - 1);
g = (uint16_t)((uint16_t)prevX >> 4);
if (g != curGroup) {
curGroup = g;
stopBits = stFloodStopBits(rowBase, g, matchColor, newColor, matchEqual);
}
rem = (uint16_t)(stopBits >> (15u - ((uint16_t)prevX & 15u)));
if (rem == 0u) {
// Everything from prevX to the group's first pixel is
// fillable: take the rest of the group in one step.
leftX = (int16_t)(g << 4);
continue;
}
while ((rem & 1u) == 0u) {
rem = (uint16_t)(rem >> 1);
leftX--;
}
break;
}
// Walk right: mirror image, shifting the stop mask up so bit 15 is
// the candidate pixel.
rightX = startX;
curGroup = seedGroup;
stopBits = seedStopBits;
while (rightX < SURFACE_WIDTH - 1) {
nextX = (int16_t)(rightX + 1);
g = (uint16_t)((uint16_t)nextX >> 4);
if (g != curGroup) {
curGroup = g;
stopBits = stFloodStopBits(rowBase, g, matchColor, newColor, matchEqual);
}
rem = (uint16_t)(stopBits << ((uint16_t)nextX & 15u));
if (rem == 0u) {
// Everything from nextX to the group's last pixel is
// fillable: take the rest of the group in one step.
rightX = (int16_t)((g << 4) + 15);
continue;
}
while ((rem & 0x8000u) == 0u) {
rem = (uint16_t)(rem << 1);
rightX++;
}
break;
}
*leftXOut = leftX;
*rightXOut = rightX;
return true;
}
bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
StPlanarT *pd;
const uint8_t *rowBase;
const uint16_t *gp;
uint8_t *mb;
uint16_t markBits;
uint16_t bit;
int16_t x;
int16_t groupEnd;
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return false;
}
matchColor = (uint8_t)(matchColor & 0x0Fu);
newColor = (uint8_t)(newColor & 0x0Fu);
rowBase = pd->base + (uint16_t)scanY * ST_BYTES_PER_ROW;
mb = markBuf;
x = leftX;
while (x <= rightX) {
gp = (const uint16_t *)(rowBase + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP);
if (matchEqual) {
markBits = stGroupEqBits(gp, matchColor);
} else {
markBits = (uint16_t)~(stGroupEqBits(gp, matchColor) | stGroupEqBits(gp, newColor));
}
groupEnd = (int16_t)((uint16_t)x | 15u);
if (groupEnd > rightX) {
groupEnd = rightX;
}
bit = (uint16_t)(1u << (15u - ((uint16_t)x & 15u)));
while (x <= groupEnd) {
*mb = (uint8_t)((markBits & bit) ? 1u : 0u);
mb++;
bit = (uint16_t)(bit >> 1);
x++;
}
}
return true;
}
// Phase 7: pixel reader. Pre-Phase-9 reads from the chunky shadow
// (s->pixels) since that's the source-of-truth during transition.
// Once Phase 9 sets s->pixels = NULL the planar shadow becomes
// authoritative and we walk the 4 plane bits at (x, y).
uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
if (s->pixels != NULL) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu);
return (uint8_t)((byte & 0xF0u) >> 4);
}
{
StPlanarT *pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return 0u;
}
return stPlanarGetPixel(pd, x, y);
}
}
// Derive 160 chunky bytes per row from the word-interleaved planar
// buffer (20 groups x 4 plane words). Same shape as the Amiga's
// amigaPlanesToChunkyRow but per-group instead of per-byte. Used by
// jlpSurfaceHash to fold the planar surface into the same byte stream
// the chunky ports hash, so cross-port hash comparisons stay valid.
static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) {
uint16_t group;
uint16_t p;
uint16_t bitMask;
uint8_t pix;
const uint16_t *gp;
for (group = 0; group < ST_GROUPS_PER_ROW; group++) {
gp = (const uint16_t *)(pd->base
+ (uint16_t)y * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP);
for (p = 0; p < 16u; p++) {
bitMask = (uint16_t)(1u << (15u - p));
pix = 0u;
if (gp[0] & bitMask) { pix = (uint8_t)(pix | 1u); }
if (gp[1] & bitMask) { pix = (uint8_t)(pix | 2u); }
if (gp[2] & bitMask) { pix = (uint8_t)(pix | 4u); }
if (gp[3] & bitMask) { pix = (uint8_t)(pix | 8u); }
if ((p & 1u) == 0u) {
dstChunkyRow[group * 8u + (p >> 1)] = (uint8_t)(pix << 4);
} else {
dstChunkyRow[group * 8u + (p >> 1)] = (uint8_t)(dstChunkyRow[group * 8u + (p >> 1)] | pix);
}
}
}
}
uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
StPlanarT *pd;
uint16_t lo = 0xACE1u;
uint16_t hi = 0x1357u;
uint16_t n;
uint16_t v;
int16_t row;
uint16_t col;
uint8_t b;
uint8_t chunkyRow[SURFACE_BYTES_PER_ROW];
const uint16_t *w;
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return 0u;
}
/* Pixel hash: derive equivalent chunky bytes from the planar
* shadow row by row, fold them into the same SURFACE_HASH_MIX_BYTE
* the chunky ports use so cross-port hash comparisons stay valid. */
for (row = 0; row < SURFACE_HEIGHT; row++) {
stPlanarToChunkyRow(pd, row, chunkyRow);
for (col = 0; col < SURFACE_BYTES_PER_ROW; col++) {
b = chunkyRow[col];
SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
}
/* SCB + palette mix unchanged from chunky days. */
{
const uint8_t *sp = s->scb;
for (n = 0; n < (uint16_t)SURFACE_HEIGHT; n++) {
b = *sp++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
}
w = &s->palette[0][0];
for (n = 0; n < (uint16_t)SURFACE_PALETTE_ENTRIES; n++) {
v = *w++;
b = (uint8_t)((v >> 8) & 0xFFu); SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = (uint8_t)(v & 0xFFu); SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
return ((uint32_t)hi << 16) | (uint32_t)lo;
}
// On-disk format is the ST's native interleaved planar buffer; one
// fread fills it directly, no chunky scratch or c2p step.
bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
StPlanarT *pd;
pd = (StPlanarT *)dst->portData;
if (pd == NULL) {
return false;
}
return fread(pd->base, 1, ST_PLANAR_SIZE, fp) == ST_PLANAR_SIZE;
}
bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
StPlanarT *pd;
pd = (StPlanarT *)src->portData;
if (pd == NULL) {
return false;
}
return fwrite(pd->base, 1, ST_PLANAR_SIZE, fp) == ST_PLANAR_SIZE;
}
// Phase 9: no chunky storage on the ST. Cross-platform code treats
// NULL as "port has no chunky shadow" (same contract Amiga uses).
uint8_t *jlpSurfaceAllocPixels(void) {
return NULL;
}
// Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc
// stores NULL in s->pixels and skips the chunky memset.
uint8_t *jlpStageAllocPixels(void) {
return NULL;
}