More optimizations. New indexed-to-display conversion routines.

This commit is contained in:
Scott Duensing 2026-10-17 19:45:09 -05:00
parent 423f992583
commit 154b116208
51 changed files with 2916 additions and 164 deletions

View file

@ -417,15 +417,6 @@ above.
---
### libxmp-lite 4.7.0
- **Used on:** none. It played modules on DOS, the ST, the X68000 and the
hosted ports until 2026-10-14, when JoeyLib's own module player replaced
it; the installer still installs it (component `libxmp`).
- **URL:** <https://github.com/libxmp/libxmp>
- **License:** MIT
- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
## Content tools (optional; not shipped, not linked)
### Furnace 0.6.8.3

View file

@ -38,7 +38,7 @@ and the PSP, from a single codebase:
The Apple IIgs defines the capability ceiling. Stronger platforms coast.
Hot paths are hand-written assembly per port; the public API is C.
See `claudeDocs/DESIGN.md` for the full 1.0 design.
The manual (`docs/`, built by `make docs`) documents the whole API.
## Quick start

View file

@ -6,8 +6,8 @@ There are three layers, and on some platforms they share physical hardware:
* *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
voices plus noise at register-write cost, with no CPU mixing. The recommended
music path.
voices plus noise at register-write cost on the machines' sound chips (DVX
and the hosted ports mix them in software). The recommended music path.
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and
the streaming forms.
@ -412,10 +412,11 @@ fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; and DVX and the
hosted ports mix the YM2149's own noise in software (its 17-bit generator),
so it sounds as on the ST (DVX with only the PC speaker: a noise tone of its
sound library). DOS uses the OPL2
rhythm-mode snare, leaving the three tone channels untouched: a tuned drum,
not free-running noise, whose pitch spreads over the snare's range rather than
following the YM2149's rate.
sound library). DOS mixes the same generator into the Sound Blaster's
stream, at about 22 kHz, so pitches 0 to 5 sound alike there; an AdLib with
no Sound Blaster plays the OPL2 rhythm-mode snare instead, leaving the three
tone channels untouched: a tuned drum, not free-running noise, whose pitch
spreads over the snare's range rather than following the YM2149's rate.
[WARNING]
====
@ -432,9 +433,10 @@ bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
----
Plays a JYM1 event stream baked by `songbake` through the tone and noise layer.
One authored song plays on every port at register-write cost. Because there
is no CPU mixing, this is fully viable on the ST and cheap on DOS, which
`.MOD` playback is not.
One authored song plays on every port, at register-write cost wherever a
sound chip makes it (DVX and the hosted ports mix it in software, and DOS
mixes its noise into the Sound Blaster's stream), so it is fully viable on
the ST and cheap on DOS, which `.MOD` playback is not.
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so
tempo holds across 50, 60, and 70 Hz ports.

View file

@ -234,10 +234,11 @@ uint16_t jlFrameHz(void);
*Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a
PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that
cannot show PAL; 50 or 60 on the ST, as the machine is set; 55 on the X68000;
70 on VGA mode 13h and on DVX, whose window has a virtual 70 Hz (and whose full
screen is mode 13h's, or a VBE mode's, with the same frame count); and 60 on
every hosted port, whose window has a virtual 60 Hz. The X68000 times its
cannot show PAL; 50 or 60 on the ST, as the machine is set (71 with a
monochrome monitor); 55 on the X68000; 70 on DOS's VGA and on DVX, whose
window has a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE
mode's, with the same frame count); and 60 on every hosted port, whose window
has a virtual 60 Hz. The X68000 times its
display at `jlInit` and rounds to the nearest whole number: 55.46 Hz on the
machine, 56.07 Hz under MAME, which therefore reports 56. The real cadence may
drift slightly; this is the figure to divide by when converting frames to

View file

@ -139,6 +139,77 @@ Erase *all* regions, then draw *all* sprites. Never mix this with per-sprite
save and restore in the same region.
====
=== jlSurfacePasteIndexed
[source,c]
----
void jlSurfacePasteIndexed(jlSurfaceT *dst, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
----
Writes a rectangle of colour indices from your memory onto a surface: one byte
a pixel, whatever the port stores pixels as. Row `r` of the source starts at
`src + r * srcStride`. Each byte's low nibble is the colour index; the high
nibble is ignored, so a byte array of 0..15 with flags in the top bits pastes
as it is.
With `doubleWide` each byte fills two pixels side by side, so `w` bytes cover
`2 * w` pixels: a 160-wide picture fills the 320-wide surface, the way Sierra's
AGI draws.
The rectangle is clipped to the surface pixel by pixel (a doubled byte cut by
an edge shows its one pixel there). Only the pasted pixels change and are
marked for the next `jlStagePresent`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`x`, `y` |`int16_t` |Top-left pixel on the surface.
|`w`, `h` |`uint16_t` |Bytes a row and rows.
|`src` |`const uint8_t *` |The colour indices, row by row.
|`srcStride` |`uint16_t` |Bytes from one source row to the next.
|`doubleWide` |`bool` |Each byte fills two pixels.
|===
*Returns:* nothing.
[source,c]
----
// A 160x168 AGI picture, doubled to 320 wide, below an 8-row status line.
jlSurfacePasteIndexed(jlStageGet(), 0, 8, 160, 168, picture, 160, true);
----
Every port has a fast path: asm on the IIgs (two pixel pairs a 16-bit
operation) and on the planar ports (table-driven chunky-to-planar, eight
pixels a step), four bytes a step in C on the chunky ports.
=== jlSurfaceSnapIndexed
[source,c]
----
void jlSurfaceSnapIndexed(const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
----
The reverse of `jlSurfacePasteIndexed`: reads a rectangle of a surface into your
memory as colour indices, one byte a pixel (0..15), row `r` at
`dst + r * dstStride`. With `doubleWide` the rectangle is `2 * w` pixels wide
and each byte takes the left pixel of its pair. A byte whose pixel is off the
surface reads 0, as `jlSamplePixel` does. Bytes between `w` and `dstStride` are
left alone.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`src` |`const jlSurfaceT *` |Source surface.
|`x`, `y` |`int16_t` |Top-left pixel on the surface.
|`w`, `h` |`uint16_t` |Bytes a row and rows.
|`dst` |`uint8_t *` |Where the colour indices go, row by row.
|`dstStride` |`uint16_t` |Bytes from one destination row to the next.
|`doubleWide` |`bool` |Read every other pixel.
|===
*Returns:* nothing.
=== jlSurfaceSaveFile
[source,c]

View file

@ -69,8 +69,8 @@
|`surface.h`
|`jlSurfaceCreate`, `jlSurfaceDestroy`, `jlStageGet`, `jlSurfaceCopy`,
`jlSurfaceCopyRect`, `jlSurfaceSaveFile`, `jlSurfaceLoadFile`,
`jlSurfaceHash`
`jlSurfaceCopyRect`, `jlSurfacePasteIndexed`, `jlSurfaceSnapIndexed`,
`jlSurfaceSaveFile`, `jlSurfaceLoadFile`, `jlSurfaceHash`
|`present.h`
|`jlStagePresent`
@ -86,7 +86,8 @@
|`tile.h`
|`jlTileCopy`, `jlTileCopyMasked`, `jlTileFill`, `jlTileBankLoad`,
`jlTileBankLoadToSurface`, `jlTileSnap`, `jlTilePaste`, `jlTilePasteMono`,
`jlTilePasteGlyph`, `jlDrawText`, `jlTileMapPaste`
`jlTilePasteGlyph`, `jlDrawText`, `jlTileMapPaste`, `jlTileLayerCreate`,
`jlTileLayerDestroy`, `jlTileLayerDraw`, `jlTileLayerSetTile`
|`sprite.h`
|`jlSpriteBankLoad`, `jlSpriteBankLoadPrecompiled`,

View file

@ -243,8 +243,8 @@ one-time startup cost for a much smaller distribution.
=== Music: the Portable Chip Tracker
This is the recommended music path. One authored song plays on every port
at register-write cost, with no CPU mixing -- which matters enormously on the
ST, where `.MOD` playback is only marginally viable.
at register-write cost on the machines' sound chips -- which matters
enormously on the ST, where `.MOD` playback is only marginally viable.
Songs are plain text, one row per line:

View file

@ -58,7 +58,7 @@ directory -- the paths in the Makefile are relative to it -- or with `make -C`:
[source,console]
----
$ make # every target whose toolchain is installed
$ make dos # one target: iigs amiga atarist ste dos x68000 dvx
$ make dos # one target (any PLATFORM name, below)
$ make clean # the project's build/ (clean-dos: one target's)
$ make help
----

View file

@ -182,7 +182,8 @@ hardware:
|Chip tracker
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
noise, at register-write cost with no CPU mixing.
noise, at register-write cost on the machines' sound chips (mixed in
software on DVX and the hosted ports).
|Voices and effects
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms.

View file

@ -583,6 +583,38 @@ static void settleLayer(void) {
}
// Indexed rows: an AGI picture, 160x168 color indices each doubled to two
// pixels, pushed below an 8-row status line, and snapped back. The source
// bytes carry high nibbles the paste must ignore.
#define UBER_INDEXED_W 160u
#define UBER_INDEXED_H 168u
#define UBER_INDEXED_Y 8
static uint8_t *gIndexed = NULL;
static void indexedData(void) {
uint16_t x;
uint16_t y;
for (y = 0; y < UBER_INDEXED_H; y++) {
for (x = 0; x < UBER_INDEXED_W; x++) {
gIndexed[y * UBER_INDEXED_W + x] = (uint8_t)(x * 7u + y * 13u + (x ^ y));
}
}
}
static void op_indexedPaste(void) {
jlSurfacePasteIndexed(jlgStage, 0, UBER_INDEXED_Y, UBER_INDEXED_W, UBER_INDEXED_H, gIndexed, UBER_INDEXED_W, true);
}
static void op_indexedSnap(void) {
jlSurfaceSnapIndexed(jlgStage, 0, UBER_INDEXED_Y, UBER_INDEXED_W, UBER_INDEXED_H, gIndexed, UBER_INDEXED_W, true);
}
#ifdef UBER_PROBE
// NATIVE-PERF Phase 6 W0 probe rows. NOT part of the golden contract:
// this block only exists in the scratch probe binary (-DUBER_PROBE,
@ -2053,6 +2085,157 @@ static void __attribute__((noinline)) checkFillRectRows(void) {
}
// jlSurfacePasteIndexed and jlSurfaceSnapIndexed: rectangles single and
// doubled, at odd and even columns, cut by every edge (a doubled byte
// showing its one pixel at column 0 or 319), from a source whose high
// nibbles are set -- pasted on the stage and on another surface, then each
// spot snapped back from both (off-surface bytes read 0). One hash folds
// both surfaces and every snapped row whole, the stride padding the snap
// must leave alone included. (checkIndexedSweep checks the pixels one by
// one.)
#define UBER_CHK_INDEXED_W 24u
#define UBER_CHK_INDEXED_H 12u
#define UBER_CHK_INDEXED_STRIDE 27u
#define UBER_CHK_INDEXED_SPOTS 12u
typedef struct UberIndexedSpotT {
int16_t x;
int16_t y;
bool wide;
} UberIndexedSpotT;
static const UberIndexedSpotT kIndexedSpots[UBER_CHK_INDEXED_SPOTS] = {
{ 0, 0, false },
{ 33, 14, false },
{ -5, 30, false },
{ 301, 44, false },
{ 2, 60, true },
{ 77, 75, true },
{ -7, 90, true },
{ 297, 105, true },
{ 140, 194, true },
{ 200, -6, false },
{ -30, 150, true },
{ 319, 170, false }
};
static uint8_t gChkIndexedSrc[UBER_CHK_INDEXED_STRIDE * UBER_CHK_INDEXED_H];
static uint8_t gChkIndexedSnap[UBER_CHK_INDEXED_STRIDE * UBER_CHK_INDEXED_H];
static uint32_t indexedFold(uint32_t h, const jlSurfaceT *s) {
const UberIndexedSpotT *spot;
uint16_t i;
uint16_t n;
for (n = 0; n < UBER_CHK_INDEXED_SPOTS; n++) {
spot = &kIndexedSpots[n];
jlSurfaceSnapIndexed(s, spot->x, spot->y, UBER_CHK_INDEXED_W, UBER_CHK_INDEXED_H, gChkIndexedSnap, UBER_CHK_INDEXED_STRIDE, spot->wide);
for (i = 0; i < UBER_CHK_INDEXED_STRIDE * UBER_CHK_INDEXED_H; i++) {
h = h * 31u + gChkIndexedSnap[i];
}
}
return h;
}
static void __attribute__((noinline)) checkIndexed(void) {
const UberIndexedSpotT *spot;
jlSurfaceT *other;
uint32_t h;
uint16_t i;
uint16_t n;
uberMbOp(124);
for (i = 0; i < UBER_CHK_INDEXED_STRIDE * UBER_CHK_INDEXED_H; i++) {
gChkIndexedSrc[i] = (uint8_t)(i * 37u + (i >> 3));
}
other = jlSurfaceCreate();
if (other == NULL) {
chkPassFail("indexed (surface alloc)", false);
return;
}
jlSurfaceClear(jlgStage, 4);
jlSurfaceClear(other, 11);
for (n = 0; n < UBER_CHK_INDEXED_SPOTS; n++) {
spot = &kIndexedSpots[n];
jlSurfacePasteIndexed(jlgStage, spot->x, spot->y, UBER_CHK_INDEXED_W, UBER_CHK_INDEXED_H, gChkIndexedSrc, UBER_CHK_INDEXED_STRIDE, spot->wide);
jlSurfacePasteIndexed(other, (int16_t)(spot->x + 1), spot->y, UBER_CHK_INDEXED_W, UBER_CHK_INDEXED_H, gChkIndexedSrc, UBER_CHK_INDEXED_STRIDE, spot->wide);
}
h = jlSurfaceHash(jlgStage);
h = h * 31u + jlSurfaceHash(other);
h = indexedFold(h, jlgStage);
h = indexedFold(h, other);
jlSurfaceDestroy(other);
jlLogF("UBER-CHK: indexed: hash=%08lX", (unsigned long)h);
}
// jlSurfacePasteIndexed pixel by pixel on the target, where the planar
// ports' asm runs: rectangles 1-20 bytes wide at all 16 pixel phases (both
// column parities, so heads, pairs, and tails), single and doubled, two
// rows on a cleared band; every pixel of the band from a pixel left of the
// rectangle to one right of it checked, then the rectangle snapped back.
// The hash is the number of wrong pixels and bytes: 0.
#define UBER_ISWEEP_Y 100
#define UBER_ISWEEP_H 2u
#define UBER_ISWEEP_X0 64
#define UBER_ISWEEP_PHASES 16
#define UBER_ISWEEP_MAX_W 20u
#define UBER_ISWEEP_BG 5u
static uint32_t indexedSweepCase(int16_t x, uint16_t w, bool wide) {
uint32_t bad;
int16_t px;
int16_t right;
uint16_t r;
uint16_t i;
uint8_t want;
bad = 0u;
right = (int16_t)(x + (int16_t)(w << (wide ? 1u : 0u)));
jlFillRect(jlgStage, 0, UBER_ISWEEP_Y, JL_SURFACE_WIDTH, UBER_ISWEEP_H, UBER_ISWEEP_BG);
jlSurfacePasteIndexed(jlgStage, x, UBER_ISWEEP_Y, w, UBER_ISWEEP_H, gChkIndexedSrc, UBER_CHK_INDEXED_STRIDE, wide);
for (r = 0; r < UBER_ISWEEP_H; r++) {
for (px = (int16_t)(x - 1); px <= right; px++) {
want = UBER_ISWEEP_BG;
if (px >= x && px < right) {
want = (uint8_t)(gChkIndexedSrc[r * UBER_CHK_INDEXED_STRIDE + ((uint16_t)(px - x) >> (wide ? 1u : 0u))] & 0x0Fu);
}
if (jlSamplePixel(jlgStage, px, (int16_t)(UBER_ISWEEP_Y + r)) != want) {
bad++;
}
}
}
jlSurfaceSnapIndexed(jlgStage, x, UBER_ISWEEP_Y, w, UBER_ISWEEP_H, gChkIndexedSnap, UBER_CHK_INDEXED_STRIDE, wide);
for (r = 0; r < UBER_ISWEEP_H; r++) {
for (i = 0; i < w; i++) {
if (gChkIndexedSnap[r * UBER_CHK_INDEXED_STRIDE + i] != (gChkIndexedSrc[r * UBER_CHK_INDEXED_STRIDE + i] & 0x0Fu)) {
bad++;
}
}
}
return bad;
}
static void __attribute__((noinline)) checkIndexedSweep(void) {
uint32_t bad;
int16_t phase;
uint16_t w;
uberMbOp(125);
bad = 0u;
for (phase = 0; phase < UBER_ISWEEP_PHASES; phase++) {
for (w = 1u; w <= UBER_ISWEEP_MAX_W; w++) {
bad += indexedSweepCase((int16_t)(UBER_ISWEEP_X0 + phase), w, false);
bad += indexedSweepCase((int16_t)(UBER_ISWEEP_X0 + phase), w, true);
}
}
jlLogF("UBER-CHK: indexed sweep: hash=%08lX", (unsigned long)bad);
}
static void __attribute__((noinline)) runCorrectnessChecks(void) {
uberMbPhase(UBER_PHASE_CHECKS);
jlLogF("UBER-CHK: ----- begin -----");
@ -2068,6 +2251,8 @@ static void __attribute__((noinline)) runCorrectnessChecks(void) {
checkTileLayer();
checkTilePasteGlyph();
checkFillRectRows();
checkIndexed();
checkIndexedSweep();
// jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown
// of the whole library mid-run; it gets a dedicated micro-example in
// Phase 1 rather than risking the benchmark's display state here.
@ -2209,6 +2394,19 @@ static void __attribute__((noinline)) runAllTests(void) {
jlLogF("UBER: jlTileLayerDraw scroll: SKIPPED (layer alloc failed)");
}
// Indexed rows (appended last per the cumulative-hash safety rule). The
// snap reads back what the paste wrote, so it leaves the stage as is.
gIndexed = (uint8_t *)jlAlloc((uint32_t)UBER_INDEXED_W * UBER_INDEXED_H);
if (gIndexed != NULL) {
indexedData();
timeOp("jlSurfacePasteIndexed 160x168 wide", op_indexedPaste);
timeOp("jlSurfaceSnapIndexed 160x168 wide", op_indexedSnap);
jlFree(gIndexed);
gIndexed = NULL;
} else {
jlLogF("UBER: jlSurfacePasteIndexed 160x168 wide: SKIPPED (buffer alloc failed)");
}
#ifdef UBER_PROBE
// The presents count from here (the IIgs and hosted ones only while
// asked).

View file

@ -258,10 +258,11 @@ void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// channel C. Noise shares channel C's volume register
// with tone voice 2: author noise and voice-2 notes
// apart (songbake warns on overlap).
// DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise
// source) on channel 7; the three tone channels are
// untouched. A tuned drum: its pitch spreads over the
// snare's range instead of following the rate above.
// DOS -- the YM2149's 17-bit generator, mixed into the Sound
// Blaster's stream (~22 kHz, so pitches 0-5 sound
// alike). An OPL2 without a Sound Blaster plays its
// rhythm-mode snare drum instead, a tuned drum whose
// pitch spreads over the snare's range.
// Amiga -- Paula channel 3 looping a chip-RAM LFSR noise sample
// (channels 0-2 are the tone voices).
// IIgs -- a resident LFSR noise sample on SFX slot 3.
@ -276,9 +277,10 @@ void jlAudioNoise(uint8_t pitch, uint8_t atten);
// Portable chip-tracker music: play a JYM1 event stream (baked from
// tools/songbake text notation) through the jlAudioVoice tone layer.
// One authored song plays on every port at register-write cost --
// there is no CPU mixing, so unlike jlAudioPlayMod this path is fully
// viable on the ST and cheap on DOS. The stream is read IN PLACE:
// One authored song plays on every port at register-write cost on the
// retro machines' chips (DOS's noise and the DVX and hosted ports mix in
// software), so unlike jlAudioPlayMod this path is fully viable on the
// ST and cheap on DOS. The stream is read IN PLACE:
// data must stay valid until jlMusicStop or song end. Playback is
// pumped by jlAudioFrameTick (call it once per frame, as usual) and
// paced by jlMillisElapsed, so tempo holds across 50/60/70 Hz ports.

View file

@ -93,8 +93,8 @@ uint16_t jlFrameCount(void);
// Display frame rate in Hz: 60 on the IIgs (50 on a PAL machine), 50 on
// the Amiga (60 on an NTSC machine that cannot show PAL), 50 or 60 on
// the ST as the machine is set, 70 on DOS and DVX, 55 on the X68000 (56
// under MAME), and 60 on every hosted port. The actual VBL cadence may
// the ST as the machine is set (71 with a monochrome monitor), 70 on DOS
// and DVX, 55 on the X68000 (56 under MAME), and 60 on every hosted port. The actual VBL cadence may
// drift slightly; the value reported here is what benchmarks divide
// by to convert iters-per-N-frames to ops/sec.
uint16_t jlFrameHz(void);

View file

@ -8,8 +8,8 @@
// The library forces color 0 of every palette to black; jlPaletteSet
// silently masks that entry to $000 regardless of what the caller
// passed, and jlSurfaceLoadFile does the same. Palette and SCB changes
// to the stage reach the display at the next jlStagePresent. See
// claudeDocs/DESIGN.md sections 5 and 10.
// to the stage reach the display at the next jlStagePresent. See the
// manual's "Palette and Scanline Control" (docs/ApiPalette.adoc).
#ifndef JOEYLIB_PALETTE_H
#define JOEYLIB_PALETTE_H

View file

@ -193,6 +193,8 @@
#define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: jlIigsCopyRectRows
// [dp],y row copies (short rows lose to
// memcpy's per-call setup), function
#define JL_HAS_SURFACE_PASTE_INDEXED // whole bytes in asm (iigsIndexed.s), function
#define JL_HAS_SURFACE_SNAP_INDEXED // whole bytes in asm (iigsIndexed.s), function
#define JL_HAS_TILE_LAYER_BLIT // the generic rings, an unrolled stage blit (iigsTileLayer.c)
#define JL_HAS_TILE_LAYER_PUT // the generic rings, an asm put (iigsTileLayer.c)
#define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro
@ -280,6 +282,8 @@
#define JL_HAS_SURFACE_COPY_RECT // amiga per-plane rect row copy, function
#define JL_HAS_SAMPLE_PIXEL // planarSamplePixel, inline (amigaTile.h)
#define JL_HAS_SURFACE_HASH // amiga planar hash, function
#define JL_HAS_SURFACE_PASTE_INDEXED // the shared planar paste (src/m68k/planarIndexed.c), function
#define JL_HAS_SURFACE_SNAP_INDEXED // the shared planar snap (src/m68k/planarIndexed.c), function
#define JL_HAS_SURFACE_LOAD_FILE // amiga planar load, function
#define JL_HAS_SURFACE_SAVE_FILE // amiga planar save, function
#define JL_HAS_STAGE_ALLOC_PIXELS // amiga: NULL (no chunky stage shadow), function
@ -364,6 +368,8 @@
#define JL_HAS_SURFACE_COPY_RECT // st interleaved rect row memcpy, function
#define JL_HAS_SAMPLE_PIXEL // stSamplePixel, inline (stTile.h)
#define JL_HAS_SURFACE_HASH // st planar hash, function
#define JL_HAS_SURFACE_PASTE_INDEXED // the shared planar paste (src/m68k/planarIndexed.c), function
#define JL_HAS_SURFACE_SNAP_INDEXED // the shared planar snap (src/m68k/planarIndexed.c), function
#define JL_HAS_SURFACE_LOAD_FILE // st planar load, function
#define JL_HAS_SURFACE_SAVE_FILE // st planar save, function
#define JL_HAS_STAGE_ALLOC_PIXELS // st: NULL (no chunky stage shadow), function
@ -506,6 +512,8 @@
#define JL_HAS_SURFACE_COPY_RECT
#define JL_HAS_SAMPLE_PIXEL
#define JL_HAS_SURFACE_HASH
#define JL_HAS_SURFACE_PASTE_INDEXED
#define JL_HAS_SURFACE_SNAP_INDEXED
#define JL_HAS_SURFACE_LOAD_FILE
#define JL_HAS_SURFACE_SAVE_FILE
#define JL_HAS_STAGE_ALLOC_PIXELS

View file

@ -12,12 +12,12 @@
// each row is 4 bytes (8 pixels at 4bpp packed; high nibble = left
// pixel).
//
// Color 0 is always transparent on draw (DESIGN.md contract). Use a
// Color 0 is always transparent on draw (a library contract). Use a
// tile-block draw if you need an opaque rectangle.
//
// Performance contract: jlSpriteDraw should be at least as fast as the
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
// code per CPU). The machine ports have runtime emitters (IIgs 65816,
// IIgs reference (the design calls for runtime-compiled draw code per
// CPU). The machine ports have runtime emitters (IIgs 65816,
// Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does
// Win16 (x86, as DOS);
// jlSpriteCompile compiles the shift/op variants into the codegen arena

View file

@ -13,7 +13,8 @@
// does not see the storage format.
//
// Each surface carries its own 200-entry SCB (scanline control byte)
// table and a 16-by-16 $0RGB palette set. See claudeDocs/DESIGN.md section 6.
// table and a 16-by-16 $0RGB palette set. See the manual's "Surfaces and
// Presenting" (docs/ApiSurface.adoc).
#ifndef JOEYLIB_SURFACE_H
#define JOEYLIB_SURFACE_H
@ -72,6 +73,23 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
// same region.
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h);
// Write a rectangle of color indices onto a surface at (x, y): src holds
// h rows of w bytes, row r starting at src + r * srcStride, one pixel's
// color index in each (0..15; the high nibble is ignored). With doubleWide
// each byte fills two pixels side by side, so the rectangle is 2w pixels
// wide on the surface (a 160-wide image on the 320-wide screen). Clipped
// to the surface, pixel by pixel. Whatever a port stores pixels as, the
// same bytes give the same pixels on every port.
void jlSurfacePasteIndexed(jlSurfaceT *dst, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
// The reverse of jlSurfacePasteIndexed: read a rectangle of a surface at
// (x, y) into dst as color indices, h rows of w bytes, row r starting at
// dst + r * dstStride, one pixel's color index (0..15) in each. With
// doubleWide the rectangle is 2w pixels wide on the surface and each byte
// takes the left pixel of its pair. A byte whose pixel is off the surface
// reads 0, as jlSamplePixel does.
void jlSurfaceSnapIndexed(const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
// Persist a surface to disk in target-native form: 32000 bytes of
// pixels followed by 200 bytes of SCB followed by 512 bytes of $0RGB
// palette table (16 palettes x 16 colors, native uint16_t). Returns

View file

@ -1,42 +0,0 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP,
# web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14,
# when JoeyLib's own module player (src/core/modPlayer.c) replaced it. No
# build uses it now; it stays a reference to compare that player against,
# installed only when asked for by name (install libxmp), so no toolchain
# fetches it.
compDescription="audio: libxmp-lite 4.7.0 source (optional; no build links it; a reference for the module player)"
compGroups="optional"
compRequires="host"
compHostDeps="tar"
compUrl="https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.0/libxmp-lite-4.7.0.tar.gz"
compSha256="69967a7802d2d2f938347f6f9cd95232bf45a4623cbfcc196e37cdaefadc3974"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -f "$compDir/include/libxmp-lite/xmp.h" ]
}
compVerify() {
[ -d "$compDir/src" ]
}
compEnv() {
envVar LIBXMP_DIR "$compDir"
}

View file

@ -55,6 +55,7 @@ LIB_OBJS := \
$(BUILD)/obj/port/ptplayer.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/68k/planarHash.o \
$(BUILD)/obj/68k/planarIndexed.o \
$(BUILD)/obj/68k/planarSprite.o \
$(BUILD)/obj/68k/spriteC2p.o \
$(BUILD)/obj/68k/planarTileLayer.o \

View file

@ -54,6 +54,7 @@ LIB_OBJS := \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/68k/spriteEmitInterleaved68k.o \
$(BUILD)/obj/68k/planarHash.o \
$(BUILD)/obj/68k/planarIndexed.o \
$(BUILD)/obj/68k/planarSprite.o \
$(BUILD)/obj/68k/spriteC2p.o \
$(BUILD)/obj/codegen/spriteCompile.o \

View file

@ -9,7 +9,7 @@
REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..)
# Tool paths (AMIGA_CC, LLVM816_ROOT, LIBXMP_DIR, ...) come from the file
# Tool paths (AMIGA_CC, LLVM816_ROOT, X68K_ROOT, ...) come from the file
# install.sh generates; it is the only place they are defined.
-include $(REPO_DIR)/toolchains/config.mk

View file

@ -10,13 +10,11 @@
# bank placement is handled by -ffunction-sections + link816
# dead-stripping.
#
# STATUS: the launchable-binary pipeline (multi-segment OMF emission +
# disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
# compile-checks every library + example translation unit, and
# `iigs-clang-smoke` (not part of the default goal: it needs MAME)
# validates the full clang -> llvm-mc -> link816 -> MAME path end to end. Per-example binary targets build via
# clang-build.sh; large examples need the multi-segment OMF wiring still
# in progress.
# `iigs` (and `iigs-lib`) builds the library alone; the examples come from
# `iigs-examples`, or `iigs-disk`, which also packs them on a volume.
# `iigs-clang-smoke` (not part of the default goal: it needs MAME) checks
# the clang -> llvm-mc -> link816 -> MAME path end to end. Programs are
# multi-segment OMF from clang-build.sh.
#
# This file holds JoeyLib's library for the IIgs, programRules, the IIgs
# rules for one program, which make/programs.mk instantiates for every

View file

@ -20,7 +20,7 @@
#
# make every port the programs build for whose toolchain
# is installed (ports without one are noted, skipped)
# make <port> one port: iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp
# make <port> one port: any of JOEY_PORTS (make/common.mk)
# make <port>-<goal> one of that port's goals: iigs-game-disk GAME=NAME,
# iigs-verify-game-disk, iigs-game-disks
# make JOEY_PORT=<port> <target>

View file

@ -62,8 +62,10 @@ LIB_OBJS := \
$(BUILD)/obj/68k/sfxMix.o \
$(BUILD)/obj/68k/planarSpriteWalk.o \
$(BUILD)/obj/68k/planarLayerBlit.o \
$(BUILD)/obj/68k/planarIndexedColumns.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/68k/planarHash.o \
$(BUILD)/obj/68k/planarIndexed.o \
$(BUILD)/obj/68k/planarSprite.o \
$(BUILD)/obj/68k/spriteC2p.o \
$(BUILD)/obj/68k/planarTileLayer.o \

36
scripts/check-indexed.sh Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-indexed.sh - Host gate for jlSurfacePasteIndexed and
# jlSurfaceSnapIndexed: the BLANK port built with the host cc, and
# tests/host/indexedHost.c pasting and snapping edge-case and seeded random
# rectangles (odd columns, both edges, doubleWide bytes cut by an edge, rows
# off both ends, the int16_t limits), each checked against a reference made
# one pixel at a time with jlDrawPixel and jlSamplePixel; then the planar
# ports' paste and snap (src/m68k/planarIndexed.c) on host buffers in both
# planar layouts, separate and ST-interleaved.
#
# Usage: scripts/check-indexed.sh
# Exit: 0 PASS, 1 FAIL
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-indexed.XXXXXX)
trap 'rm -rf "$work"' EXIT
cd "$repo"
"$cc" -O2 -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Isrc/m68k -Wall -Wextra -Werror \
src/core/*.c src/generic/*.c src/blank/blank.c \
src/m68k/planarIndexed.c src/m68k/planarHash.c tests/host/indexedHost.c \
-lm -o "$work/indexedHost"
if "$work/indexedHost"; then
echo "check-indexed: PASS (indexed paste and snap match the pixel-by-pixel reference, clipped at every edge, chunky and both planar layouts)"
else
echo "check-indexed: FAIL"
exit 1
fi

View file

@ -10,14 +10,15 @@
# Kinds, in the order `all` runs them:
# host the shared layer on the host, no emulator: the new-port
# template (blank), file roots, the dirty-band marker, the line
# and circle fallbacks at the far ends of their coordinates, input,
# and circle fallbacks at the far ends of their coordinates, the
# indexed paste and snap against pixel-by-pixel ones, input,
# millis, the module player the software-mixing ports share,
# save, serial, sfxmix, tilebank, tile layers against their maps,
# the Tiled map importer, and the VGM converter (with Furnace
# installed, on its demo songs too)
# golden UBER on each port, its 54 per-op surface hashes against
# golden UBER on each port, its per-op surface hashes against
# tests/goldens/uber (x68000 against the IIgs's, its own gate;
# dvx, sdl3, web, win32, win16, psp, haiku, haiku32 and beos
# dvx, sdl3, web, win32, win16, psp, haiku, haiku32, and beos
# against the DOS port's, whose chunky stage they share; psp under
# PPSSPP with every memory access checked as a PSP checks it, so
# an unaligned one fails; haiku, haiku32 and beos in their VMs
@ -232,6 +233,7 @@ host host blank scripts/check-blank.sh
host host roots scripts/check-fileroots.sh
host host marks scripts/check-dirtymark.sh
host host draw scripts/check-draw.sh
host host indexed scripts/check-indexed.sh
host host input scripts/check-input.sh
host host millis scripts/check-millis.sh
host host mod scripts/check-modplayer.sh

View file

@ -12,7 +12,7 @@
# Manager takes the focus, the desktop comes back and UBER presents into
# its window) and resumed (the Task Manager's Switch To): the
# screenshots must be the 320x200 mode, the desktop, then 320x200 again;
# DVX.LOG must show the mode set twice; and UBER's 54 hashes must equal
# DVX.LOG must show the mode set twice; and UBER's hashes must equal
# the DOS port's (tests/goldens/uber/dos.txt): the stage is DOS's
# wherever it is shown.
# 2. KEYS on the whole screen: the typed string must reach jlInputGetChar
@ -227,4 +227,4 @@ if [ -n "$fails" ]; then
echo "verify-dvx-fullscreen: FAIL - $fails"
exit 1
fi
echo "verify-dvx-fullscreen: PASS (UBER 54/54 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)"
echo "verify-dvx-fullscreen: PASS (UBER's hashes on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)"

View file

@ -1885,7 +1885,7 @@ uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
const AmigaPlanarT *pd;
pd = (const AmigaPlanarT *)s->portData;
return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, AMIGA_BYTES_PER_ROW, PLANAR_HASH_SEPARATE_PAIR_STEP);
return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, AMIGA_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP);
}
@ -1908,6 +1908,12 @@ bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
}
// The shared planar paste (src/m68k/planarIndexed.c), over separate planes.
void jlpSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
jlPlanarPasteIndexed(((AmigaPlanarT *)dst->portData)->planes, AMIGA_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP, x, y, w, h, src, srcStride, doubleWide);
}
bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
AmigaPlanarT *pd;
uint8_t i;
@ -1925,6 +1931,12 @@ bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
}
// The shared planar snap (src/m68k/planarIndexed.c), over separate planes.
void jlpSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
jlPlanarSnapIndexed((const uint8_t *const *)((const AmigaPlanarT *)src->portData)->planes, AMIGA_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP, x, y, w, h, dst, dstStride, doubleWide);
}
uint8_t *jlpStageAllocPixels(void) {
/* Phase 9: Amiga has no chunky shadow. The stage pixels pointer
* stays NULL; cross-platform code reads pixels via jlpSamplePixel

View file

@ -274,6 +274,7 @@ static uint16_t quantizeColorToSt(uint16_t orgb);
static void quantizePalettes(ScreenTablesT *tables, const jlSurfaceT *src, uint16_t mask);
static void setShifterVideoBase(const uint8_t *page);
static uint16_t stFirstMarkedPair(void);
static void stPlaneStarts(const jlSurfaceT *s, uint8_t *planes[]);
static uint8_t tablesToWrite(void);
extern void jlStLeaveSuper(void *savedSsp);
@ -694,6 +695,20 @@ static uint16_t stFirstMarkedPair(void) {
}
// Where plane p starts, as the shared planar helpers (src/m68k) address it:
// its words sit at 2 * p in each group, a group (two plane-byte columns)
// every ST_BYTES_PER_GROUP bytes.
static void stPlaneStarts(const jlSurfaceT *s, uint8_t *planes[]) {
uint8_t *base;
uint16_t p;
base = ((const StPlanarT *)s->portData)->base;
for (p = 0u; p < ST_PLANES; p++) {
planes[p] = base + p * sizeof(uint16_t);
}
}
static long writePrevPaletteRegs(void) {
uint16_t i;
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
@ -1457,18 +1472,12 @@ bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
}
// The shared planar hash (src/m68k/planarHash.c): plane p's words sit at
// 2 * p in each group, a group (two plane-byte columns) every 8 bytes.
// The shared planar hash (src/m68k/planarHash.c).
uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
const StPlanarT *pd;
const uint8_t *planes[ST_PLANES];
uint16_t p;
uint8_t *planes[ST_PLANES];
pd = (const StPlanarT *)s->portData;
for (p = 0u; p < ST_PLANES; p++) {
planes[p] = pd->base + p * sizeof(uint16_t);
}
return jlPlanarSurfaceHash(s, planes, ST_BYTES_PER_ROW, ST_BYTES_PER_GROUP);
stPlaneStarts(s, planes);
return jlPlanarSurfaceHash(s, (const uint8_t *const *)planes, ST_BYTES_PER_ROW, ST_BYTES_PER_GROUP);
}
@ -1485,6 +1494,15 @@ bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
}
// The shared planar paste (src/m68k/planarIndexed.c).
void jlpSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
uint8_t *planes[ST_PLANES];
stPlaneStarts(dst, planes);
jlPlanarPasteIndexed(planes, ST_BYTES_PER_ROW, ST_BYTES_PER_GROUP, x, y, w, h, src, srcStride, doubleWide);
}
bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
StPlanarT *pd;
@ -1496,6 +1514,15 @@ bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
}
// The shared planar snap (src/m68k/planarIndexed.c).
void jlpSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
uint8_t *planes[ST_PLANES];
stPlaneStarts(src, planes);
jlPlanarSnapIndexed((const uint8_t *const *)planes, ST_BYTES_PER_ROW, ST_BYTES_PER_GROUP, x, y, w, h, dst, dstStride, doubleWide);
}
// 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) {

View file

@ -14,8 +14,9 @@
// 0xF8..0xFB Relative mouse packet (3 bytes total). The header's
// low two bits are the button state (bit 1 = left,
// bit 0 = right). Next two bytes are signed dx, dy.
// Other Discarded (joystick / clock / status etc.) -- we just
// consume the documented byte count to stay in sync.
// 0xFD..0xFF Joystick packets: both sticks (2 bytes) or one (1).
// Other Discarded (clock / status etc.) -- we just consume the
// documented byte count to stay in sync.
//
// The ISR writes into private gIsrState (keyboard) and gIsrMouse*
// buffers; jlpInputPoll copies them into the public globals.

View file

@ -158,7 +158,7 @@ void jlAudioTone(uint16_t freqHz) {
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
// Deliberately NOT gated on gAudioReady -- the engine is for the
// PCM/SFX paths that need DMA + libxmp wiring. The voices are direct
// PCM/SFX paths that need DMA and module-player wiring. The voices are direct
// chip-register programming and work whether that came up or not.
// Range-check voice here so the per-port HAL can assume
// voice < JL_AUDIO_VOICES.

View file

@ -25,9 +25,9 @@
// interp()/mix arithmetic (delta * fracHalf up to ~8.4M, and the
// >> 15 on a possibly-negative value) requires a >= 32-bit int with
// arithmetic right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy
// this; the file is excluded from the IIgs build (16-bit int) by
// make/iigs.mk. Fail loudly if anyone ever shares this mixer with a
// arithmetic right shift. Every port that builds it satisfies this; the
// IIgs (16-bit int), the Amiga, and DVX build without it
// (CORE_C_SRCS_NO_MIX, make/common.mk). Fail loudly if anyone ever shares this mixer with a
// 16-bit-int target instead of producing silent garbage SFX.
#if INT_MAX < 0x7FFFFFFF
#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"

View file

@ -2,8 +2,8 @@
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Shared SFX overlay mixer used by HALs whose engine outputs MOD-only
// (libxmp-lite on DOS + ST). The mixer overlays one-shot signed 8-bit
// Shared SFX overlay mixer used by the HALs that mix on the CPU (DOS, the
// ST, the X68000, and the hosted ports, around modPlayer.c). The mixer overlays one-shot signed 8-bit
// PCM samples on top of the engine's already-rendered unsigned 8-bit
// MOD output, with a 16.16 fixed-point step so the SFX rate can
// differ from the output rate without needing an up-front resampling

View file

@ -5,10 +5,12 @@
// Portable chip-tracker music sequencer. One authored song (baked to
// the JYM1 event stream by tools/songbake) plays on every port through
// the jlAudioVoice 3-voice tone layer plus the jlAudioNoise percussion
// generator: YM2149 on the ST, Paula on the Amiga, OPL2 on DOS, DOC
// oscillators on the IIgs. Per tick the work is a handful of event
// bytes and at most four register-level calls, so sustained playback
// costs register-write time on every port -- no CPU mixing anywhere.
// generator: YM2149 on the ST, Paula on the Amiga, OPL2 (and Sound
// Blaster noise) on DOS, DOC oscillators on the IIgs, the YM2151 on the
// X68000, and psgSynth.c in software on DVX and the hosted ports. Per
// tick the work is a handful of event bytes and at most four
// register-level calls, so sustained playback costs register-write time
// wherever a chip makes the sound.
//
// Time base is jlMillisElapsed(), not the frame counter, so one song
// keeps its tempo across 50/60/70 Hz displays. jlAudioFrameTick pumps

View file

@ -87,6 +87,8 @@ extern void jlIigsTileMapPasteMark (uint16_t bx, uint16_t by, uint16_t wTiles, u
extern void jlIigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor);
extern void jlIigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void jlIigsCopyRectRows (uint8_t *dst, uint16_t srcLo, uint16_t srcBank, uint16_t rowBytes, uint16_t rows);
extern void jlIigsPasteIndexedRows (uint8_t *dst, const uint8_t *src, uint16_t srcStride, uint16_t bytes, uint16_t rows, uint16_t mode);
extern void jlIigsSnapIndexedRows (const uint8_t *src, uint8_t *dst, uint16_t dstStride, uint16_t bytes, uint16_t rows, uint16_t mode);
extern void jlIigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
extern void jlIigsFillRectNoMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
extern void jlIigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp);
@ -711,6 +713,26 @@ void jlpGenericSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t
#endif
#endif
// Indexed-color rectangles (jlSurfacePasteIndexed / jlSurfaceSnapIndexed
// payload): w bytes a row, h rows, the rectangle wholly on the surface
// (core clips, plots the edge halves, and marks).
void jlpGenericSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
#if !defined(jlpSurfacePasteIndexed)
#if defined(JL_HAS_SURFACE_PASTE_INDEXED)
void jlpSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
#else
#define jlpSurfacePasteIndexed(_d, _x, _y, _w, _h, _s, _st, _dw) jlpGenericSurfacePasteIndexed((_d), (_x), (_y), (_w), (_h), (_s), (_st), (_dw))
#endif
#endif
void jlpGenericSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
#if !defined(jlpSurfaceSnapIndexed)
#if defined(JL_HAS_SURFACE_SNAP_INDEXED)
void jlpSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
#else
#define jlpSurfaceSnapIndexed(_s, _x, _y, _w, _h, _d, _st, _dw) jlpGenericSurfaceSnapIndexed((_s), (_x), (_y), (_w), (_h), (_d), (_st), (_dw))
#endif
#endif
void jlpGenericSurfaceFreePixels(uint8_t *pixels);
#if !defined(jlpSurfaceFreePixels)
#if defined(JL_HAS_SURFACE_FREE_PIXELS)

View file

@ -26,7 +26,7 @@
// 8x8 tile geometry constants come from joey/tile.h -- one source of
// truth for the 4bpp packed layout (32 bytes/tile, 4 bytes/row, 8 px/side).
// Color 0 is always transparent for sprites (DESIGN.md contract);
// Color 0 is always transparent for sprites (a library contract);
// TRANSPARENT_NIBBLE comes from spriteInternal.h, shared with every
// per-CPU emitter.

View file

@ -31,6 +31,8 @@ typedef char stageBandsGroupMinRows[(STAGE_MARK_GROUP_MIN_ROWS == 9u) ? 1 : -1];
// (public API declared in joey/surface.h)
// (internal prototypes declared in surfaceInternal.h)
static bool indexedOnSurface(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t shift);
static bool indexedRows(int16_t y, uint16_t h, uint16_t *first, uint16_t *end);
#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JL_CPU_68000)
static inline void markGroup(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord);
static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
@ -81,6 +83,34 @@ uint16_t jlgStagePaletteDirtyMask = 0xFFFFu;
// ----- Internal helpers (alphabetical) -----
// An indexed rectangle (w bytes of 1 << shift pixels, h rows) wholly on the
// surface, in 16-bit sums (every operand under 640): the common case, which
// needs no clipping.
static bool indexedOnSurface(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t shift) {
return x >= 0 && y >= 0 && w <= JL_SURFACE_WIDTH && h <= JL_SURFACE_HEIGHT && (uint16_t)((uint16_t)x + (uint16_t)(w << shift)) <= JL_SURFACE_WIDTH && (uint16_t)((uint16_t)y + h) <= JL_SURFACE_HEIGHT;
}
// The rows [first, end) of an h-row rectangle at y that are on the surface;
// false when none are.
static bool indexedRows(int16_t y, uint16_t h, uint16_t *first, uint16_t *end) {
int32_t top;
int32_t bottom;
top = y < 0 ? -(int32_t)y : 0;
bottom = (int32_t)JL_SURFACE_HEIGHT - y;
if (bottom > (int32_t)h) {
bottom = h;
}
if (top >= bottom) {
return false;
}
*first = (uint16_t)top;
*end = (uint16_t)bottom;
return true;
}
#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JL_CPU_68000)
// Widen the STAGE_DIRTY_GROUP4_ROWS rows at minPtr / maxPtr. (Spelling the
// four rows out benched slower on DOS than this loop.)
@ -372,6 +402,71 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
}
// The bytes wholly on the surface go to the port in one call (whatever it
// stores pixels as); with doubleWide a byte cut by an edge shows one
// pixel there (a head at column 0, a tail at 319), plotted on its own.
void jlSurfacePasteIndexed(jlSurfaceT *dst, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
const uint8_t *row;
int32_t first;
int32_t end;
int32_t left;
int32_t right;
uint16_t rowFirst;
uint16_t rowEnd;
uint16_t r;
uint8_t shift;
bool head;
bool tail;
if (dst == NULL || src == NULL || w == 0u || h == 0u || x >= JL_SURFACE_WIDTH) {
return;
}
shift = doubleWide ? 1u : 0u;
if (indexedOnSurface(x, y, w, h, shift)) {
jlpSurfacePasteIndexed(dst, (uint16_t)x, (uint16_t)y, w, h, src, srcStride, doubleWide);
surfaceMarkDirtyRect(dst, x, y, (int16_t)(w << shift), (int16_t)h);
return;
}
if (!indexedRows(y, h, &rowFirst, &rowEnd)) {
return;
}
left = x;
right = left + ((int32_t)w << shift);
if (right <= 0) {
return;
}
// Byte i covers columns x + (i << shift) .. x + ((i + 1) << shift) - 1.
first = left < 0 ? ((-left + (1 << shift) - 1) >> shift) : 0;
end = ((int32_t)JL_SURFACE_WIDTH - left) >> shift;
if (end > (int32_t)w) {
end = w;
}
head = doubleWide && left < 0 && ((-left) & 1) != 0;
tail = doubleWide && end < (int32_t)w && (((int32_t)JL_SURFACE_WIDTH - left) & 1) != 0;
if (first < end) {
jlpSurfacePasteIndexed(dst, (uint16_t)(left + (first << shift)), (uint16_t)(y + rowFirst), (uint16_t)(end - first), (uint16_t)(rowEnd - rowFirst), src + (uint32_t)rowFirst * srcStride + (uint32_t)first, srcStride, doubleWide);
}
if (head || tail) {
for (r = rowFirst; r < rowEnd; r++) {
row = src + (uint32_t)r * srcStride;
if (head) {
jlpDrawPixel(dst, 0u, (uint16_t)(y + r), (uint8_t)(row[first - 1] & 0x0Fu));
}
if (tail) {
jlpDrawPixel(dst, (uint16_t)(JL_SURFACE_WIDTH - 1), (uint16_t)(y + r), (uint8_t)(row[end] & 0x0Fu));
}
}
}
if (left < 0) {
left = 0;
}
if (right > JL_SURFACE_WIDTH) {
right = JL_SURFACE_WIDTH;
}
surfaceMarkDirtyRect(dst, (int16_t)left, (int16_t)(y + rowFirst), (int16_t)(right - left), (int16_t)(rowEnd - rowFirst));
}
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
FILE *fp;
@ -399,6 +494,58 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
}
// The bytes whose pixels are on the surface come from the port in one call;
// the rest read 0.
void jlSurfaceSnapIndexed(const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
uint8_t *row;
int32_t first;
int32_t end;
int32_t left;
uint16_t rowFirst;
uint16_t rowEnd;
uint16_t r;
uint8_t shift;
if (src == NULL || dst == NULL || w == 0u || h == 0u) {
return;
}
shift = doubleWide ? 1u : 0u;
if (indexedOnSurface(x, y, w, h, shift)) {
jlpSurfaceSnapIndexed(src, (uint16_t)x, (uint16_t)y, w, h, dst, dstStride, doubleWide);
return;
}
left = x;
// Byte i reads column x + (i << shift).
first = left < 0 ? ((-left + (1 << shift) - 1) >> shift) : 0;
end = left < JL_SURFACE_WIDTH ? (((int32_t)JL_SURFACE_WIDTH - 1 - left) >> shift) + 1 : 0;
if (first > (int32_t)w) {
first = w;
}
if (end > (int32_t)w) {
end = w;
}
if (end < first) {
end = first;
}
if (!indexedRows(y, h, &rowFirst, &rowEnd)) {
rowFirst = h;
rowEnd = h;
}
for (r = 0; r < h; r++) {
row = dst + (uint32_t)r * dstStride;
if (r < rowFirst || r >= rowEnd || first == end) {
memset(row, 0, w);
continue;
}
memset(row, 0, (size_t)first);
memset(row + end, 0, (size_t)((int32_t)w - end));
}
if (rowFirst < rowEnd && first < end) {
jlpSurfaceSnapIndexed(src, (uint16_t)(left + (first << shift)), (uint16_t)(y + rowFirst), (uint16_t)(end - first), (uint16_t)(rowEnd - rowFirst), dst + (uint32_t)rowFirst * dstStride + (uint32_t)first, dstStride, doubleWide);
}
}
// ----- Internal (alphabetical) -----
bool jlStageAlloc(void) {

View file

@ -142,10 +142,19 @@ extern jlSurfaceT *jlgStage;
// (src/m68k/planarHash.c) is the planar ports' whole hash.
#define SURFACE_HASH_SEED_LO 0xACE1u
#define SURFACE_HASH_SEED_HI 0x1357u
// jlPlanarSurfaceHash's pairStep for separate planes: two columns, two bytes.
#define PLANAR_HASH_SEPARATE_PAIR_STEP 2u
// The planar helpers' pairStep for separate planes: two columns, two bytes.
#define PLANAR_SEPARATE_PAIR_STEP 2u
uint32_t jlPlanarSurfaceHash(const jlSurfaceT *s, const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep);
// A plane byte's 8 bits at the low bit of its 8 pixels' nibbles, the left
// pixel's highest: four lookups or'd at shifts 0 to 3 are the 8 pixels' 4
// chunky bytes (src/m68k/planarHash.c).
#define PLANAR_SPREAD_VALUES 256u
extern const uint32_t jlgPlanarSpread[PLANAR_SPREAD_VALUES];
// The planar ports' jlpSurfacePasteIndexed / jlpSurfaceSnapIndexed
// (src/m68k/planarIndexed.c), planes addressed as jlPlanarSurfaceHash's.
void jlPlanarPasteIndexed(uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
void jlPlanarSnapIndexed(const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
uint32_t jlSurfaceHashFinish(uint16_t lo, uint16_t hi, const jlSurfaceT *s);
// Stage SCB / palette dirty flags. jlScbSet* and jlPaletteSet set them

View file

@ -17,6 +17,10 @@
#include "surfaceInternal.h" // full jlSurfaceT (s->pixels) + JL_SURFACE_PIXELS_SIZE
#include "port.h" // jlpGenericSurfaceClear prototype
// Four bytes' low nibbles: the indexed paste and snap take four bytes a
// step, each byte's nibble moved within it, so byte order never matters.
#define NIBBLES_32 0x0F0F0F0Fu
// ----- Prototypes -----
static inline void plotNibble(uint8_t *row, uint16_t x, uint8_t nibble);
@ -206,3 +210,124 @@ void jlpGenericDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorInd
}
plotNibble(&s->pixels[SURFACE_ROW_OFFSET(y)], x, (uint8_t)(colorIndex & 0x0F));
}
// Indexed bytes onto a chunky surface, the rectangle wholly on it: a
// doubled pixel pair from an even column is one whole byte (c * 0x11),
// four at a time, two single pixels from an even column are one byte, and
// an odd column starts and ends nibble by nibble. A surface with no chunky
// pixels (a planar port's, with no paste of its own) is plotted pixel by
// pixel.
void jlpGenericSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
const uint8_t *s;
uint8_t *row;
uint8_t *d;
uint32_t v;
uint16_t r;
uint16_t i;
uint16_t px;
uint8_t c;
if (dst->pixels == NULL) {
for (r = 0; r < h; r++) {
s = src + (uint32_t)r * srcStride;
for (i = 0; i < w; i++) {
c = (uint8_t)(s[i] & 0x0Fu);
if (doubleWide) {
jlpDrawPixel(dst, (uint16_t)(x + 2u * i), (uint16_t)(y + r), c);
jlpDrawPixel(dst, (uint16_t)(x + 2u * i + 1u), (uint16_t)(y + r), c);
} else {
jlpDrawPixel(dst, (uint16_t)(x + i), (uint16_t)(y + r), c);
}
}
}
return;
}
for (r = 0; r < h; r++) {
s = src + (uint32_t)r * srcStride;
row = &dst->pixels[SURFACE_ROW_OFFSET(y + r)];
if (doubleWide && (x & 1u) == 0u) {
d = row + (x >> 1);
for (i = 0; (uint16_t)(i + 4u) <= w; i = (uint16_t)(i + 4u)) {
memcpy(&v, s + i, sizeof(v));
v &= NIBBLES_32;
v |= v << 4;
memcpy(d + i, &v, sizeof(v));
}
for (; i < w; i++) {
d[i] = (uint8_t)((s[i] & 0x0Fu) * 0x11u);
}
} else if (doubleWide) {
px = x;
for (i = 0; i < w; i++) {
c = (uint8_t)(s[i] & 0x0Fu);
plotNibble(row, px, c);
plotNibble(row, (uint16_t)(px + 1u), c);
px = (uint16_t)(px + 2u);
}
} else {
i = 0;
px = x;
if ((px & 1u) != 0u) {
plotNibble(row, px, (uint8_t)(s[0] & 0x0Fu));
px++;
i = 1;
}
d = row + (px >> 1);
for (; (uint16_t)(i + 1u) < w; i = (uint16_t)(i + 2u)) {
*d++ = (uint8_t)(((s[i] & 0x0Fu) << 4) | (s[i + 1u] & 0x0Fu));
}
if (i < w) {
plotNibble(row, (uint16_t)(x + i), (uint8_t)(s[i] & 0x0Fu));
}
}
}
}
// A chunky surface's pixels back to indexed bytes, the rectangle wholly on
// it: byte i is column x + (i << doubleWide), its nibble (even columns the
// high one), doubled ones four at a time. A surface with no chunky pixels
// is sampled pixel by pixel.
void jlpGenericSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
const uint8_t *row;
uint8_t *d;
uint32_t v;
uint16_t r;
uint16_t i;
uint16_t px;
uint8_t shift;
shift = doubleWide ? 1u : 0u;
if (src->pixels == NULL) {
for (r = 0; r < h; r++) {
d = dst + (uint32_t)r * dstStride;
for (i = 0; i < w; i++) {
d[i] = jlpSamplePixel(src, (int16_t)(x + (i << shift)), (int16_t)(y + r));
}
}
return;
}
for (r = 0; r < h; r++) {
d = dst + (uint32_t)r * dstStride;
row = &src->pixels[SURFACE_ROW_OFFSET(y + r)];
if (doubleWide) {
// Column x + 2i is byte i's high nibble from an even x, its
// low one from an odd x.
row += x >> 1;
for (i = 0; (uint16_t)(i + 4u) <= w; i = (uint16_t)(i + 4u)) {
memcpy(&v, row + i, sizeof(v));
v = ((x & 1u) == 0u ? v >> 4 : v) & NIBBLES_32;
memcpy(d + i, &v, sizeof(v));
}
for (; i < w; i++) {
d[i] = (uint8_t)((x & 1u) == 0u ? row[i] >> 4 : row[i] & 0x0Fu);
}
continue;
}
for (i = 0; i < w; i++) {
px = (uint16_t)(x + i);
d[i] = (px & 1u) != 0u ? (uint8_t)(row[px >> 1] & 0x0Fu) : (uint8_t)(row[px >> 1] >> 4);
}
}
}

View file

@ -19,8 +19,8 @@
// Timing: a window has no vertical blank, so the frame rate is a virtual
// one, HOSTED_FRAME_HZ, kept by the backend's microsecond clock from jlInit
// on. jlWaitVBL sleeps to the clock's next frame boundary. The frame reaches
// the window at most once a virtual frame: when a present lands in a frame
// that has not been shown yet, else at the next frame wait or input poll.
// the window at most once a virtual frame: a present in a frame not shown
// yet shows at once, and one in a frame already shown waits for the next.
// A program that presents many times a frame (UBER's present rows) pays for
// expanding its rows, not for the window. Each show hands the backend the
// rectangles expanded since the last one -- one a run of changed rows, as

View file

@ -201,6 +201,15 @@ extern void jlIigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uin
#define LINE_MS_SHIFT 4u
#define LINE_MS_STEPS (((BEAM_LINES_PAL - 1u) >> LINE_MS_SHIFT) + 2u)
// The indexed rows' modes (iigsIndexed.s's PIM_* and SIM_*, which say
// what each makes of a byte).
#define IIGS_PASTE_DOUBLE 0u
#define IIGS_PASTE_JOIN1 1u
#define IIGS_PASTE_JOIN2 2u
#define IIGS_SNAP_HIGH 0u
#define IIGS_SNAP_LOW 1u
#define IIGS_SNAP_BOTH 2u
// ----- Module state -----
// The beam's lines a frame, and the line the heartbeat's tick steps on,
@ -652,6 +661,93 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
}
// The indexed paste: jlIigsPasteIndexedRows does every row's whole surface
// bytes (by IIGS_PASTE_*: a doubled pixel pair from one source byte, or a
// byte joining two), and a pixel alone in a row's first byte's low nibble
// (an odd x) or its last byte's high one is plotted here. Doubled pixels
// from an odd x always leave both: the bytes between join neighbours.
void jlpSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
uint8_t *row;
uint16_t head;
uint16_t pairs;
uint16_t tailByte;
uint16_t r;
bool tail;
row = dst->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1);
head = (uint16_t)(x & 1u);
if (doubleWide) {
if (head == 0u) {
jlIigsPasteIndexedRows(row, src, srcStride, w, h, IIGS_PASTE_DOUBLE);
return;
}
if (w > 1u) {
jlIigsPasteIndexedRows(row + 1, src, srcStride, (uint16_t)(w - 1u), h, IIGS_PASTE_JOIN1);
}
tail = true;
tailByte = w;
} else {
pairs = (uint16_t)((w - head) >> 1);
if (pairs != 0u) {
jlIigsPasteIndexedRows(row + head, src + head, srcStride, pairs, h, IIGS_PASTE_JOIN2);
}
tail = ((w - head) & 1u) != 0u;
tailByte = (uint16_t)(head + pairs);
}
if (head == 0u && !tail) {
return;
}
for (r = 0; r < h; r++) {
if (head != 0u) {
row[0] = (uint8_t)((row[0] & 0xF0u) | (src[0] & 0x0Fu));
}
if (tail) {
row[tailByte] = (uint8_t)((row[tailByte] & 0x0Fu) | (uint8_t)(src[w - 1u] << 4));
}
row += JL_SURFACE_BYTES_PER_ROW;
src += srcStride;
}
}
// The indexed snap: jlIigsSnapIndexedRows does every row's whole surface
// bytes (by IIGS_SNAP_*: doubled pixels are one nibble a byte, the high
// one from an even x and the low from an odd; single ones both), and a
// single pixel alone in a byte at either end of a row is read here.
void jlpSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
const uint8_t *row;
uint16_t head;
uint16_t pairs;
uint16_t r;
bool tail;
row = src->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1);
head = (uint16_t)(x & 1u);
if (doubleWide) {
jlIigsSnapIndexedRows(row, dst, dstStride, w, h, head != 0u ? IIGS_SNAP_LOW : IIGS_SNAP_HIGH);
return;
}
pairs = (uint16_t)((w - head) >> 1);
if (pairs != 0u) {
jlIigsSnapIndexedRows(row + head, dst + head, dstStride, (uint16_t)(pairs << 1), h, IIGS_SNAP_BOTH);
}
tail = ((w - head) & 1u) != 0u;
if (head == 0u && !tail) {
return;
}
for (r = 0; r < h; r++) {
if (head != 0u) {
dst[0] = (uint8_t)(row[0] & 0x0Fu);
}
if (tail) {
dst[w - 1u] = (uint8_t)(row[head + pairs] >> 4);
}
row += JL_SURFACE_BYTES_PER_ROW;
dst += dstStride;
}
}
// IIgs is chunky-native: portData is unused. The chunky `pixels`
// buffer at $01:2000 is the stage's pixel storage and the source for
// jlStagePresent's PEI-slam to $E1.

465
src/iigs/iigsIndexed.s Normal file
View file

@ -0,0 +1,465 @@
; Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
; See LICENSE.md for license information.
; SPDX-License-Identifier: MIT
;
; iigsIndexed.s - the IIgs indexed paste and snap (jlSurfacePasteIndexed /
; jlSurfaceSnapIndexed): whole surface bytes of rows; hal.c works out the
; arguments and plots a lone nibble at either end of a row.
;
; GAS syntax for the llvm-mos w65816 toolchain (ABI notes: joeyDraw.s).
;
; A surface row never crosses a bank (the stage is $01:2000, and every
; other surface's pixels are attrNoCross), so the surface side runs with
; DBR on its bank and a 16-bit address: (dp),y or |0,x (the | keeps it
; absolute: a bare 0,x is direct page, bank 0). The caller's bytes
; can be anywhere, so that side is [dp],y, and a row step carries into the
; bank byte. The work registers are the runtime direct page's IMG0 ..
; IMG7 at $D0 (llvm816: a call clobbers them; the Loader puts the page on
; a page boundary, so no dp access pays the unaligned-D cycle); the
; arguments are read from the stack. Pixel
; bytes go two at a time through 16-bit A: masked to $0F0F, a source word
; is two pixels.
;
; ====================================================================
; void jlIigsPasteIndexedRows(uint8_t *dst, const uint8_t *src,
; uint16_t srcStride, uint16_t bytes,
; uint16_t rows, uint16_t mode)
;
; dst is the first row's first surface byte, rows of 160; bytes (>= 1) a
; row's surface bytes. Surface byte k of a row is, by mode:
; PIM_DOUBLE src[k] doubled, (src[k] & 15) * 17: a pair of doubled
; pixels from an even column
; PIM_JOIN1 (src[k] << 4) | (src[k + 1] & 15): doubled pixels from an
; odd column, every byte straddling two source bytes
; PIM_JOIN2 (src[2k] << 4) | (src[2k + 1] & 15): single pixels
; A joined byte from the word s = src[j] | src[j + 1] << 8 is the low byte
; of (m << 4) | (m >> 8), m = s & $0F0F; two of them make a stored word.
;
; ABI: arg0 dst in A:X; src @4,s, srcStride @8,s, bytes @10,s, rows @12,s,
; mode @14,s on JSL entry (+2 after php, phb).
; ====================================================================
.set PIM_DOUBLE, 0
.set PIM_JOIN1, 1
.set PIM_JOIN2, 2
.set IX_ROW_BYTES, 160
.set IX_SRC, 0xD0 ; caller bytes, far (4)
.set IX_TMP, 0xD4
.set IX_ACC, 0xD6
.set IX_SURF, 0xD8 ; surface row address
.set IX_END, 0xDA ; a row's surface end (join)
.set IX_ROWS, 0xDC ; rows left
.set IX_ODD, 0xDE ; a row's odd last byte (1)
.set IX_RUNS, IX_ACC ; double: a row's runs of 4 words
.set IX_WORDS, IX_END ; double: then its words (0-3)
.set IP_SRC, 6 ; ,s after php + phb
.set IP_SSTRIDE, 10
.set IP_BYTES, 12
.set IP_ROWS, 14
.set IP_MODE, 16
.section .text.jlIigsPasteIndexedRows,"ax"
; A joined byte of the source word at [IX_SRC],y into A's low byte (the
; high byte is left over).
.macro IX_JOIN
lda [IX_SRC],y
and #0x0F0F
sta IX_TMP
asl a
asl a
asl a
asl a
ora IX_TMP+1
.endm
; A word of two doubled pixel pairs: two source bytes at [IX_SRC],y, each
; nibble times 17, to (IX_SURF),y.
.macro IX_DOUBLE
lda [IX_SRC],y
and #0x0F0F
sta IX_TMP
asl a
asl a
asl a
asl a
ora IX_TMP
sta (IX_SURF),y
iny
iny
.endm
; The next row: the source on by srcStride (a carry into its bank byte
; only when there is one), the surface on by 160; then back to \row while
; rows are left. (Its labels are 8 and 9, so a caller's 1b-7b pass over it.)
.macro IX_NEXT_ROW row
lda IX_SRC
clc
adc IP_SSTRIDE,s
sta IX_SRC
bcc 8f
sep #0x20
.a8
inc IX_SRC+2
rep #0x20
.a16
8:
lda IX_SURF
clc
adc #IX_ROW_BYTES
sta IX_SURF
dec IX_ROWS
beq 9f
brl \row
9:
.endm
; Every row of joined bytes: Y walks the source (step per byte), X the
; surface, two bytes a store up to IX_END, an odd last one (IX_ODD) stored
; alone.
.macro IX_JOIN_ROWS step
1:
ldy #0
ldx IX_SURF
lda IP_BYTES,s
and #0xFFFE
clc
adc IX_SURF
sta IX_END
cpx IX_END
bcs 3f
2:
IX_JOIN
and #0x00FF
sta IX_ACC
.rept \step
iny
.endr
IX_JOIN
xba
and #0xFF00
ora IX_ACC
sta |0,x
.rept \step
iny
.endr
inx
inx
cpx IX_END
bcc 2b
3:
lda IX_ODD
beq 4f
IX_JOIN
sep #0x20
.a8
sta |0,x
rep #0x20
.a16
4:
IX_NEXT_ROW 1b
.endm
.globl jlIigsPasteIndexedRows
jlIigsPasteIndexedRows:
php
phb
rep #0x30
.a16
.i16
sta IX_SURF
sep #0x20
.a8
txa
pha
plb ; DBR = the surface's bank
rep #0x20
.a16
lda IP_SRC,s
sta IX_SRC
lda IP_SRC+2,s
sta IX_SRC+2
lda IP_ROWS,s
sta IX_ROWS
; a row's shape, the same in every row
lda IP_BYTES,s
and #1
sta IX_ODD
lda IP_MODE,s
beq pxDouble
cmp #PIM_JOIN1
beq pxJoin1
brl pxJoin2
pxJoin1:
IX_JOIN_ROWS 1
brl pxDone
pxJoin2:
IX_JOIN_ROWS 2
brl pxDone
pxDouble:
; Y walks both sides: runs of 4 words (X counts them), the
; 0-3 words left one at a time, an odd last byte alone
lda IP_BYTES,s
lsr a
lsr a
lsr a
sta IX_RUNS
lda IP_BYTES,s
and #6
lsr a
sta IX_WORDS
pxDoubleRow:
ldy #0
ldx IX_RUNS
beq pxDoubleWords
pxDoubleRun:
.rept 4
IX_DOUBLE
.endr
dex
bne pxDoubleRun
pxDoubleWords:
ldx IX_WORDS
beq pxDoubleOdd
pxDoubleWord:
IX_DOUBLE
dex
bne pxDoubleWord
pxDoubleOdd:
lda IX_ODD
beq pxDoubleNext
sep #0x20
.a8
lda [IX_SRC],y
and #0x0F
sta IX_TMP
asl a
asl a
asl a
asl a
ora IX_TMP
sta (IX_SURF),y
rep #0x20
.a16
pxDoubleNext:
IX_NEXT_ROW pxDoubleRow
pxDone:
plb
plp
rtl
; ====================================================================
; void jlIigsSnapIndexedRows(const uint8_t *src, uint8_t *dst,
; uint16_t dstStride, uint16_t bytes,
; uint16_t rows, uint16_t mode)
;
; src is the first row's first surface byte, rows of 160; bytes (>= 1) a
; row's output bytes. Output byte k of a row is, by mode:
; SIM_HIGH surface byte k >> 4: doubled pixels from an even column
; SIM_LOW surface byte k & 15: doubled pixels from an odd column
; SIM_BOTH surface byte k / 2's high nibble when k is even, its low one
; when odd: single pixels (bytes even)
; SIM_BOTH turns a surface word b0 | b1 << 8 into H = its high nibbles and
; L = its low ones, then stores H.lo | L.lo << 8 and H.hi | L.hi << 8.
; A 16-bit surface read may take one byte past a row; it is never stored.
;
; ABI: arg0 src in A:X; dst @4,s, dstStride @8,s, bytes @10,s, rows @12,s,
; mode @14,s on JSL entry (+2 after php, phb).
; ====================================================================
.set SIM_HIGH, 0
.set SIM_LOW, 1
.set SIM_BOTH, 2
.set SX_TMP2, 0xD0
.set SX_ROWS, 0xD2 ; rows left
.set SX_DST, 0xDC ; caller bytes, far (4)
.set IS_DST, 6 ; ,s after php + phb
.set IS_DSTRIDE, 10
.set IS_BYTES, 12
.set IS_ROWS, 14
.set IS_MODE, 16
.section .text.jlIigsSnapIndexedRows,"ax"
; A word of SIM_HIGH (\shift 1) or SIM_LOW (0) output: two surface bytes
; at (IX_SURF),y to [SX_DST],y.
.macro IS_NIBBLE_WORD shift
lda (IX_SURF),y
.if \shift
and #0xF0F0
lsr a
lsr a
lsr a
lsr a
.else
and #0x0F0F
.endif
sta [SX_DST],y
iny
iny
.endm
; One row of SIM_HIGH or SIM_LOW: Y walks both sides, two words a loop.
.macro IS_NIBBLE_ROW shift
ldy #0
lda IS_BYTES,s
lsr a
lsr a
tax
beq 4f
1:
IS_NIBBLE_WORD \shift
IS_NIBBLE_WORD \shift
dex
bne 1b
4:
lda IS_BYTES,s
and #2
beq 2f
IS_NIBBLE_WORD \shift
2:
lda IS_BYTES,s
lsr a
bcc 3f
lda (IX_SURF),y
.if \shift
and #0x00F0
lsr a
lsr a
lsr a
lsr a
.else
and #0x000F
.endif
sep #0x20
.a8
sta [SX_DST],y
rep #0x20
.a16
3:
.endm
.globl jlIigsSnapIndexedRows
jlIigsSnapIndexedRows:
php
phb
rep #0x30
.a16
.i16
sta IX_SURF
sep #0x20
.a8
txa
pha
plb ; DBR = the surface's bank
rep #0x20
.a16
lda IS_DST,s
sta SX_DST
lda IS_DST+2,s
sta SX_DST+2
lda IS_ROWS,s
sta SX_ROWS
sxRow:
lda IS_MODE,s
bne sxNotHigh
IS_NIBBLE_ROW 1
brl sxNext
sxNotHigh:
cmp #SIM_LOW
bne sxBoth
IS_NIBBLE_ROW 0
brl sxNext
sxBoth:
; X walks the surface, Y the output, four bytes from a word
ldy #0
ldx IX_SURF
lda IS_BYTES,s
and #0xFFFC
lsr a
clc
adc IX_SURF
sta IX_END
cpx IX_END
bcs sxBothTail
sxBothWord:
lda |0,x
sta IX_TMP ; b0 | b1 << 8
lsr a
lsr a
lsr a
lsr a
and #0x0F0F
sta IX_ACC ; H
lda IX_TMP
and #0x0F0F
sta IX_TMP ; L
xba
and #0xFF00
sta SX_TMP2 ; L.lo << 8
lda IX_ACC
and #0x00FF
ora SX_TMP2
sta [SX_DST],y ; H.lo | L.lo << 8
iny
iny
lda IX_ACC
xba
and #0x00FF
sta SX_TMP2
lda IX_TMP
and #0xFF00
ora SX_TMP2
sta [SX_DST],y ; H.hi | L.hi << 8
iny
iny
inx
inx
cpx IX_END
bcc sxBothWord
sxBothTail:
lda IS_BYTES,s
and #2
beq sxNext
lda |0,x
and #0x00FF
sta IX_TMP ; b0
lsr a
lsr a
lsr a
lsr a
sta IX_ACC ; b0 >> 4
lda IX_TMP
and #0x000F
xba
ora IX_ACC
sta [SX_DST],y ; b0 >> 4 | (b0 & 15) << 8
sxNext:
; the output's carry rides into its bank byte
lda SX_DST
clc
adc IS_DSTRIDE,s
sta SX_DST
sep #0x20
.a8
lda SX_DST+2
adc #0
sta SX_DST+2
rep #0x20
.a16
lda IX_SURF
clc
adc #IX_ROW_BYTES
sta IX_SURF
dec SX_ROWS
beq sxDone
brl sxRow
sxDone:
plb
plp
rtl

View file

@ -5,31 +5,37 @@
// The planar ports' jlpSurfaceHash (Amiga, X68000, Atari ST): the pixels
// hashed as the chunky bytes they would be, so a planar port's hash matches
// the chunky ports' for the same picture. A plane byte holds one plane's bit
// of 8 pixels; gSpread[b] puts byte b's bits at the low bit of those pixels'
// nibbles, left pixel highest, so four lookups or'd at shifts 0 to 3 are the
// 4 chunky bytes of the 8 pixels. Decoding a pixel at a time cost four bit
// tests a pixel, and was most of an UBER run.
// of 8 pixels; jlgPlanarSpread[b] puts byte b's bits at the low bit of those
// pixels' nibbles, left pixel highest, so four lookups or'd at shifts 0 to 3
// are the 4 chunky bytes of the 8 pixels (planarIndexed.c's snap reads them
// the same way). Decoding a pixel at a time cost four bit tests a pixel, and
// was most of an UBER run.
//
// Both layouts reach plane p's byte of column c (0..39) in a row at
// planes[p] + row * rowBytes + (c >> 1) * pairStep + (c & 1): separate
// planes are 40 bytes a row with pairStep 2, and the ST's interleaved
// groups put plane p's word at 2 * p, a group (two columns) every 8 bytes.
#include <stdbool.h>
#include <stdint.h>
#include "surfaceInternal.h"
#define SPREAD_VALUES 256u
#define PIXELS_PER_PLANE_BYTE 8u
#define NIBBLE_BITS 4u
#define COLUMN_PAIRS (JL_SURFACE_WIDTH / 16)
static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, uint16_t *hi, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3);
static void spreadBuild(void);
// Bit j of a plane byte (pixel 7 - j) at bit 4 * j.
#define SPREAD(_b) ((uint32_t)((((uint32_t)(_b) & 0x01u)) | (((uint32_t)(_b) & 0x02u) << 3) | (((uint32_t)(_b) & 0x04u) << 6) | (((uint32_t)(_b) & 0x08u) << 9) | (((uint32_t)(_b) & 0x10u) << 12) | (((uint32_t)(_b) & 0x20u) << 15) | (((uint32_t)(_b) & 0x40u) << 18) | (((uint32_t)(_b) & 0x80u) << 21)))
#define SPREAD_ROW(_h) \
SPREAD((_h) * 16u + 0u), SPREAD((_h) * 16u + 1u), SPREAD((_h) * 16u + 2u), SPREAD((_h) * 16u + 3u), \
SPREAD((_h) * 16u + 4u), SPREAD((_h) * 16u + 5u), SPREAD((_h) * 16u + 6u), SPREAD((_h) * 16u + 7u), \
SPREAD((_h) * 16u + 8u), SPREAD((_h) * 16u + 9u), SPREAD((_h) * 16u + 10u), SPREAD((_h) * 16u + 11u), \
SPREAD((_h) * 16u + 12u), SPREAD((_h) * 16u + 13u), SPREAD((_h) * 16u + 14u), SPREAD((_h) * 16u + 15u)
static uint32_t gSpread[SPREAD_VALUES];
static bool gSpreadBuilt = false;
static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, uint16_t *hi, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3);
const uint32_t jlgPlanarSpread[PLANAR_SPREAD_VALUES] = {
SPREAD_ROW(0u), SPREAD_ROW(1u), SPREAD_ROW(2u), SPREAD_ROW(3u), SPREAD_ROW(4u), SPREAD_ROW(5u), SPREAD_ROW(6u), SPREAD_ROW(7u),
SPREAD_ROW(8u), SPREAD_ROW(9u), SPREAD_ROW(10u), SPREAD_ROW(11u), SPREAD_ROW(12u), SPREAD_ROW(13u), SPREAD_ROW(14u), SPREAD_ROW(15u)
};
uint32_t jlPlanarSurfaceHash(const jlSurfaceT *s, const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep) {
@ -43,9 +49,6 @@ uint32_t jlPlanarSurfaceHash(const jlSurfaceT *s, const uint8_t *const planes[],
uint16_t row;
uint16_t pair;
if (!gSpreadBuilt) {
spreadBuild();
}
lo = SURFACE_HASH_SEED_LO;
hi = SURFACE_HASH_SEED_HI;
for (row = 0u; row < JL_SURFACE_HEIGHT; row++) {
@ -75,7 +78,7 @@ static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, ui
uint16_t h;
uint8_t b;
chunky = gSpread[b0] | (gSpread[b1] << 1) | (gSpread[b2] << 2) | (gSpread[b3] << 3);
chunky = jlgPlanarSpread[b0] | (jlgPlanarSpread[b1] << 1) | (jlgPlanarSpread[b2] << 2) | (jlgPlanarSpread[b3] << 3);
l = *lo;
h = *hi;
b = (uint8_t)(chunky >> 24);
@ -90,20 +93,3 @@ static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, ui
*hi = h;
}
static void spreadBuild(void) {
uint32_t value;
uint16_t b;
uint16_t k;
for (b = 0u; b < SPREAD_VALUES; b++) {
value = 0u;
for (k = 0u; k < PIXELS_PER_PLANE_BYTE; k++) {
if ((b & (0x80u >> k)) != 0u) {
value |= (uint32_t)1u << ((PIXELS_PER_PLANE_BYTE - 1u - k) * NIBBLE_BITS);
}
}
gSpread[b] = value;
}
gSpreadBuilt = true;
}

339
src/m68k/planarIndexed.c Normal file
View file

@ -0,0 +1,339 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The planar ports' indexed paste and snap (jlpSurfacePasteIndexed /
// jlpSurfaceSnapIndexed on the Amiga, X68000, and Atari ST), a column -- 8
// pixels, one byte of each plane -- at a time. Plane p's byte of column c in
// a row is at planes[p] + row * rowBytes + (c >> 1) * pairStep + (c & 1), as
// in planarHash.c. The core has clipped the rectangle to the surface.
//
// Paste: a column's plane bytes are two pairs of kPasteTable lookups,
// planarIndexedColumns.s's method (its header has the table's layout): each
// lookup takes two source nibbles a and b and gives their pixels' plane
// bits. A row's first and last columns, which the rectangle may only partly
// cover, are merged under masks of the pixels it does. On the 68k ports the
// asm does every row but those of doubled pixels from an odd column, whose
// pairs straddle source bytes; pasteRow here does them, and on the host all
// of them, which is where scripts/check-indexed.sh proves the table and the
// method. (The asm reads a first or last column's whole source window, up
// to 7 bytes outside a source row; pasteRow reads only the row, the rest of
// its window colour 15.)
//
// Snap: a column's 4 plane bytes are its 4 chunky bytes through
// jlgPlanarSpread, as the hash reads them, whose nibbles are the
// rectangle's bytes: whole columns at fixed nibbles (the asm on the 68k
// ports), the bytes before and after them one at a time.
#include <stdbool.h>
#include <stdint.h>
#include "surfaceInternal.h"
#define PLANES 4u
#define COLUMN_PIXELS 8u
#define COLUMN_SHIFT 3u
#define COLUMN_MASK 7u
#define NIBBLE_MASK 0x0Fu
#define NIBBLE_BITS 4u
#define NO_COLUMN 0xFFFFu
// kPasteTable: block a (16 of them) holds four 16-entry tables of b, each
// entry the plane bytes (plane 0 the top byte) of nibble a at the pixels of
// mask _ma and nibble b at those of _mb.
#define PASTE_SINGLE_A 0u // a at pixel 0, b at 2
#define PASTE_SINGLE_B 1u // a at pixel 1, b at 3
#define PASTE_DOUBLE_A 2u // a at pixels 0-1, b at 4-5
#define PASTE_DOUBLE_B 3u // a at pixels 2-3, b at 6-7
#define PASTE_TABLES 4u
#define PASTE_ENTRIES (16u * PASTE_TABLES * 16u)
#define PASTE_DOUBLE_OFFSET (PASTE_DOUBLE_A * 16u)
#define PLANE_BITS(_c, _m) ((((_c) & 1u) != 0u ? (uint32_t)(_m) << 24 : 0u) | (((_c) & 2u) != 0u ? (uint32_t)(_m) << 16 : 0u) | (((_c) & 4u) != 0u ? (uint32_t)(_m) << 8 : 0u) | (((_c) & 8u) != 0u ? (uint32_t)(_m) : 0u))
#define PASTE_ENTRY(_a, _b, _ma, _mb) (PLANE_BITS((_a), (_ma)) | PLANE_BITS((_b), (_mb)))
#define PASTE_ROW(_a, _ma, _mb) \
PASTE_ENTRY((_a), 0u, (_ma), (_mb)), PASTE_ENTRY((_a), 1u, (_ma), (_mb)), PASTE_ENTRY((_a), 2u, (_ma), (_mb)), PASTE_ENTRY((_a), 3u, (_ma), (_mb)), \
PASTE_ENTRY((_a), 4u, (_ma), (_mb)), PASTE_ENTRY((_a), 5u, (_ma), (_mb)), PASTE_ENTRY((_a), 6u, (_ma), (_mb)), PASTE_ENTRY((_a), 7u, (_ma), (_mb)), \
PASTE_ENTRY((_a), 8u, (_ma), (_mb)), PASTE_ENTRY((_a), 9u, (_ma), (_mb)), PASTE_ENTRY((_a), 10u, (_ma), (_mb)), PASTE_ENTRY((_a), 11u, (_ma), (_mb)), \
PASTE_ENTRY((_a), 12u, (_ma), (_mb)), PASTE_ENTRY((_a), 13u, (_ma), (_mb)), PASTE_ENTRY((_a), 14u, (_ma), (_mb)), PASTE_ENTRY((_a), 15u, (_ma), (_mb))
#define PASTE_BLOCK(_a) \
PASTE_ROW((_a), 0x80u, 0x20u), PASTE_ROW((_a), 0x40u, 0x10u), PASTE_ROW((_a), 0xC0u, 0x0Cu), PASTE_ROW((_a), 0x30u, 0x03u)
// The lookup of table _t at nibbles _a and _b (their high nibbles ignored).
#define PASTE(_t, _a, _b) kPasteTable[(((uint16_t)(_a) & NIBBLE_MASK) << 6) | ((_t) << 4) | ((uint16_t)(_b) & NIBBLE_MASK)]
static const uint32_t kPasteTable[PASTE_ENTRIES] = {
PASTE_BLOCK(0u), PASTE_BLOCK(1u), PASTE_BLOCK(2u), PASTE_BLOCK(3u), PASTE_BLOCK(4u), PASTE_BLOCK(5u), PASTE_BLOCK(6u), PASTE_BLOCK(7u),
PASTE_BLOCK(8u), PASTE_BLOCK(9u), PASTE_BLOCK(10u), PASTE_BLOCK(11u), PASTE_BLOCK(12u), PASTE_BLOCK(13u), PASTE_BLOCK(14u), PASTE_BLOCK(15u)
};
// What a column's source bytes are: single pixels (8 bytes), doubled ones
// (4), or doubled ones from an odd column (5, every pair but the end
// pixels straddling two).
#define KIND_SINGLE 0u
#define KIND_DOUBLE 1u
#define KIND_STRADDLE 2u
#define WINDOW_BYTES COLUMN_PIXELS
#define WINDOW_OUTSIDE 0x0Fu
#define FULL_MASK 0xFFu
// A whole column's snap: all 8 pixels, or doubled from pixels 0, 2, 4, 6
// or 1, 3, 5, 7 (planarIndexedColumns.s's modes).
#define SNAP_SINGLE 0u
#define SNAP_EVEN 1u
#define SNAP_ODD 2u
#define SNAP_NIBBLES 0x0F0F0F0Fu
#define CHUNKY_BYTES (COLUMN_PIXELS / 2u)
#if defined(JL_CPU_68000)
// planarIndexedColumns.s.
void jlPlanarPasteColumnsInterleaved(const uint8_t *src, uint16_t srcStride, uint8_t *base, uint16_t dstOff, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *table, bool doubleWide, uint8_t headMask, uint8_t tailMask);
void jlPlanarPasteColumnsSeparate(const uint8_t *src, uint16_t srcStride, uint8_t *const *planes, uint16_t dstOff, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *table, bool doubleWide, uint8_t headMask, uint8_t tailMask);
void jlPlanarSnapColumnsInterleaved(const uint8_t *base, uint16_t srcOff, uint16_t srcStride, uint8_t *dst, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *spread, uint8_t mode);
void jlPlanarSnapColumnsSeparate(const uint8_t *const *planes, uint16_t srcOff, uint16_t srcStride, uint8_t *dst, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *spread, uint8_t mode);
#endif
static inline uint16_t columnOffset(uint16_t c, uint16_t pairStep);
static uint32_t columnPlanes(const uint8_t *sp, uint8_t kind);
static void mergeColumn(uint8_t *const rows[], uint16_t off, uint32_t planes, uint8_t mask);
static void pasteRow(uint8_t *const rows[], uint16_t pairStep, uint16_t c0, uint16_t columns, const uint8_t *s, uint16_t lead, uint16_t w, uint8_t kind, uint8_t headMask, uint8_t tailMask);
static void snapBytes(const uint8_t *const rows[], uint16_t pairStep, uint16_t px, uint16_t n, uint8_t *d, uint8_t shift);
#if !defined(JL_CPU_68000)
static void snapColumns(const uint8_t *const rows[], uint16_t pairStep, uint16_t c, uint16_t n, uint8_t *d, uint8_t mode);
#endif
static inline uint32_t snapDecode(const uint8_t *const rows[], uint16_t off);
static inline void storeColumn(uint8_t *const rows[], uint16_t off, uint32_t planes);
static inline uint16_t columnOffset(uint16_t c, uint16_t pairStep) {
return (uint16_t)((c >> 1) * pairStep + (c & 1u));
}
// A column's plane bytes from its source bytes sp[0..]. Single pixels 0-7
// are sp[0..7]; doubled ones from an even column sp[0..3] twice each; from
// an odd column sp[0], sp[1] twice, sp[2] twice, sp[3] twice, sp[4].
static uint32_t columnPlanes(const uint8_t *sp, uint8_t kind) {
if (kind == KIND_DOUBLE) {
return PASTE(PASTE_DOUBLE_A, sp[0], sp[2]) | PASTE(PASTE_DOUBLE_B, sp[1], sp[3]);
}
if (kind == KIND_STRADDLE) {
return PASTE(PASTE_SINGLE_A, sp[0], sp[1]) | PASTE(PASTE_SINGLE_B, sp[1], sp[2]) | ((PASTE(PASTE_SINGLE_A, sp[2], sp[3]) | PASTE(PASTE_SINGLE_B, sp[3], sp[4])) >> NIBBLE_BITS);
}
return PASTE(PASTE_SINGLE_A, sp[0], sp[2]) | PASTE(PASTE_SINGLE_B, sp[1], sp[3]) | ((PASTE(PASTE_SINGLE_A, sp[4], sp[6]) | PASTE(PASTE_SINGLE_B, sp[5], sp[7])) >> NIBBLE_BITS);
}
void jlPlanarPasteIndexed(uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
uint8_t *rows[PLANES];
uint32_t rowOff;
uint16_t last;
uint16_t c0;
uint16_t columns;
uint16_t lead;
uint16_t r;
uint16_t p;
uint8_t shift;
uint8_t kind;
uint8_t headMask;
uint8_t tailMask;
// The columns c0 .. c0 + columns - 1 the pixels x .. last touch; the
// first's and the last's pixels in the rectangle (one column: both).
// lead: the source bytes the first column's window starts before the
// row (a doubled pixel from an odd x pairs with the byte before).
shift = doubleWide ? 1u : 0u;
kind = !doubleWide ? KIND_SINGLE : ((x & 1u) == 0u ? KIND_DOUBLE : KIND_STRADDLE);
last = (uint16_t)(x + (w << shift) - 1u);
c0 = (uint16_t)(x >> COLUMN_SHIFT);
columns = (uint16_t)((last >> COLUMN_SHIFT) - c0 + 1u);
headMask = (uint8_t)(FULL_MASK >> (x & COLUMN_MASK));
tailMask = (uint8_t)(FULL_MASK << (COLUMN_MASK - (last & COLUMN_MASK)));
if (columns == 1u) {
headMask = (uint8_t)(headMask & tailMask);
}
lead = (uint16_t)(((x & COLUMN_MASK) + (kind == KIND_STRADDLE ? 1u : 0u)) >> shift);
rowOff = (uint32_t)y * rowBytes;
#if defined(JL_CPU_68000)
if (kind != KIND_STRADDLE) {
if (pairStep == PLANAR_SEPARATE_PAIR_STEP) {
jlPlanarPasteColumnsSeparate(src - lead, srcStride, planes, (uint16_t)(rowOff + columnOffset(c0, pairStep)), rowBytes, columns, h, &kPasteTable[shift != 0u ? PASTE_DOUBLE_OFFSET : 0u], doubleWide, headMask, tailMask);
} else {
jlPlanarPasteColumnsInterleaved(src - lead, srcStride, planes[0], (uint16_t)(rowOff + columnOffset(c0, pairStep)), rowBytes, columns, h, &kPasteTable[shift != 0u ? PASTE_DOUBLE_OFFSET : 0u], doubleWide, headMask, tailMask);
}
return;
}
#endif
for (p = 0; p < PLANES; p++) {
rows[p] = planes[p] + rowOff;
}
for (r = 0; r < h; r++) {
pasteRow(rows, pairStep, c0, columns, src, lead, w, kind, headMask, tailMask);
src += srcStride;
for (p = 0; p < PLANES; p++) {
rows[p] += rowBytes;
}
}
}
void jlPlanarSnapIndexed(const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
const uint8_t *rows[PLANES];
uint32_t rowOff;
uint16_t lead;
uint16_t full;
uint16_t trail;
uint16_t xFull;
uint16_t r;
uint16_t p;
uint8_t shift;
uint8_t first;
uint8_t mode;
// A doubled snap takes pixel 0 or 1 of each pair (x's parity): a whole
// column is 8 bytes single and 4 doubled. The lead bytes come before
// the first whole column, the trail ones after the last.
shift = doubleWide ? 1u : 0u;
first = (uint8_t)(x & shift);
lead = (uint16_t)((((uint16_t)(COLUMN_PIXELS + first) - (x & COLUMN_MASK)) & COLUMN_MASK) >> shift);
if (lead > w) {
lead = w;
}
full = (uint16_t)((w - lead) >> (COLUMN_SHIFT - shift));
trail = (uint16_t)(w - lead - (full << (COLUMN_SHIFT - shift)));
xFull = (uint16_t)(x + (lead << shift));
mode = shift == 0u ? SNAP_SINGLE : (first == 0u ? SNAP_EVEN : SNAP_ODD);
rowOff = (uint32_t)y * rowBytes;
for (p = 0; p < PLANES; p++) {
rows[p] = planes[p] + rowOff;
}
#if defined(JL_CPU_68000)
if (full != 0u) {
if (pairStep == PLANAR_SEPARATE_PAIR_STEP) {
jlPlanarSnapColumnsSeparate(planes, (uint16_t)(rowOff + columnOffset((uint16_t)(xFull >> COLUMN_SHIFT), pairStep)), rowBytes, dst + lead, dstStride, full, h, jlgPlanarSpread, mode);
} else {
jlPlanarSnapColumnsInterleaved(planes[0], (uint16_t)(rowOff + columnOffset((uint16_t)(xFull >> COLUMN_SHIFT), pairStep)), rowBytes, dst + lead, dstStride, full, h, jlgPlanarSpread, mode);
}
}
#endif
for (r = 0; r < h; r++) {
snapBytes(rows, pairStep, x, lead, dst, shift);
#if !defined(JL_CPU_68000)
snapColumns(rows, pairStep, (uint16_t)(xFull >> COLUMN_SHIFT), full, dst + lead, mode);
#endif
snapBytes(rows, pairStep, (uint16_t)(xFull + (full << COLUMN_SHIFT)), trail, dst + w - trail, shift);
dst += dstStride;
for (p = 0; p < PLANES; p++) {
rows[p] += rowBytes;
}
}
}
// A column's plane bytes where mask has a pixel, the old ones elsewhere.
static void mergeColumn(uint8_t *const rows[], uint16_t off, uint32_t planes, uint8_t mask) {
uint16_t p;
uint8_t keep;
keep = (uint8_t)~mask;
for (p = 0; p < PLANES; p++) {
rows[p][off] = (uint8_t)((rows[p][off] & keep) | ((uint8_t)(planes >> ((PLANES - 1u - p) * 8u)) & mask));
}
}
// One row's columns c0 .. c0 + columns - 1, as planarIndexedColumns.s does
// them: the first and the last merged under their masks, from windows of
// the source, the ones between whole, straight from the row. The first
// column's window starts lead bytes before the row's first. A window byte
// outside the row's w reads WINDOW_OUTSIDE, every plane bit set, standing
// for whatever the asm reads there: the masks have to drop it.
static void pasteRow(uint8_t *const rows[], uint16_t pairStep, uint16_t c0, uint16_t columns, const uint8_t *s, uint16_t lead, uint16_t w, uint8_t kind, uint8_t headMask, uint8_t tailMask) {
uint8_t window[WINDOW_BYTES];
int32_t first;
int32_t index;
uint16_t c;
uint16_t k;
uint16_t step;
step = kind == KIND_SINGLE ? COLUMN_PIXELS : CHUNKY_BYTES;
first = -(int32_t)lead;
for (c = 0; c < columns; c++) {
if (c == 0u || c == columns - 1u) {
for (k = 0; k < WINDOW_BYTES; k++) {
index = first + (int32_t)k;
window[k] = (index >= 0 && index < (int32_t)w) ? s[index] : WINDOW_OUTSIDE;
}
mergeColumn(rows, columnOffset((uint16_t)(c0 + c), pairStep), columnPlanes(window, kind), c == 0u ? headMask : tailMask);
} else {
storeColumn(rows, columnOffset((uint16_t)(c0 + c), pairStep), columnPlanes(s + first, kind));
}
first += step;
}
}
// n bytes of a row from columns px, px + (1 << shift), ..., one at a time
// (a column decoded once).
static void snapBytes(const uint8_t *const rows[], uint16_t pairStep, uint16_t px, uint16_t n, uint8_t *d, uint8_t shift) {
uint32_t chunky;
uint16_t last;
uint16_t c;
uint16_t i;
last = NO_COLUMN;
chunky = 0u;
for (i = 0; i < n; i++) {
c = (uint16_t)(px >> COLUMN_SHIFT);
if (c != last) {
chunky = snapDecode(rows, columnOffset(c, pairStep));
last = c;
}
d[i] = (uint8_t)((chunky >> ((COLUMN_MASK - (px & COLUMN_MASK)) * NIBBLE_BITS)) & NIBBLE_MASK);
px = (uint16_t)(px + (1u << shift));
}
}
#if !defined(JL_CPU_68000)
// n whole columns of a row from column c, as planarIndexedColumns.s does
// them: single, all 8 pixels, the chunky bytes' high nibbles (pixels 0, 2,
// 4, 6) to the even bytes and the low to the odd; doubled, 4 bytes, the
// high nibbles (SNAP_EVEN) or the low.
static void snapColumns(const uint8_t *const rows[], uint16_t pairStep, uint16_t c, uint16_t n, uint8_t *d, uint8_t mode) {
uint32_t chunky;
uint32_t wanted;
uint16_t k;
for (; n > 0u; n--) {
chunky = snapDecode(rows, columnOffset(c, pairStep));
c++;
wanted = (mode == SNAP_ODD ? chunky : chunky >> NIBBLE_BITS) & SNAP_NIBBLES;
for (k = 0; k < CHUNKY_BYTES; k++) {
d[k << (mode == SNAP_SINGLE ? 1u : 0u)] = (uint8_t)(wanted >> ((CHUNKY_BYTES - 1u - k) * 8u));
}
if (mode == SNAP_SINGLE) {
wanted = chunky & SNAP_NIBBLES;
for (k = 0; k < CHUNKY_BYTES; k++) {
d[(k << 1) + 1u] = (uint8_t)(wanted >> ((CHUNKY_BYTES - 1u - k) * 8u));
}
d += COLUMN_PIXELS;
} else {
d += CHUNKY_BYTES;
}
}
}
#endif
// The chunky long of the column at off: its 8 pixels' nibbles, pixel 0's
// highest.
static inline uint32_t snapDecode(const uint8_t *const rows[], uint16_t off) {
return jlgPlanarSpread[rows[0][off]] | (jlgPlanarSpread[rows[1][off]] << 1) | (jlgPlanarSpread[rows[2][off]] << 2) | (jlgPlanarSpread[rows[3][off]] << 3);
}
static inline void storeColumn(uint8_t *const rows[], uint16_t off, uint32_t planes) {
rows[0][off] = (uint8_t)(planes >> 24);
rows[1][off] = (uint8_t)(planes >> 16);
rows[2][off] = (uint8_t)(planes >> 8);
rows[3][off] = (uint8_t)planes;
}

View file

@ -0,0 +1,690 @@
| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
| See LICENSE.md for license information.
| SPDX-License-Identifier: MIT
|
| The planar ports' indexed paste and snap, a column (8 pixels, one byte of
| each plane) at a time: the paste every column of its rows, a first or
| last one the rectangle cuts merged under its mask; the snap whole columns
| (planarIndexed.c does the bytes before and after them, and pasted doubled
| pixels from an odd column).
|
| void jlPlanarPasteColumnsInterleaved(const uint8_t *src, uint16_t srcStride, uint8_t *base, uint16_t dstOff, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *table, bool doubleWide, uint8_t headMask, uint8_t tailMask);
| void jlPlanarPasteColumnsSeparate(const uint8_t *src, uint16_t srcStride, uint8_t *const *planes, uint16_t dstOff, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *table, bool doubleWide, uint8_t headMask, uint8_t tailMask);
| void jlPlanarSnapColumnsInterleaved(const uint8_t *base, uint16_t srcOff, uint16_t srcStride, uint8_t *dst, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *spread, uint8_t mode);
| void jlPlanarSnapColumnsSeparate(const uint8_t *const *planes, uint16_t srcOff, uint16_t srcStride, uint8_t *dst, uint16_t dstStride, uint16_t columns, uint16_t rows, const uint32_t *spread, uint8_t mode);
|
| Paste:
|
| src is the first column's first source byte (before the row's when the
| rectangle starts inside the column), dstOff its plane-0 byte's offset (in
| base, the ST's interleaved buffer, or in each of the four separate
| planes), columns (>= 1) every column a row touches. headMask and tailMask
| are the first and last columns' pixels in the rectangle (bit 7 the left
| one; one column: headMask alone, already both). table is planarIndexed.c's
| kPasteTable, + 128 bytes for doubleWide. A first or last column reads its
| whole source window, up to 7 bytes outside a source row (RAM on these
| machines), and the mask drops what those bytes make.
|
| movep.l 0(src) reads source bytes 0, 2, 4, 6 and movep.l 1(src) bytes 1,
| 3, 5, 7, at any alignment. Masked to their nibbles a and b, the low word
| of either is (a << 8) | b, and with b times 4 it is a byte offset
| into kPasteTable: 16 blocks of 256 bytes, block a holding four 16-entry
| tables of b, each entry the plane bytes (plane 0 the top byte) of a and
| b at fixed pixels. Two lookups or'd are four pixels' bits:
| single +0: a at pixel 0, b at 2 +64: a at 1, b at 3
| so bytes 0-3 are one lookup pair and 4-7 another, shifted
| right 4 (8 source bytes a column)
| doubled +128: a at pixels 0-1, b at 4-5 +192: a at 2-3, b at 6-7
| (4 source bytes a column, so one movep.l pair is two columns)
| A column's four plane bytes go out with one movep.l on the ST (plane p's
| byte of a column sits at 2 * p) and through an 8-byte scratch on the
| stack on the separate-plane ports: two columns movep'd into it at 0 and 1
| are each plane's word for the pair. A masked column is (old & ~mask) |
| (new & mask), the mask's byte four times over.
|
| Registers: a0 the source, a1 the table, a2 the destination (a2-a5 the
| four planes when separate; on the ST a3 the row's start and a4 the row
| stride), a6 the source's step to the next row, d0/d1 the indices, d2/d3
| the columns, d4 the pair count, d5 the nibble mask, d6 the rows left
| (separate: the planes' row step in its high word), d7 scratch.
|
| Snap: srcOff is the first column's plane-0 byte; spread is
| jlgPlanarSpread, whose lookups of a column's four plane bytes, plane p's
| shifted left p, are its 4 chunky bytes (planarHash.c). mode 0 takes all
| 8 pixels -- the high nibbles are pixels 0, 2, 4, 6 and the low ones 1, 3,
| 5, 7, so two movep.l at 0 and 1 interleave them into the row -- mode 1
| (doubled, from even pixels) the high nibbles and mode 2 (doubled, from
| odd ones) the low, 4 consecutive bytes stored as a long, or byte, word,
| byte at an odd address. Registers: a0 the destination, a1 the table, a2
| the column (a2-a5 the planes when separate), d0 the chunky long, d1 the
| index, d2 scratch, d3 mode 1's shift (4, else 0), d4 the pair or column
| count, d5 the nibble mask, d6 the rows left, d7 the index mask.
|
| movep is a 68000-68040 instruction; a 68060 emulates it in software.
|
| ABI: cdecl, d2-d7/a2-a6 saved.
|
| GAS m68k syntax; every entry has both spellings (a.out/hunk "_" and ELF).
.equ SAVED, 44 | d2-d7/a2-a6
.equ LOCALS, 24
.equ L_SCRATCH, 0 | 8 bytes, even
.equ L_PAIRS, 8 | the middle's pairs - 1 (-1: none)
.equ L_HEAD, 10 | the middle starts at an odd column
.equ L_TAIL, 11 | a middle column after its pairs
.equ L_HEADMASK, 12 | the first column's mask, x4
.equ L_TAILMASK, 16 | the last column's
.equ L_FIRSTSTEP, 20 | ST: the first column to the next
.equ L_LAST, 22 | a last column the rectangle cuts
.equ L_FIRST, 23 | a first column the rectangle cuts
.equ ARGS, (LOCALS + SAVED + 4)
.equ A_SRC, ARGS + 0
.equ A_SRCSTRIDE, ARGS + 4 + 2
.equ A_DST, ARGS + 8
.equ A_DSTOFF, ARGS + 12 + 2
.equ A_DSTSTRIDE, ARGS + 16 + 2
.equ A_COLUMNS, ARGS + 20 + 2
.equ A_ROWS, ARGS + 24 + 2
.equ A_TABLE, ARGS + 28
.equ A_DOUBLE, ARGS + 32 + 3
.equ A_HEADMASK, ARGS + 36 + 3
.equ A_TAILMASK, ARGS + 40 + 3
.equ S_LOCALS, 28
.equ S_ROWSRC, 0 | the row's first column offset
.equ S_ROWDST, 4
.equ S_PLANES, 8 | four plane bases
.equ S_PAIRS, 24 | pairs - 1 (-1: none)
.equ S_HEAD, 26 | an odd first column
.equ S_TAIL, 27 | a last column after the pairs
.equ S_ARGS, (S_LOCALS + SAVED + 4)
.equ S_SRC, S_ARGS + 0
.equ S_SRCOFF, S_ARGS + 4 + 2
.equ S_SRCSTRIDE, S_ARGS + 8 + 2
.equ S_DST, S_ARGS + 12
.equ S_DSTSTRIDE, S_ARGS + 16 + 2
.equ S_COLUMNS, S_ARGS + 20 + 2
.equ S_ROWS, S_ARGS + 24 + 2
.equ S_SPREAD, S_ARGS + 28
.equ S_MODE, S_ARGS + 32 + 3
.equ MODE_SINGLE, 0
.equ MODE_EVEN, 1
.equ INDEX_MASK, 0x03FC | a byte times 4
.equ NIBBLE_SHIFT, 4
.equ PAIR_TABLE, 64 | the second lookup of a pair
.equ NIBBLES, 0x0F0F0F0F
.equ ST_PAIR_NEXT, 8 | the ST's group: two columns
.equ ST_ODD_NEXT, 7 | an odd column to the next group
.equ ST_EVEN_NEXT, 1 | an even column to the odd one
| The two lookups of the index pair in d0/d1's low words, into \out (b
| times 4 by two adds: 8 cycles to lsl.b #2's 10).
.macro LOOKUP out
add.b %d0,%d0
add.b %d0,%d0
add.b %d1,%d1
add.b %d1,%d1
move.l 0(%a1,%d0.w),\out
or.l PAIR_TABLE(%a1,%d1.w),\out
.endm
| One doubled column: 4 source bytes.
.macro DOUBLE_ONE out
movep.w 0(%a0),%d0
movep.w 1(%a0),%d1
addq.l #4,%a0
and.w %d5,%d0
and.w %d5,%d1
LOOKUP \out
.endm
| Two doubled columns: 8 source bytes, the right column's from the low
| words, the left's from the high.
.macro DOUBLE_PAIR left, right
movep.l 0(%a0),%d0
movep.l 1(%a0),%d1
addq.l #8,%a0
and.l %d5,%d0
and.l %d5,%d1
LOOKUP \right
swap %d0
swap %d1
LOOKUP \left
.endm
| One single column: 8 source bytes, pixels 4-7 from the low words.
.macro SINGLE_ONE out
movep.l 0(%a0),%d0
movep.l 1(%a0),%d1
addq.l #8,%a0
and.l %d5,%d0
and.l %d5,%d1
LOOKUP %d7
lsr.l #4,%d7
swap %d0
swap %d1
LOOKUP \out
or.l %d7,\out
.endm
.macro SINGLE_PAIR left, right
SINGLE_ONE \left
SINGLE_ONE \right
.endm
| Stores. ST: a column's plane bytes at 0, 2, 4, 6 from a2. A head column
| is odd, so the next group starts 7 on; a tail column is even, so the next
| column is 1 on.
.macro ST_TAIL col
movep.l \col,0(%a2)
addq.l #ST_EVEN_NEXT,%a2
.endm
.macro ST_HEAD col
movep.l \col,0(%a2)
addq.l #ST_ODD_NEXT,%a2
.endm
.macro ST_PAIR left, right
movep.l \left,0(%a2)
movep.l \right,1(%a2)
addq.l #ST_PAIR_NEXT,%a2
.endm
| Separate planes: a byte into each, or a word of two columns (the pairs
| start at an even column).
.macro SEP_ONE col
movep.l \col,L_SCRATCH(%sp)
move.b L_SCRATCH(%sp),(%a2)+
move.b L_SCRATCH+2(%sp),(%a3)+
move.b L_SCRATCH+4(%sp),(%a4)+
move.b L_SCRATCH+6(%sp),(%a5)+
.endm
.macro SEP_HEAD col
SEP_ONE \col
.endm
.macro SEP_TAIL col
SEP_ONE \col
.endm
.macro SEP_PAIR left, right
movep.l \left,L_SCRATCH(%sp)
movep.l \right,L_SCRATCH+1(%sp)
move.w L_SCRATCH(%sp),(%a2)+
move.w L_SCRATCH+2(%sp),(%a3)+
move.w L_SCRATCH+4(%sp),(%a4)+
move.w L_SCRATCH+6(%sp),(%a5)+
.endm
| Masked stores: \col where the mask at \mask(sp) has a pixel, the old
| plane bytes elsewhere. The ST leaves a2 on the column (ST_FIRST_NEXT
| steps it); separate planes step every plane on.
.macro ST_MASKED col, mask
movep.l 0(%a2),%d0
move.l \mask(%sp),%d1
and.l %d1,\col
not.l %d1
and.l %d1,%d0
or.l \col,%d0
movep.l %d0,0(%a2)
.endm
.macro SEP_MASKED col, mask
move.l \mask(%sp),%d1
and.l %d1,\col
movep.l \col,L_SCRATCH(%sp)
not.b %d1
move.b (%a2),%d0
and.b %d1,%d0
or.b L_SCRATCH(%sp),%d0
move.b %d0,(%a2)+
move.b (%a3),%d0
and.b %d1,%d0
or.b L_SCRATCH+2(%sp),%d0
move.b %d0,(%a3)+
move.b (%a4),%d0
and.b %d1,%d0
or.b L_SCRATCH+4(%sp),%d0
move.b %d0,(%a4)+
move.b (%a5),%d0
and.b %d1,%d0
or.b L_SCRATCH+6(%sp),%d0
move.b %d0,(%a5)+
.endm
.macro ST_FIRST_NEXT
adda.w L_FIRSTSTEP(%sp),%a2
.endm
.macro SEP_FIRST_NEXT
.endm
| Row starts and ends. The pointers stay in registers from row to row: the
| source (a0) steps on by a6, srcStride less the bytes a row's columns
| read. The ST starts each row's a2 at a3 and steps a3 on by a4 (the row
| stride); separate planes step each plane on by the stride less the
| columns, kept in d6's high word.
.macro ST_ROW
movea.l %a3,%a2
.endm
.macro ST_ROW_END
adda.l %a4,%a3
.endm
.macro SEP_ROW
.endm
.macro SEP_ROW_END
swap %d6
adda.w %d6,%a2
adda.w %d6,%a3
adda.w %d6,%a4
adda.w %d6,%a5
swap %d6
.endm
| Every row: a first column cut by the rectangle, masked; the middle (a
| head column, the pairs, a tail column); a last column cut by it, masked;
| then the next row.
.macro ROWS layout, mode
1:
\layout\()_ROW
tst.b L_FIRST(%sp)
beq.s 7f
\mode\()_ONE %d2
\layout\()_MASKED %d2, L_HEADMASK
\layout\()_FIRST_NEXT
7:
tst.b L_HEAD(%sp)
beq.s 2f
\mode\()_ONE %d2
\layout\()_HEAD %d2
2:
move.w L_PAIRS(%sp),%d4
bmi.w 4f
3:
\mode\()_PAIR %d2, %d3
\layout\()_PAIR %d2, %d3
dbra %d4,3b
4:
tst.b L_TAIL(%sp)
beq.s 5f
\mode\()_ONE %d2
\layout\()_TAIL %d2
5:
tst.b L_LAST(%sp)
beq.s 6f
\mode\()_ONE %d2
\layout\()_MASKED %d2, L_TAILMASK
6:
adda.l %a6,%a0
\layout\()_ROW_END
subq.w #1,%d6
bne.w 1b
.endm
| a6 = srcStride - columns * (1 << \shift): a row's source bytes read
| (4 a doubled column, 8 a single one) to the next row's first.
.macro SRC_SKIP shift
moveq #0,%d0
move.w A_SRCSTRIDE(%sp),%d0
moveq #0,%d1
move.w A_COLUMNS(%sp),%d1
lsl.l #\shift,%d1
sub.l %d1,%d0
movea.l %d0,%a6
.endm
| A mask argument's byte four times over, into a local.
.macro REPLICATE arg, local
moveq #0,%d0
move.b \arg(%sp),%d0
move.w %d0,%d1
lsl.w #8,%d1
or.w %d1,%d0
move.w %d0,%d1
swap %d0
move.w %d1,%d0
move.l %d0,\local(%sp)
.endm
| The shared setup: locals, the source, the table, the nibble mask, the
| rows, the masks, and the split of the columns: a first one the rectangle
| cuts (its mask not 0xFF), a last one it cuts (of two or more), and the
| middle -- the rest: a head column when it starts odd (dstOff's parity,
| flipped after a cut first column), the pairs, and a tail column.
.macro SETUP
movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -LOCALS(%sp),%sp
movea.l A_SRC(%sp),%a0
movea.l A_TABLE(%sp),%a1
move.l #NIBBLES,%d5
move.w A_ROWS(%sp),%d6
REPLICATE A_HEADMASK, L_HEADMASK
REPLICATE A_TAILMASK, L_TAILMASK
move.w A_DSTOFF(%sp),%d0
andi.w #1,%d0 | the first column odd
move.w #ST_EVEN_NEXT,L_FIRSTSTEP(%sp)
tst.w %d0
beq.s 1f
move.w #ST_ODD_NEXT,L_FIRSTSTEP(%sp)
1:
move.w A_COLUMNS(%sp),%d1
clr.b L_FIRST(%sp)
cmpi.b #0xFF,A_HEADMASK(%sp)
beq.s 2f
st L_FIRST(%sp)
subq.w #1,%d1
eori.w #1,%d0 | the middle starts after it
2:
clr.b L_LAST(%sp)
cmpi.w #2,A_COLUMNS(%sp)
bcs.s 3f
cmpi.b #0xFF,A_TAILMASK(%sp)
beq.s 3f
st L_LAST(%sp)
subq.w #1,%d1
3:
clr.b L_HEAD(%sp)
clr.b L_TAIL(%sp)
move.w #-1,L_PAIRS(%sp)
tst.w %d1 | the middle's columns
beq.s 4f
move.b %d0,L_HEAD(%sp) | the middle starts odd
sub.w %d0,%d1
move.w %d1,%d0
andi.w #1,%d0
move.b %d0,L_TAIL(%sp)
lsr.w #1,%d1
subq.w #1,%d1
move.w %d1,L_PAIRS(%sp)
4:
.endm
.macro FINISH
lea LOCALS(%sp),%sp
movem.l (%sp)+,%d2-%d7/%a2-%a6
rts
.endm
| The mode's rows: a6 for its source bytes, then the rows. (Label 9: ROWS
| has 1 to 7.)
.macro MODES layout
tst.b A_DOUBLE(%sp)
beq.w 9f
SRC_SKIP 2
ROWS \layout, DOUBLE
FINISH
9:
SRC_SKIP 3
ROWS \layout, SINGLE
FINISH
.endm
.text
.globl jlPlanarPasteColumnsInterleaved
.globl _jlPlanarPasteColumnsInterleaved
.globl jlPlanarPasteColumnsSeparate
.globl _jlPlanarPasteColumnsSeparate
jlPlanarPasteColumnsInterleaved:
_jlPlanarPasteColumnsInterleaved:
SETUP
movea.l A_DST(%sp),%a3
moveq #0,%d0
move.w A_DSTOFF(%sp),%d0
adda.l %d0,%a3
moveq #0,%d0
move.w A_DSTSTRIDE(%sp),%d0
movea.l %d0,%a4
MODES ST
jlPlanarPasteColumnsSeparate:
_jlPlanarPasteColumnsSeparate:
SETUP
moveq #0,%d0
move.w A_DSTOFF(%sp),%d0
movea.l A_DST(%sp),%a5
movea.l (%a5)+,%a2
adda.l %d0,%a2
movea.l (%a5)+,%a3
adda.l %d0,%a3
movea.l (%a5)+,%a4
adda.l %d0,%a4
movea.l (%a5),%a5
adda.l %d0,%a5
move.w A_DSTSTRIDE(%sp),%d0
sub.w A_COLUMNS(%sp),%d0 | the planes' skip
swap %d6
move.w %d0,%d6
swap %d6
MODES SEP
| ----- Snap -----
| One plane byte's lookup into d0, the first (\first: move) or or'd onto
| d0 shifted left one.
.macro SPREAD src, first
move.b \src,%d1
add.w %d1,%d1
add.w %d1,%d1
and.w %d7,%d1
.ifc \first,1
move.l 0(%a1,%d1.w),%d0
.else
add.l %d0,%d0
or.l 0(%a1,%d1.w),%d0
.endif
.endm
| A column's chunky long from its plane bytes, plane 3 first: on the ST at
| \at + 6, 4, 2, 0 from a2, separate from each plane's cursor.
.macro ST_DECODE at
SPREAD 6+\at(%a2), 1
SPREAD 4+\at(%a2), 0
SPREAD 2+\at(%a2), 0
SPREAD 0+\at(%a2), 0
.endm
.macro SEP_DECODE
SPREAD (%a5)+, 1
SPREAD (%a4)+, 0
SPREAD (%a3)+, 0
SPREAD (%a2)+, 0
.endm
| A column's bytes out. Single: the high nibbles to the even bytes and the
| low to the odd. Doubled: the wanted nibbles to 4 consecutive bytes (an
| even or odd destination: \odd).
.macro OUT_SINGLE odd
move.l %d0,%d2
lsr.l #NIBBLE_SHIFT,%d2
and.l %d5,%d2
and.l %d5,%d0
movep.l %d2,0(%a0)
movep.l %d0,1(%a0)
addq.l #8,%a0
.endm
.macro OUT_DOUBLE odd
lsr.l %d3,%d0
and.l %d5,%d0
.ifc \odd,0
move.l %d0,(%a0)+
.else
rol.l #8,%d0
move.b %d0,(%a0)+
swap %d0
move.w %d0,(%a0)+
swap %d0
lsr.l #8,%d0
move.b %d0,(%a0)+
.endif
.endm
| Row starts. ST: a2 = base + row offset; separate: each plane's.
.macro ST_SROW
movea.l S_SRC(%sp),%a2
adda.l S_ROWSRC(%sp),%a2
.endm
.macro SEP_SROW
move.l S_ROWSRC(%sp),%d0
movea.l S_PLANES(%sp),%a2
adda.l %d0,%a2
movea.l S_PLANES+4(%sp),%a3
adda.l %d0,%a3
movea.l S_PLANES+8(%sp),%a4
adda.l %d0,%a4
movea.l S_PLANES+12(%sp),%a5
adda.l %d0,%a5
.endm
| One row's columns. The ST walks a head column, pairs, and a tail (its
| columns alternate 1 and 7 apart); separate planes step every cursor one.
.macro ST_SCOLUMNS out, odd
tst.b S_HEAD(%sp)
beq.s 2f
ST_DECODE 0
\out \odd
addq.l #ST_ODD_NEXT,%a2
2:
move.w S_PAIRS(%sp),%d4
bmi.w 4f
3:
ST_DECODE 0
\out \odd
ST_DECODE 1
\out \odd
addq.l #ST_PAIR_NEXT,%a2
dbra %d4,3b
4:
tst.b S_TAIL(%sp)
beq.s 5f
ST_DECODE 0
\out \odd
5:
.endm
.macro SEP_SCOLUMNS out, odd
move.w S_COLUMNS(%sp),%d4
subq.w #1,%d4
3:
SEP_DECODE
\out \odd
dbra %d4,3b
.endm
| Every row; a doubled snap picks the even- or odd-address column body by
| the row's destination.
.macro SROWS layout
1:
movea.l S_ROWDST(%sp),%a0
\layout\()_SROW
tst.b S_MODE(%sp)
bne.w 6f
\layout\()_SCOLUMNS OUT_SINGLE, 0
bra.w 8f
6:
move.l %a0,%d0
btst #0,%d0
bne.w 7f
\layout\()_SCOLUMNS OUT_DOUBLE, 0
bra.w 8f
7:
\layout\()_SCOLUMNS OUT_DOUBLE, 1
8:
moveq #0,%d0
move.w S_SRCSTRIDE(%sp),%d0
add.l %d0,S_ROWSRC(%sp)
move.w S_DSTSTRIDE(%sp),%d0
add.l %d0,S_ROWDST(%sp)
subq.w #1,%d6
bne.w 1b
.endm
.macro SSETUP
movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -S_LOCALS(%sp),%sp
moveq #0,%d0
move.w S_SRCOFF(%sp),%d0
move.l %d0,S_ROWSRC(%sp)
move.l S_DST(%sp),S_ROWDST(%sp)
movea.l S_SPREAD(%sp),%a1
move.l #NIBBLES,%d5
move.w #INDEX_MASK,%d7
move.w S_ROWS(%sp),%d6
moveq #0,%d3
cmpi.b #MODE_EVEN,S_MODE(%sp)
bne.s 1f
moveq #NIBBLE_SHIFT,%d3
1:
move.w S_COLUMNS(%sp),%d1
andi.w #1,%d0
move.b %d0,S_HEAD(%sp)
sub.w %d0,%d1
move.w %d1,%d0
andi.w #1,%d0
move.b %d0,S_TAIL(%sp)
lsr.w #1,%d1
subq.w #1,%d1
move.w %d1,S_PAIRS(%sp)
.endm
.macro SFINISH
lea S_LOCALS(%sp),%sp
movem.l (%sp)+,%d2-%d7/%a2-%a6
rts
.endm
.globl jlPlanarSnapColumnsInterleaved
.globl _jlPlanarSnapColumnsInterleaved
.globl jlPlanarSnapColumnsSeparate
.globl _jlPlanarSnapColumnsSeparate
jlPlanarSnapColumnsInterleaved:
_jlPlanarSnapColumnsInterleaved:
SSETUP
SROWS ST
SFINISH
jlPlanarSnapColumnsSeparate:
_jlPlanarSnapColumnsSeparate:
SSETUP
movea.l S_SRC(%sp),%a0
move.l (%a0)+,S_PLANES(%sp)
move.l (%a0)+,S_PLANES+4(%sp)
move.l (%a0)+,S_PLANES+8(%sp)
move.l (%a0),S_PLANES+12(%sp)
SROWS SEP
SFINISH

View file

@ -22,7 +22,8 @@
#include "m68kIrq.h"
#include "port.h"
// The sample side lives in audioPcm.c (libxmp-lite + MSM6258 ADPCM). It is
// The sample side lives in audioPcm.c (modPlayer.c and the SFX mixer,
// encoded to MSM6258 ADPCM). It is
// driven from this file's Init/Shutdown/FrameTick so there is one owner of the
// audio lifecycle, matching how the other ports are laid out.
bool jlpAudioInitPcm(void);

View file

@ -381,7 +381,7 @@ uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
const X68kPlanarT *pd;
pd = (const X68kPlanarT *)s->portData;
return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, X68K_BYTES_PER_ROW, PLANAR_HASH_SEPARATE_PAIR_STEP);
return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, X68K_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP);
}
@ -404,6 +404,12 @@ bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
}
// The shared planar paste (src/m68k/planarIndexed.c), over separate planes.
void jlpSurfacePasteIndexed(jlSurfaceT *dst, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide) {
jlPlanarPasteIndexed(((X68kPlanarT *)dst->portData)->planes, X68K_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP, x, y, w, h, src, srcStride, doubleWide);
}
bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
X68kPlanarT *pd;
uint8_t i;
@ -421,6 +427,12 @@ bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
}
// The shared planar snap (src/m68k/planarIndexed.c), over separate planes.
void jlpSurfaceSnapIndexed(const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide) {
jlPlanarSnapIndexed((const uint8_t *const *)((const X68kPlanarT *)src->portData)->planes, X68K_BYTES_PER_ROW, PLANAR_SEPARATE_PAIR_STEP, x, y, w, h, dst, dstStride, doubleWide);
}
uint8_t *jlpStageAllocPixels(void) {
/* Phase 9: Amiga has no chunky shadow. The stage pixels pointer
* stays NULL; cross-platform code reads pixels via jlpSamplePixel

View file

@ -221,6 +221,10 @@ UBER: jlTileMapPaste 10x5: 49 iters / 16 frames = 153 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 11 iters / 16 frames = 34 ops/sec | hash=C51FE171
UBER: jlTileLayerDraw scroll: 22 iters / 132 frames = 8 ops/sec | hash=2DDFC909
UBER: jlTileLayerDraw scroll + present: 18 iters / 131 frames = 6 ops/sec | hash=2DDFC909
UBER: jlSurfacePasteIndexed 160x168 wide: 2 iters / 17 frames = 5 ops/sec | hash=E5A3D565
UBER: jlSurfaceSnapIndexed 160x168 wide: 2 iters / 28 frames = 3 ops/sec | hash=E5A3D565
UBER-CHK: tileLayer moves: hash=59A121BE
UBER-CHK: tilePasteGlyph: hash=CA66C63E
UBER-CHK: fillRectRows: hash=FA0501E4
UBER-CHK: indexed: hash=9956D67C
UBER-CHK: indexed sweep: hash=00000000

View file

@ -217,6 +217,10 @@ UBER: jlTileMapPaste 10x5: 49 iters / 18 frames = 163 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 13 iters / 16 frames = 48 ops/sec | hash=C51FE171
UBER: jlTileLayerDraw scroll: 23 iters / 132 frames = 10 ops/sec | hash=2DDFC909
UBER: jlTileLayerDraw scroll + present: 18 iters / 131 frames = 8 ops/sec | hash=2DDFC909
UBER: jlSurfacePasteIndexed 160x168 wide: 3 iters / 23 frames = 7 ops/sec | hash=E5A3D565
UBER: jlSurfaceSnapIndexed 160x168 wide: 2 iters / 30 frames = 3 ops/sec | hash=E5A3D565
UBER-CHK: tileLayer moves: hash=59A121BE
UBER-CHK: tilePasteGlyph: hash=CA66C63E
UBER-CHK: fillRectRows: hash=FA0501E4
UBER-CHK: indexed: hash=9956D67C
UBER-CHK: indexed sweep: hash=00000000

View file

@ -217,6 +217,10 @@ UBER: jlTileMapPaste 10x5: 113 iters / 18 frames = 450 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 43 iters / 16 frames = 195 ops/sec | hash=C51FE171
UBER: jlTileLayerDraw scroll: 93 iters / 128 frames = 50 ops/sec | hash=2DDFC909
UBER: jlTileLayerDraw scroll + present: 13 iters / 135 frames = 6 ops/sec | hash=2DDFC909
UBER: jlSurfacePasteIndexed 160x168 wide: 6 iters / 17 frames = 25 ops/sec | hash=E5A3D565
UBER: jlSurfaceSnapIndexed 160x168 wide: 6 iters / 17 frames = 24 ops/sec | hash=E5A3D565
UBER-CHK: tileLayer moves: hash=59A121BE
UBER-CHK: tilePasteGlyph: hash=CA66C63E
UBER-CHK: fillRectRows: hash=FA0501E4
UBER-CHK: indexed: hash=9956D67C
UBER-CHK: indexed sweep: hash=00000000

View file

@ -117,6 +117,10 @@ UBER: jlTileMapPaste 10x5: 15 iters / 16 frames = 56 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 5 iters / 16 frames = 18 ops/sec | hash=C51FE171
UBER: jlTileLayerDraw scroll: 20 iters / 128 frames = 9 ops/sec | hash=2DDFC909
UBER: jlTileLayerDraw scroll + present: 10 iters / 128 frames = 4 ops/sec | hash=2DDFC909
UBER: jlSurfacePasteIndexed 160x168 wide: 2 iters / 23 frames = 5 ops/sec | hash=E5A3D565
UBER: jlSurfaceSnapIndexed 160x168 wide: 2 iters / 22 frames = 5 ops/sec | hash=E5A3D565
UBER-CHK: tileLayer moves: hash=59A121BE
UBER-CHK: tilePasteGlyph: hash=CA66C63E
UBER-CHK: fillRectRows: hash=FA0501E4
UBER-CHK: indexed: hash=9956D67C
UBER-CHK: indexed sweep: hash=00000000

511
tests/host/indexedHost.c Normal file
View file

@ -0,0 +1,511 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// indexedHost.c - Host-cc harness for jlSurfacePasteIndexed and
// jlSurfaceSnapIndexed (src/core/surface.c clipping, the port's rectangle
// writer and reader behind it). Each case is checked against a reference
// built one pixel at a time with jlDrawPixel and jlSamplePixel:
// paste the surface after a paste equals a copy of the same background
// with every on-surface pixel of the rectangle plotted by hand
// (high nibbles in the source ignored, doubleWide pairs, a
// doubleWide byte cut by the left or right edge showing its one
// on-surface pixel)
// snap every byte of the rectangle equals jlSamplePixel of its column
// (0 off the surface), and the bytes between w and the stride are
// left alone
// Edge cases (odd columns, both edges, rows past both ends, w 65535, the
// int16_t limits) run first, then seeded random rectangles.
//
// The planar ports' paste and snap (src/m68k/planarIndexed.c, plain C) run
// here too, on host buffers in both of their layouts -- four separate
// planes (Amiga, X68000) and the ST's interleaved plane words -- given
// on-surface rectangles as the core hands them over, and checked against
// the same kind of pixel-by-pixel reference.
//
// Built by scripts/check-indexed.sh with -DJOEYLIB_PLATFORM_BLANK.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "joey/joey.h"
#include "surfaceInternal.h"
#define SRC_BYTES 81920u
#define RANDOM_CASES 600u
#define RANDOM_SEED 0x1D2C3B4Au
#define RANDOM_X_SPAN 700
#define RANDOM_X_LOW (-360)
#define RANDOM_Y_SPAN 440
#define RANDOM_Y_LOW (-220)
#define RANDOM_W_MAX 200u
#define RANDOM_H_MAX 220u
#define RANDOM_PAD_MAX 5u
#define CANARY 0xA5u
#define BACK_X_MUL 7
#define BACK_Y_MUL 3
#define PLANAR_PLANES 4u
#define PLANAR_ROW_BYTES (JL_SURFACE_WIDTH / 8)
#define PLANAR_CASES 500u
#define PLANAR_H_MAX 24u
#define ST_ROW_BYTES 160u
#define ST_GROUP_BYTES 8u
typedef struct LayoutT {
const char *name;
uint8_t *planes[PLANAR_PLANES];
uint16_t rowBytes;
uint16_t pairStep;
} LayoutT;
typedef struct CaseT {
int16_t x;
int16_t y;
uint16_t w;
uint16_t h;
uint16_t pad;
bool doubleWide;
} CaseT;
static const CaseT kEdgeCases[] = {
{ 0, 0, 160u, 200u, 0u, true },
{ 0, 0, 320u, 200u, 0u, false },
{ 1, 3, 17u, 5u, 2u, false },
{ 1, 3, 16u, 5u, 0u, false },
{ 1, 3, 17u, 5u, 1u, true },
{ -1, 10, 20u, 4u, 0u, true },
{ -1, 10, 20u, 4u, 3u, false },
{ -2, 10, 20u, 4u, 0u, true },
{ -3, 10, 2u, 4u, 0u, true },
{ -3, 10, 1u, 4u, 0u, true },
{ 319, 50, 8u, 3u, 0u, true },
{ 318, 50, 8u, 3u, 0u, true },
{ 317, 50, 8u, 3u, 0u, true },
{ 319, 50, 8u, 3u, 0u, false },
{ 320, 50, 8u, 3u, 0u, true },
{ -9, -5, 170u, 210u, 4u, true },
{ -9, -5, 340u, 210u, 4u, false },
{ 40, 198, 10u, 10u, 0u, false },
{ 40, -10, 10u, 10u, 0u, true },
{ 40, -11, 10u, 10u, 0u, true },
{ 40, 200, 10u, 10u, 0u, true },
{ 5, 5, 0u, 10u, 2u, true },
{ 5, 5, 10u, 0u, 0u, true },
{ -301, 7, 65535u, 1u, 0u, true },
{ -301, 7, 65535u, 1u, 0u, false },
{-32768, -32768, 9u, 2u, 0u, true },
{ 32767, 32767, 9u, 2u, 0u, false }
};
#define EDGE_CASE_COUNT ((uint16_t)(sizeof(kEdgeCases) / sizeof(kEdgeCases[0])))
static uint8_t gSrc[SRC_BYTES];
static uint8_t gSnap[SRC_BYTES];
static uint32_t gRandom = RANDOM_SEED;
static uint8_t gSeparate[PLANAR_PLANES][PLANAR_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gInterleaved[ST_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gPlanarBack[PLANAR_PLANES][ST_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gRef[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH];
static bool check(const char *name, bool ok, const char *detail);
static bool comparePaste(const jlSurfaceT *got, const jlSurfaceT *want, const CaseT *c, char *detail, size_t size);
static bool compareSnap(const jlSurfaceT *back, const CaseT *c, char *detail, size_t size);
static void fillBackground(jlSurfaceT *s);
static uint32_t nextRandom(void);
static void pasteReference(jlSurfaceT *s, const CaseT *c, uint16_t stride);
static uint8_t planarGet(const LayoutT *l, uint16_t x, uint16_t y);
static uint8_t *planarByte(const LayoutT *l, uint16_t p, uint16_t x, uint16_t y);
static bool planarCase(LayoutT *l, const CaseT *c, char *detail, size_t size);
static void planarSet(const LayoutT *l, uint16_t x, uint16_t y, uint8_t color);
static void planarStore(const LayoutT *l, bool save);
static bool runCase(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, const CaseT *c, char *detail, size_t size);
static bool runCases(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, bool random);
static bool runPlanar(LayoutT *l);
static uint8_t sampleWide(const jlSurfaceT *s, int32_t x, int32_t y);
static bool check(const char *name, bool ok, const char *detail) {
printf(" %s: %s%s%s\n", name, ok ? "PASS" : "FAIL", detail[0] != '\0' ? " -- " : "", detail);
return ok;
}
static bool comparePaste(const jlSurfaceT *got, const jlSurfaceT *want, const CaseT *c, char *detail, size_t size) {
int16_t x;
int16_t y;
uint8_t a;
uint8_t b;
if (jlSurfaceHash(got) == jlSurfaceHash(want)) {
return true;
}
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
a = jlSamplePixel(got, x, y);
b = jlSamplePixel(want, x, y);
if (a != b) {
snprintf(detail, size, "paste at (%d,%d) w %u h %u%s: pixel (%d,%d) is %u, want %u", c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", x, y, a, b);
return false;
}
}
}
snprintf(detail, size, "paste at (%d,%d) w %u h %u: hashes differ, pixels equal", c->x, c->y, c->w, c->h);
return false;
}
static bool compareSnap(const jlSurfaceT *back, const CaseT *c, char *detail, size_t size) {
const uint8_t *row;
uint32_t stride;
uint32_t r;
uint32_t i;
uint8_t want;
stride = (uint32_t)c->w + c->pad;
for (r = 0; r < c->h; r++) {
row = gSnap + r * stride;
for (i = 0; i < c->w; i++) {
want = sampleWide(back, (int32_t)c->x + (int32_t)(i << (c->doubleWide ? 1u : 0u)), (int32_t)c->y + (int32_t)r);
if (row[i] != want) {
snprintf(detail, size, "snap at (%d,%d) w %u h %u%s: byte %lu of row %lu is %u, want %u", c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", (unsigned long)i, (unsigned long)r, row[i], want);
return false;
}
}
for (; i < stride; i++) {
if (row[i] != CANARY) {
snprintf(detail, size, "snap at (%d,%d) w %u h %u: wrote past w at byte %lu of row %lu", c->x, c->y, c->w, c->h, (unsigned long)i, (unsigned long)r);
return false;
}
}
}
return true;
}
static void fillBackground(jlSurfaceT *s) {
int16_t x;
int16_t y;
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
jlDrawPixel(s, x, y, (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F));
}
}
}
static uint32_t nextRandom(void) {
gRandom ^= gRandom << 13;
gRandom ^= gRandom >> 17;
gRandom ^= gRandom << 5;
return gRandom;
}
static void pasteReference(jlSurfaceT *s, const CaseT *c, uint16_t stride) {
uint32_t r;
uint32_t i;
uint32_t k;
uint32_t span;
int32_t px;
int32_t py;
uint8_t color;
span = c->doubleWide ? 2u : 1u;
for (r = 0; r < c->h; r++) {
py = (int32_t)c->y + (int32_t)r;
if (py < 0 || py >= JL_SURFACE_HEIGHT) {
continue;
}
for (i = 0; i < c->w; i++) {
color = (uint8_t)(gSrc[r * stride + i] & 0x0Fu);
for (k = 0; k < span; k++) {
px = (int32_t)c->x + (int32_t)(i * span + k);
if (px >= 0 && px < JL_SURFACE_WIDTH) {
jlDrawPixel(s, (int16_t)px, (int16_t)py, color);
}
}
}
}
}
static uint8_t planarGet(const LayoutT *l, uint16_t x, uint16_t y) {
uint16_t p;
uint8_t bit;
uint8_t color;
bit = (uint8_t)(0x80u >> (x & 7u));
color = 0u;
for (p = 0; p < PLANAR_PLANES; p++) {
if ((*planarByte(l, p, x, y) & bit) != 0u) {
color = (uint8_t)(color | (1u << p));
}
}
return color;
}
static uint8_t *planarByte(const LayoutT *l, uint16_t p, uint16_t x, uint16_t y) {
uint16_t c;
c = (uint16_t)(x >> 3);
return l->planes[p] + (uint32_t)y * l->rowBytes + (uint32_t)(c >> 1) * l->pairStep + (c & 1u);
}
// One on-surface rectangle: pasted on the planes and, a pixel at a time, on
// gRef, then compared everywhere; snapped back from the planes and compared
// with gRef's pixels at the rectangle's columns.
static bool planarCase(LayoutT *l, const CaseT *c, char *detail, size_t size) {
uint32_t bytes;
uint32_t i;
uint16_t stride;
uint16_t r;
uint16_t x;
uint16_t y;
uint16_t span;
uint16_t k;
uint8_t got;
stride = (uint16_t)(c->w + c->pad);
bytes = (uint32_t)stride * c->h;
span = c->doubleWide ? 2u : 1u;
for (i = 0; i < bytes; i++) {
gSrc[i] = (uint8_t)nextRandom();
}
planarStore(l, false);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
gRef[y][x] = (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F);
}
}
jlPlanarPasteIndexed(l->planes, l->rowBytes, l->pairStep, (uint16_t)c->x, (uint16_t)c->y, c->w, c->h, gSrc, stride, c->doubleWide);
for (r = 0; r < c->h; r++) {
for (i = 0; i < c->w; i++) {
for (k = 0; k < span; k++) {
gRef[c->y + r][c->x + i * span + k] = (uint8_t)(gSrc[r * stride + i] & 0x0Fu);
}
}
}
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
got = planarGet(l, x, y);
if (got != gRef[y][x]) {
snprintf(detail, size, "%s paste at (%d,%d) w %u h %u%s: pixel (%u,%u) is %u, want %u", l->name, c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", x, y, got, gRef[y][x]);
return false;
}
}
}
memset(gSnap, CANARY, bytes);
jlPlanarSnapIndexed((const uint8_t *const *)l->planes, l->rowBytes, l->pairStep, (uint16_t)c->x, (uint16_t)c->y, c->w, c->h, gSnap, stride, c->doubleWide);
for (r = 0; r < c->h; r++) {
for (i = 0; i < stride; i++) {
got = gSnap[r * stride + i];
if (i < c->w ? got != gRef[c->y + r][c->x + i * span] : got != CANARY) {
snprintf(detail, size, "%s snap at (%d,%d) w %u h %u%s: byte %lu of row %u is %u", l->name, c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", (unsigned long)i, r, got);
return false;
}
}
}
return true;
}
static void planarSet(const LayoutT *l, uint16_t x, uint16_t y, uint8_t color) {
uint8_t *b;
uint16_t p;
uint8_t bit;
bit = (uint8_t)(0x80u >> (x & 7u));
for (p = 0; p < PLANAR_PLANES; p++) {
b = planarByte(l, p, x, y);
if (((color >> p) & 1u) != 0u) {
*b = (uint8_t)(*b | bit);
} else {
*b = (uint8_t)(*b & ~bit);
}
}
}
// Save the layout's background to gPlanarBack, or put it back.
static void planarStore(const LayoutT *l, bool save) {
uint8_t *buffer;
size_t bytes;
uint16_t p;
if (l->pairStep == ST_GROUP_BYTES) {
buffer = gInterleaved;
bytes = sizeof(gInterleaved);
if (save) {
memcpy(gPlanarBack[0], buffer, bytes);
} else {
memcpy(buffer, gPlanarBack[0], bytes);
}
return;
}
for (p = 0; p < PLANAR_PLANES; p++) {
bytes = sizeof(gSeparate[p]);
if (save) {
memcpy(gPlanarBack[p], gSeparate[p], bytes);
} else {
memcpy(gSeparate[p], gPlanarBack[p], bytes);
}
}
}
static bool runCase(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, const CaseT *c, char *detail, size_t size) {
uint32_t bytes;
uint32_t i;
uint16_t stride;
stride = (uint16_t)(c->w + c->pad);
bytes = (uint32_t)stride * c->h;
for (i = 0; i < bytes; i++) {
gSrc[i] = (uint8_t)nextRandom();
}
jlSurfaceCopy(stage, back);
jlSurfaceCopy(ref, back);
jlSurfacePasteIndexed(stage, c->x, c->y, c->w, c->h, gSrc, stride, c->doubleWide);
pasteReference(ref, c, stride);
if (!comparePaste(stage, ref, c, detail, size)) {
return false;
}
memset(gSnap, CANARY, bytes);
jlSurfaceSnapIndexed(back, c->x, c->y, c->w, c->h, gSnap, stride, c->doubleWide);
return compareSnap(back, c, detail, size);
}
static bool runCases(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, bool random) {
CaseT c;
char detail[200];
uint16_t count;
uint16_t n;
detail[0] = '\0';
count = random ? (uint16_t)RANDOM_CASES : EDGE_CASE_COUNT;
for (n = 0; n < count; n++) {
if (random) {
c.x = (int16_t)(RANDOM_X_LOW + (int32_t)(nextRandom() % RANDOM_X_SPAN));
c.y = (int16_t)(RANDOM_Y_LOW + (int32_t)(nextRandom() % RANDOM_Y_SPAN));
c.w = (uint16_t)(nextRandom() % (RANDOM_W_MAX + 1u));
c.h = (uint16_t)(nextRandom() % (RANDOM_H_MAX + 1u));
c.pad = (uint16_t)(nextRandom() % (RANDOM_PAD_MAX + 1u));
c.doubleWide = (nextRandom() & 1u) != 0u;
} else {
c = kEdgeCases[n];
}
if (!runCase(stage, ref, back, &c, detail, sizeof(detail))) {
break;
}
}
if (n == count) {
snprintf(detail, sizeof(detail), "%u rectangles", count);
}
return check(random ? "random rectangles" : "edge rectangles", n == count, detail);
}
static bool runPlanar(LayoutT *l) {
CaseT c;
char detail[200];
char name[48];
uint16_t x;
uint16_t y;
uint16_t n;
uint16_t maxW;
detail[0] = '\0';
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
planarSet(l, x, y, (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F));
}
}
planarStore(l, true);
for (n = 0; n < PLANAR_CASES; n++) {
c.doubleWide = (nextRandom() & 1u) != 0u;
c.x = (int16_t)(nextRandom() % JL_SURFACE_WIDTH);
if (c.doubleWide && c.x == JL_SURFACE_WIDTH - 1) {
c.x--;
}
maxW = (uint16_t)((JL_SURFACE_WIDTH - c.x) >> (c.doubleWide ? 1u : 0u));
// Mostly narrow (the edge columns), now and then the whole span left.
c.w = (uint16_t)(1u + nextRandom() % ((n & 3u) == 0u ? maxW : (maxW < 20u ? maxW : 20u)));
c.y = (int16_t)(nextRandom() % JL_SURFACE_HEIGHT);
c.h = (uint16_t)(1u + nextRandom() % PLANAR_H_MAX);
if (c.y + c.h > JL_SURFACE_HEIGHT) {
c.h = (uint16_t)(JL_SURFACE_HEIGHT - c.y);
}
c.pad = (uint16_t)(nextRandom() % (RANDOM_PAD_MAX + 1u));
if (!planarCase(l, &c, detail, sizeof(detail))) {
break;
}
}
if (n == PLANAR_CASES) {
snprintf(detail, sizeof(detail), "%u rectangles", n);
}
snprintf(name, sizeof(name), "planar %s", l->name);
return check(name, n == PLANAR_CASES, detail);
}
static uint8_t sampleWide(const jlSurfaceT *s, int32_t x, int32_t y) {
if (x < 0 || x >= JL_SURFACE_WIDTH || y < 0 || y >= JL_SURFACE_HEIGHT) {
return 0u;
}
return jlSamplePixel(s, (int16_t)x, (int16_t)y);
}
int main(void) {
LayoutT separate;
LayoutT interleaved;
jlConfigT config;
jlSurfaceT *stage;
jlSurfaceT *ref;
jlSurfaceT *back;
uint16_t p;
bool ok;
config.codegenBytes = 0u;
config.noAudio = true;
if (!jlInit(&config)) {
printf("indexedHost: FAIL -- jlInit: %s\n", jlLastError());
return 1;
}
stage = jlStageGet();
ref = jlSurfaceCreate();
back = jlSurfaceCreate();
if (ref == NULL || back == NULL) {
printf("indexedHost: FAIL -- jlSurfaceCreate: %s\n", jlLastError());
jlShutdown();
return 1;
}
fillBackground(back);
printf("indexedHost: indexed paste and snap, chunky and planar, against pixel-by-pixel references\n");
ok = true;
ok &= runCases(stage, ref, back, false);
ok &= runCases(stage, ref, back, true);
separate.name = "separate planes";
separate.rowBytes = PLANAR_ROW_BYTES;
separate.pairStep = PLANAR_SEPARATE_PAIR_STEP;
interleaved.name = "interleaved planes";
interleaved.rowBytes = ST_ROW_BYTES;
interleaved.pairStep = ST_GROUP_BYTES;
for (p = 0; p < PLANAR_PLANES; p++) {
separate.planes[p] = gSeparate[p];
interleaved.planes[p] = gInterleaved + p * sizeof(uint16_t);
}
ok &= runPlanar(&separate);
ok &= runPlanar(&interleaved);
jlSurfaceDestroy(back);
jlSurfaceDestroy(ref);
jlShutdown();
printf("indexedHost: %s\n", ok ? "PASS" : "FAIL");
return ok ? 0 : 1;
}