From 154b116208cca9b87bb9d10fa1f397f8918f48e2 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 17 Oct 2026 19:45:09 -0500 Subject: [PATCH] More optimizations. New indexed-to-display conversion routines. --- LICENSES.md | 9 - README.md | 2 +- docs/ApiAudio.adoc | 20 +- docs/ApiCore.adoc | 9 +- docs/ApiSurface.adoc | 71 ++++ docs/Appendix.adoc | 7 +- docs/Assets.adoc | 4 +- docs/BuildIntegration.adoc | 2 +- docs/Portability.adoc | 3 +- examples/uber/uber.c | 198 +++++++++ include/joey/audio.h | 16 +- include/joey/core.h | 4 +- include/joey/palette.h | 4 +- include/joey/platform.h | 8 + include/joey/sprite.h | 6 +- include/joey/surface.h | 20 +- installer/components/libxmp.sh | 42 -- make/amiga.mk | 1 + make/atarist.mk | 1 + make/common.mk | 2 +- make/iigs.mk | 12 +- make/project.mk | 2 +- make/x68000.mk | 2 + scripts/check-indexed.sh | 36 ++ scripts/verify-all.sh | 8 +- scripts/verify-dvx-fullscreen.sh | 4 +- src/amiga/hal.c | 14 +- src/atarist/hal.c | 47 ++- src/atarist/input.c | 5 +- src/core/audio.c | 2 +- src/core/audioSfxMix.c | 6 +- src/core/audioSfxMixInternal.h | 4 +- src/core/music.c | 10 +- src/core/port.h | 22 + src/core/sprite.c | 2 +- src/core/surface.c | 147 +++++++ src/core/surfaceInternal.h | 13 +- src/generic/genericDraw.c | 125 ++++++ src/hosted/hal.c | 4 +- src/iigs/hal.c | 96 +++++ src/iigs/iigsIndexed.s | 465 +++++++++++++++++++++ src/m68k/planarHash.c | 52 +-- src/m68k/planarIndexed.c | 339 +++++++++++++++ src/m68k/planarIndexedColumns.s | 690 +++++++++++++++++++++++++++++++ src/x68000/audio.c | 3 +- src/x68000/halPlanar.c | 14 +- tests/goldens/uber/amiga.txt | 4 + tests/goldens/uber/atarist.txt | 4 + tests/goldens/uber/dos.txt | 4 + tests/goldens/uber/iigs.txt | 4 + tests/host/indexedHost.c | 511 +++++++++++++++++++++++ 51 files changed, 2916 insertions(+), 164 deletions(-) delete mode 100644 installer/components/libxmp.sh create mode 100755 scripts/check-indexed.sh create mode 100644 src/iigs/iigsIndexed.s create mode 100644 src/m68k/planarIndexed.c create mode 100644 src/m68k/planarIndexedColumns.s create mode 100644 tests/host/indexedHost.c diff --git a/LICENSES.md b/LICENSES.md index 87889d8..bdb405f 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -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:** -- **License:** MIT -- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`) - ## Content tools (optional; not shipped, not linked) ### Furnace 0.6.8.3 diff --git a/README.md b/README.md index 8bf92cb..52823b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/ApiAudio.adoc b/docs/ApiAudio.adoc index 1693d6a..e8917d9 100644 --- a/docs/ApiAudio.adoc +++ b/docs/ApiAudio.adoc @@ -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. diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index 0dd27e7..264f61b 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -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 diff --git a/docs/ApiSurface.adoc b/docs/ApiSurface.adoc index 112591e..48fac79 100644 --- a/docs/ApiSurface.adoc +++ b/docs/ApiSurface.adoc @@ -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] diff --git a/docs/Appendix.adoc b/docs/Appendix.adoc index ab845c9..4fa552a 100644 --- a/docs/Appendix.adoc +++ b/docs/Appendix.adoc @@ -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`, diff --git a/docs/Assets.adoc b/docs/Assets.adoc index 6b0e0ac..f27046e 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -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: diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index fc1ca61..d6f7716 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -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 ---- diff --git a/docs/Portability.adoc b/docs/Portability.adoc index 2f37eab..c4cc618 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -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. diff --git a/examples/uber/uber.c b/examples/uber/uber.c index b19632a..5d22d70 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -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). diff --git a/include/joey/audio.h b/include/joey/audio.h index af9c0ea..47bd89b 100644 --- a/include/joey/audio.h +++ b/include/joey/audio.h @@ -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. diff --git a/include/joey/core.h b/include/joey/core.h index 9263df6..0b1feff 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -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); diff --git a/include/joey/palette.h b/include/joey/palette.h index 39b9438..11ee71e 100644 --- a/include/joey/palette.h +++ b/include/joey/palette.h @@ -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 diff --git a/include/joey/platform.h b/include/joey/platform.h index 9553745..342177f 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -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 diff --git a/include/joey/sprite.h b/include/joey/sprite.h index d50a01d..4257a94 100644 --- a/include/joey/sprite.h +++ b/include/joey/sprite.h @@ -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 diff --git a/include/joey/surface.h b/include/joey/surface.h index 35b7c09..b220a36 100644 --- a/include/joey/surface.h +++ b/include/joey/surface.h @@ -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 diff --git a/installer/components/libxmp.sh b/installer/components/libxmp.sh deleted file mode 100644 index 6c3acc2..0000000 --- a/installer/components/libxmp.sh +++ /dev/null @@ -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" -} diff --git a/make/amiga.mk b/make/amiga.mk index 43878f4..721c3cd 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -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 \ diff --git a/make/atarist.mk b/make/atarist.mk index 3feba9f..d0b6d97 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -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 \ diff --git a/make/common.mk b/make/common.mk index db1d976..c3e6b6e 100644 --- a/make/common.mk +++ b/make/common.mk @@ -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 diff --git a/make/iigs.mk b/make/iigs.mk index fc89463..5dc6517 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -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 diff --git a/make/project.mk b/make/project.mk index 28191f6..5f994a9 100644 --- a/make/project.mk +++ b/make/project.mk @@ -20,7 +20,7 @@ # # make every port the programs build for whose toolchain # is installed (ports without one are noted, skipped) -# make one port: iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp +# make one port: any of JOEY_PORTS (make/common.mk) # make - one of that port's goals: iigs-game-disk GAME=NAME, # iigs-verify-game-disk, iigs-game-disks # make JOEY_PORT= diff --git a/make/x68000.mk b/make/x68000.mk index 8cbf2c7..786b612 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -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 \ diff --git a/scripts/check-indexed.sh b/scripts/check-indexed.sh new file mode 100755 index 0000000..58d163b --- /dev/null +++ b/scripts/check-indexed.sh @@ -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 diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index 310bcb8..9561931 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -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 diff --git a/scripts/verify-dvx-fullscreen.sh b/scripts/verify-dvx-fullscreen.sh index cf069b3..b3202c4 100755 --- a/scripts/verify-dvx-fullscreen.sh +++ b/scripts/verify-dvx-fullscreen.sh @@ -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)" diff --git a/src/amiga/hal.c b/src/amiga/hal.c index d1299e7..c117f27 100644 --- a/src/amiga/hal.c +++ b/src/amiga/hal.c @@ -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 diff --git a/src/atarist/hal.c b/src/atarist/hal.c index 8c857ca..fb3f096 100644 --- a/src/atarist/hal.c +++ b/src/atarist/hal.c @@ -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) { diff --git a/src/atarist/input.c b/src/atarist/input.c index 06b76f0..7090f25 100644 --- a/src/atarist/input.c +++ b/src/atarist/input.c @@ -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. diff --git a/src/core/audio.c b/src/core/audio.c index 4b772e3..ba8ac49 100644 --- a/src/core/audio.c +++ b/src/core/audio.c @@ -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. diff --git a/src/core/audioSfxMix.c b/src/core/audioSfxMix.c index df68d17..fe06e39 100644 --- a/src/core/audioSfxMix.c +++ b/src/core/audioSfxMix.c @@ -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" diff --git a/src/core/audioSfxMixInternal.h b/src/core/audioSfxMixInternal.h index 7cc101b..d1832d6 100644 --- a/src/core/audioSfxMixInternal.h +++ b/src/core/audioSfxMixInternal.h @@ -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 diff --git a/src/core/music.c b/src/core/music.c index c5c98ae..f0ad39d 100644 --- a/src/core/music.c +++ b/src/core/music.c @@ -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 diff --git a/src/core/port.h b/src/core/port.h index b710adf..974e574 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -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) diff --git a/src/core/sprite.c b/src/core/sprite.c index e1a6165..89dc63e 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -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. diff --git a/src/core/surface.c b/src/core/surface.c index 970ceae..3200984 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -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) { diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index 247d367..1031977 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -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 diff --git a/src/generic/genericDraw.c b/src/generic/genericDraw.c index 3f08fd7..2dc3182 100644 --- a/src/generic/genericDraw.c +++ b/src/generic/genericDraw.c @@ -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); + } + } +} diff --git a/src/hosted/hal.c b/src/hosted/hal.c index 0b377cf..5109bcc 100644 --- a/src/hosted/hal.c +++ b/src/hosted/hal.c @@ -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 diff --git a/src/iigs/hal.c b/src/iigs/hal.c index 8e9dcf1..4dcb84e 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -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. diff --git a/src/iigs/iigsIndexed.s b/src/iigs/iigsIndexed.s new file mode 100644 index 0000000..45d1fa6 --- /dev/null +++ b/src/iigs/iigsIndexed.s @@ -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 diff --git a/src/m68k/planarHash.c b/src/m68k/planarHash.c index 0e985c3..74ee2bc 100644 --- a/src/m68k/planarHash.c +++ b/src/m68k/planarHash.c @@ -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 #include #include "surfaceInternal.h" -#define SPREAD_VALUES 256u -#define PIXELS_PER_PLANE_BYTE 8u -#define NIBBLE_BITS 4u -#define COLUMN_PAIRS (JL_SURFACE_WIDTH / 16) +#define COLUMN_PAIRS (JL_SURFACE_WIDTH / 16) + +// 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 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); -static uint32_t gSpread[SPREAD_VALUES]; -static bool gSpreadBuilt = false; +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; -} diff --git a/src/m68k/planarIndexed.c b/src/m68k/planarIndexed.c new file mode 100644 index 0000000..e0dc874 --- /dev/null +++ b/src/m68k/planarIndexed.c @@ -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 +#include + +#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; +} diff --git a/src/m68k/planarIndexedColumns.s b/src/m68k/planarIndexedColumns.s new file mode 100644 index 0000000..d192323 --- /dev/null +++ b/src/m68k/planarIndexedColumns.s @@ -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 diff --git a/src/x68000/audio.c b/src/x68000/audio.c index 5186b5c..70139af 100644 --- a/src/x68000/audio.c +++ b/src/x68000/audio.c @@ -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); diff --git a/src/x68000/halPlanar.c b/src/x68000/halPlanar.c index 528ac7f..dd31ac9 100644 --- a/src/x68000/halPlanar.c +++ b/src/x68000/halPlanar.c @@ -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 diff --git a/tests/goldens/uber/amiga.txt b/tests/goldens/uber/amiga.txt index 6549be5..223cd3c 100644 --- a/tests/goldens/uber/amiga.txt +++ b/tests/goldens/uber/amiga.txt @@ -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 diff --git a/tests/goldens/uber/atarist.txt b/tests/goldens/uber/atarist.txt index 7df4e03..2bc478e 100644 --- a/tests/goldens/uber/atarist.txt +++ b/tests/goldens/uber/atarist.txt @@ -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 diff --git a/tests/goldens/uber/dos.txt b/tests/goldens/uber/dos.txt index b641468..930904e 100644 --- a/tests/goldens/uber/dos.txt +++ b/tests/goldens/uber/dos.txt @@ -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 diff --git a/tests/goldens/uber/iigs.txt b/tests/goldens/uber/iigs.txt index 620b15c..55c56ae 100644 --- a/tests/goldens/uber/iigs.txt +++ b/tests/goldens/uber/iigs.txt @@ -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 diff --git a/tests/host/indexedHost.c b/tests/host/indexedHost.c new file mode 100644 index 0000000..472e0bf --- /dev/null +++ b/tests/host/indexedHost.c @@ -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 +#include +#include +#include + +#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; +}