From e0898cc576c03ad6cba9e836d62de207e59a8014 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 5 Oct 2026 16:47:00 -0500 Subject: [PATCH] More bug fixing and optimizations. --- README.md | 6 + docs/ApiAudio.adoc | 3 +- docs/ApiCore.adoc | 5 +- docs/ApiSerial.adoc | 9 +- docs/Portability.adoc | 19 +- examples/joy/joy.c | 101 +++---- examples/sermouse/sermouse.c | 46 ++- examples/spacetaxi/assets/genC64Data.py | 5 +- examples/spacetaxi/spacetaxi.c | 90 ++++-- examples/spacetaxi/stAudio.c | 4 +- examples/spacetaxi/stCels.c | 9 +- examples/spacetaxi/stRender.c | 238 +++++++++------ examples/spacetaxi/stSim.h | 28 +- examples/spacetaxi/stTitle.c | 142 +++++---- examples/uber/uber.c | 99 ++++++- include/joey/core.h | 6 +- include/joey/platform.h | 25 +- make/amiga.mk | 4 +- make/atarist.mk | 25 +- make/common.mk | 13 + make/dos.mk | 6 +- make/dvx.mk | 32 +- make/iigs.mk | 4 +- make/x68000.mk | 17 +- scripts/bench-dvx.sh | 54 +++- scripts/check-dirtymark.sh | 27 ++ scripts/check-fileroots.sh | 44 +++ scripts/lib/dvxGate.sh | 20 +- scripts/lib/joyScreen.py | 191 ++++++++++++ scripts/lib/keysRow.py | 97 +++++-- scripts/verify-all.sh | 24 +- scripts/verify-atarist-dirtymark.sh | 76 +++++ scripts/verify-dvx-display.sh | 123 ++++++++ scripts/verify-dvx-input.sh | 205 +++++++++---- scripts/verify-dvx-joy.sh | 261 +++++++++++++++++ scripts/verify-x68000-joy.sh | 8 +- scripts/verify-x68000-serial-mouse.sh | 41 ++- scripts/verify-x68000-serial-scb.sh | 65 +++++ src/core/init.c | 2 +- src/core/palette.c | 10 +- src/core/port.h | 6 - src/core/scb.c | 19 +- src/core/sprite.c | 5 +- src/core/surface.c | 212 ++++++++------ src/core/surfaceInternal.h | 42 ++- src/dos/audio.c | 39 +-- src/dos/dosTimer.h | 36 +++ src/dos/hal.c | 182 ++++++++---- src/dvx/audio.c | 36 +-- src/dvx/dvxPort.h | 16 + src/dvx/hal.c | 206 +++++++++++-- src/dvx/program.c | 31 ++ src/generic/genericTile.c | 3 +- src/iigs/hal.c | 51 +--- src/iigs/input.c | 8 +- src/iigs/joeyDraw.s | 117 +------- src/iigs/peislam.s | 54 ---- src/m68k/stageBands.s | 152 ++++++++++ src/x68000/hal.c | 213 +++++++------- src/x68000/x68kPlanar.h | 28 +- src/x68000/x68kPresent.s | 234 +++++++++++++++ src/x68000/x68kSccRxIsr.s | 62 +++- tests/host/dirtyMarkHost.c | 246 ++++++++++++++++ tests/host/fileRootHost.c | 371 ++++++++++++++++++++++++ 64 files changed, 3542 insertions(+), 1011 deletions(-) create mode 100755 scripts/check-dirtymark.sh create mode 100755 scripts/check-fileroots.sh create mode 100644 scripts/lib/joyScreen.py create mode 100755 scripts/verify-atarist-dirtymark.sh create mode 100755 scripts/verify-dvx-display.sh create mode 100755 scripts/verify-dvx-joy.sh create mode 100755 scripts/verify-x68000-serial-scb.sh create mode 100644 src/dos/dosTimer.h create mode 100644 src/m68k/stageBands.s create mode 100644 src/x68000/x68kPresent.s create mode 100644 tests/host/dirtyMarkHost.c create mode 100644 tests/host/fileRootHost.c diff --git a/README.md b/README.md index 801726c..f53fc40 100644 --- a/README.md +++ b/README.md @@ -116,11 +116,17 @@ the DOS stage, is diffed against `tests/goldens/uber/dos.txt` make -f make/x68000.mk x68000-verify-golden # ~1 min under the patched MAME make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min make -f make/x68000.mk x68000-verify-joy # both joysticks, every line, ~10 s +make -f make/x68000.mk x68000-verify-serial-scb # RS-232C under per-line palettes, ~3 min ``` These X68000 gates run under the patched MAME and the X68000 firmware that `./install.sh install emulator:x68000` sets up. +DVX's display is gated as well: `scripts/verify-dvx-display.sh` runs UBER's +probe build (`make dvx` puts it in `build/dvx/probe`) on nine desktop depths +and scales and on the full screen, and checks every displayed pixel against +the stage. + ## Repository layout diff --git a/docs/ApiAudio.adoc b/docs/ApiAudio.adoc index ef098bd..c29e561 100644 --- a/docs/ApiAudio.adoc +++ b/docs/ApiAudio.adoc @@ -517,7 +517,8 @@ register writes -- no allocation, no `printf`, nothing blocking. On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick counter advancing at the canonical 18.2 Hz through an accumulator so `time` -stays correct. +stays correct. `jlMillisElapsed` carries on from where it was when the tick +starts or stops; it never jumps back. On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index a0677eb..1249e39 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -139,8 +139,9 @@ void jlMemCopy(void *dst, const void *src, uint32_t bytes); ---- Copies a block. Same contract as `memcpy`, including that the regions must not -overlap, but the IIgs implements it as `MVN` block moves at 7 cycles per byte -instead of a libc byte loop, split correctly at bank boundaries. +overlap, with a 32-bit byte count on every port. It is the port's libc +`memcpy`; on the IIgs that is `MVN` block moves at 7 cycles per byte, split +correctly at bank boundaries. [cols="1,1,4", options="header"] |=== diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc index 21f2e22..696c9d9 100644 --- a/docs/ApiSerial.adoc +++ b/docs/ApiSerial.adoc @@ -68,7 +68,8 @@ at 9600 baud, so: At 19200 baud each lasts half as long, and so on. -JoeyLib's own work is safe while a port is open. On the IIgs, where the +JoeyLib's own work is safe while a port is open, with one exception on the +X68000 (below). On the IIgs, where the display and sound code masks interrupts for long stretches, opening a port makes JoeyLib cut them into pieces of at most ~1.7 ms: the present and the full-surface clear, the joystick read (~2.8 ms otherwise, with the stick at @@ -98,6 +99,12 @@ These can still lose bytes, and are the program's to schedule around: that relies on the protocol to recover. The built-in modem and printer ports stay loss-free at 9600 through full-screen presents, clears, joystick reads and sound effects. +* *Per-scanline palettes on the X68000.* The port reloads its one hardware + palette as the beam scans, and a run of palette changes on consecutive lines + holds serial receive off for the whole run, about 32 microseconds a line. + Receive stays loss-free while such runs are under about 90 lines at 9600 + baud, 40 at 19200 and 15 at 38400; palette changes three or more lines apart + release it between bands. * *The program's own code*, wherever it masks interrupts. Ways to live with them: diff --git a/docs/Portability.adoc b/docs/Portability.adoc index a32ef7f..d75f401 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -42,9 +42,9 @@ jlFree(buffer); `jlAlloc` does *not* zero the memory it returns. -For bulk copies, prefer `jlMemCopy` over `memcpy`. It has the same contract -(no overlap) but the IIgs implements it as `MVN` block moves at 7 cycles per -byte instead of a libc byte loop, split correctly at bank boundaries. +For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is +`memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7 +cycles per byte, split correctly at bank boundaries. ==== Large Local Variables @@ -285,9 +285,10 @@ interrupt. Each change of palette between one line and the next costs about one line of CPU time (32 microseconds); a band more than two lines from the one before costs about two. A screen that changes palette on every line therefore spends a third of each frame in that interrupt, during which serial -and mouse interrupts wait. Palette and SCB changes take effect at the start of -the next frame rather than mid-frame, so a fade can lag its pixels by one -frame but never tears. +and mouse interrupts wait (see the serial chapter for how long a run can be +before received bytes are lost). Palette and SCB changes take effect at the +start of the next frame rather than mid-frame, so a fade can lag its pixels by +one frame but never tears. === A Portability Checklist @@ -370,7 +371,11 @@ hashes are DOS's -- and the rest behaves like this: time, at the next `jlAudioFrameTick`, because DVX's sound library may not be called from an interrupt. * *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes. - DVX's keyboard joystick is not reported as one. + Two sticks on one game port (a Y cable, or a four-axis stick in DVX's + default two-stick mode) are joysticks 0 and 1. An axis that was never + calibrated sets its range as it moves, so until it is calibrated a full + push can read a little short of 127. DVX's keyboard joystick is not + reported as one. * *Serial ports* belong to DVX's serial library, one program each. As on every port, closing one leaves DTR as it is. * A program's `main` is compiled as `jlGameMain`, which the port calls with diff --git a/examples/joy/joy.c b/examples/joy/joy.c index 82495d0..3f5fa4a 100644 --- a/examples/joy/joy.c +++ b/examples/joy/joy.c @@ -23,7 +23,9 @@ #define FRAME_SIZE 80 #define FRAME_INSET 2 #define DOT_SIZE 8 -#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2) +// A full push puts the dot against the frame's inside edge, never on the +// frame, which erasing it would notch. +#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2 - FRAME_INSET) #define BUTTON_W 24 #define BUTTON_H 16 @@ -32,21 +34,15 @@ #define STATUS_W 80 #define STATUS_H 4 +#define ROW_GAP 6 #define STICK_TOP 16 -#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + 6) -#define STATUS_TOP (BUTTON_TOP + BUTTON_H + 6) +#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + ROW_GAP) +#define STATUS_TOP (BUTTON_TOP + BUTTON_H + ROW_GAP) #define STICK0_LEFT 24 #define STICK1_LEFT 216 -static void buildPalette(jlSurfaceT *screen); -static void drawFrame(jlSurfaceT *screen, int16_t left); -static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); -static int16_t dotXFor(int16_t left, int8_t ax); -static int16_t dotYFor(int8_t ay); -static int16_t buttonXFor(int16_t left, int idx); -static void initialPaint(jlSurfaceT *screen); -static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left); +#define CODEGEN_BYTES (8u * 1024u) typedef struct { int16_t dotX; @@ -56,6 +52,15 @@ typedef struct { bool valid; } StickViewT; +static void buildPalette(jlSurfaceT *screen); +static int16_t buttonXFor(int16_t left, int16_t idx); +static int16_t dotXFor(int16_t left, int8_t ax); +static int16_t dotYFor(int8_t ay); +static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); +static void drawFrame(jlSurfaceT *screen, int16_t left); +static void initialPaint(jlSurfaceT *screen); +static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left); + static StickViewT gView[JOYSTICK_COUNT]; @@ -79,27 +84,14 @@ static void buildPalette(jlSurfaceT *screen) { } -static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { - /* jlFillRect marks the rect dirty; jlStagePresent flushes only that - * dirty band. */ - jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); - jlStagePresent(); -} +static int16_t buttonXFor(int16_t left, int16_t idx) { + int16_t centerX; - -// Draw the static stick frame (just an outlined square -- four edges -// with the inset filled background). -static void drawFrame(jlSurfaceT *screen, int16_t left) { - int16_t top; - - top = STICK_TOP; - jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME); - jlFillRect(screen, - (int16_t)(left + FRAME_INSET), - (int16_t)(top + FRAME_INSET), - (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), - (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), - COLOR_BACKGROUND); + centerX = (int16_t)(left + FRAME_SIZE / 2); + if (idx == 0) { + return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2); + } + return (int16_t)(centerX + BUTTON_GAP / 2); } @@ -123,23 +115,30 @@ static int16_t dotYFor(int8_t ay) { } -static int16_t buttonXFor(int16_t left, int idx) { - int16_t centerX; +// jlFillRect marks the rect dirty; jlStagePresent flushes only that band. +static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { + jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); + jlStagePresent(); +} - centerX = (int16_t)(left + FRAME_SIZE / 2); - if (idx == 0) { - return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2); - } - return (int16_t)(centerX + BUTTON_GAP / 2); + +// Draw the static stick frame (just an outlined square -- four edges +// with the inset filled background). +static void drawFrame(jlSurfaceT *screen, int16_t left) { + int16_t top; + + top = STICK_TOP; + jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME); + jlFillRect(screen, (int16_t)(left + FRAME_INSET), (int16_t)(top + FRAME_INSET), (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), COLOR_BACKGROUND); } static void initialPaint(jlSurfaceT *screen) { int16_t i; + int16_t left; jlSurfaceClear(screen, COLOR_BACKGROUND); for (i = 0; i < JOYSTICK_COUNT; i++) { - int16_t left; left = (i == 0) ? STICK0_LEFT : STICK1_LEFT; drawFrame(screen, left); gView[i].valid = false; @@ -174,27 +173,20 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { } if (!v->valid || v->connected != connected) { - drawAndPresent(screen, - (int16_t)(left + (FRAME_SIZE - STATUS_W) / 2), - STATUS_TOP, - STATUS_W, STATUS_H, - connected ? COLOR_CONNECTED : COLOR_DISCONNECTED); + drawAndPresent(screen, (int16_t)(left + (FRAME_SIZE - STATUS_W) / 2), STATUS_TOP, STATUS_W, STATUS_H, connected ? COLOR_CONNECTED : COLOR_DISCONNECTED); v->connected = connected; } // Move the position dot: erase the old square (paint background), - // then stamp the new one. If position didn't change, skip. + // then stamp the new one. The first pass only stamps. If the position + // didn't change, skip. if (!v->valid || v->dotX != newDotX || v->dotY != newDotY) { if (v->valid) { drawAndPresent(screen, v->dotX, v->dotY, DOT_SIZE, DOT_SIZE, COLOR_BACKGROUND); } - drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, - (ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF); + drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, (ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF); v->dotX = newDotX; v->dotY = newDotY; - } else if (!v->valid) { - // First paint of a centered dot: still need to draw it once. - drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, COLOR_DOT_OFF); } // Button squares. @@ -204,8 +196,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { continue; } bx = buttonXFor(left, i); - drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, - newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF); + drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF); v->btn[i] = newBtn[i]; } @@ -214,10 +205,10 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { int main(void) { - jlConfigT config; - jlSurfaceT *screen; + jlConfigT config; + jlSurfaceT *screen; - config.codegenBytes = 8 * 1024; + config.codegenBytes = CODEGEN_BYTES; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/sermouse/sermouse.c b/examples/sermouse/sermouse.c index ec58515..ce0929f 100644 --- a/examples/sermouse/sermouse.c +++ b/examples/sermouse/sermouse.c @@ -7,7 +7,8 @@ // MAME with a host peer on the line and Lua moving the mouse, and checks that // neither side disturbs the other. // -// Serial: the default port at 9600 8N1, both directions at once. +// Serial: the default port at LINE_BAUD (9600 unless built otherwise) 8N1, +// both directions at once. // - Transmits an endless stream, byte k = streamByte(k, TX_STREAM_MASK), // STREAM_CHUNK bytes per loop. // - Checks the peer's endless stream in order, byte k = streamByte(k, @@ -22,6 +23,13 @@ // // Mouse: a small pointer follows it. // +// Built with -DDENSE_SCB_LINES=n, stage lines 0 .. n-1 alternate between two +// palettes, a palette change on every one of them. A port that honours the SCB +// by reloading its one hardware palette as the beam scans (the X68000's raster +// interrupt) then holds that interrupt for the whole run of lines every frame, +// and the serial checks show whether receive survives it. The default, 0, is +// one palette and no changes. +// // The bottom two stage rows are verification scanlines: one nibble per pixel, // high nibble first, behind a sentinel, for a harness that reads the // framebuffer back: @@ -37,7 +45,12 @@ #include #include +#ifndef LINE_BAUD #define LINE_BAUD 9600u +#endif +#ifndef DENSE_SCB_LINES +#define DENSE_SCB_LINES 0u +#endif #define LINE_DATA_BITS 8u #define LINE_STOP_BITS 1u @@ -86,6 +99,9 @@ #define COLOR_POINTER 15 #define PALETTE_RED_SHIFT 8 #define PALETTE_GREEN_SHIFT 4 +#define GREY_PALETTE 0u +#define BLUE_PALETTE 1u // the dense SCB's other palette +#define BLUE_FLOOR 0x00Fu // added to every colour but black typedef struct StreamStatsT { uint32_t received; // peer bytes read so far = index of the next one expected @@ -108,6 +124,7 @@ typedef struct PointerT { } PointerT; static void buildPalette(jlSurfaceT *screen); +static void buildScb(jlSurfaceT *screen); static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color); static uint16_t mouseFlags(void); static void movePointer(jlSurfaceT *screen, PointerT *pointer); @@ -127,7 +144,30 @@ static void buildPalette(jlSurfaceT *screen) { for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i); } - jlPaletteSet(screen, 0, colors); + jlPaletteSet(screen, GREY_PALETTE, colors); +} + + +// The grey palette everywhere, or DENSE_SCB_LINES lines alternating between +// it and the same ramp in blue. +static void buildScb(jlSurfaceT *screen) { + jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, GREY_PALETTE); +#if DENSE_SCB_LINES > 0 + { + uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t i; + uint16_t line; + + jlPaletteGet(screen, GREY_PALETTE, colors); + for (i = 1u; i < SURFACE_COLORS_PER_PALETTE; i++) { + colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR); + } + jlPaletteSet(screen, BLUE_PALETTE, colors); + for (line = 1u; line < DENSE_SCB_LINES; line += 2u) { + jlScbSet(screen, line, BLUE_PALETTE); + } + } +#endif } @@ -295,7 +335,7 @@ int main(void) { } screen = jlStageGet(); buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + buildScb(screen); jlSurfaceClear(screen, COLOR_BACKGROUND); line.baud = LINE_BAUD; diff --git a/examples/spacetaxi/assets/genC64Data.py b/examples/spacetaxi/assets/genC64Data.py index 6c6f130..76b25bf 100644 --- a/examples/spacetaxi/assets/genC64Data.py +++ b/examples/spacetaxi/assets/genC64Data.py @@ -4,8 +4,9 @@ # exact sprite-to-background collision bits), the standard sprite # bitmaps (cab, passenger, flame, warp, death, intro star, leaving # cels), the demo-mode RNG table ($446A), the SID SFX programs (the -# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0) -# and the level-intro star velocity tables ($450C/$4513). +# 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0). +# (The level-intro star velocity tables, $450C/$4513, went with the moving +# warp; claudeDocs/spacetaxiSpriteWarp.patch puts them back.) # # Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE # "bank ram" dump, 2-byte load-address header) so the port has one diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index c52c47e..d67d548 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -27,6 +27,15 @@ #define ST_TICK_HZ 30u #define ST_MAX_CATCHUP_TICKS 4u +// The C64 runs the game every other frame but the level intro ($4666) +// every frame. +#define ST_C64_FRAMES_PER_TICK 2u +// The intro's loop count ($4508) runs 0..3 and the cab's judder ($470D) +// steps -2 into count 0; batches that cannot show every frame end on the +// counts either side of that jump (runIntro). +#define ST_INTRO_LOOP_MASK 3u +#define ST_INTRO_SHOW_BEFORE 3u +#define ST_INTRO_SHOW_AFTER 0u // Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which // $5167 loads once the 24-hour shifts have cleared all of A..X. Files are // levels/level01.dat .. level25.dat, so Y is index 24. @@ -85,6 +94,8 @@ typedef struct { bool fireArmed; // $445D: FIRE must be released first uint8_t lastFrame; uint16_t paceAcc; + uint16_t frameAcc; // C64 frames owed, in paceAcc's units + int8_t introOwed; // intro frames owed to (+) or run ahead of (-) the next batch StStateE afterJingle; // where ST_STATE_JINGLE returns to bool gameOverTune; // $5C24: song 5 started after the wait uint8_t hsSlot; // $4D51 table slot being named @@ -189,11 +200,11 @@ static bool joyFire(void); static uint8_t joyMask(void); static bool loadLevel(uint8_t index); static uint8_t nextLevelForShift(void); -static uint8_t paceTicks(void); +static uint8_t paceTicks(uint8_t *c64Frames); static bool serviceJingle(void); static void runCabbiesMenu(uint8_t ticks); static void runDemoEnd(void); -static void runIntro(uint8_t ticks); +static void runIntro(uint8_t c64Frames); static void runNameEntry(uint8_t ticks); static bool nameEntryStart(void); static void runPlaying(uint8_t ticks); @@ -604,22 +615,35 @@ static uint8_t nextLevelForShift(void) { // Game ticks due this rendered frame, from the vertical blank count: // one tick per two frames on 50/60 Hz displays, three per seven on // the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any -// length just clamps to the catch-up cap. -static uint8_t paceTicks(void) { - uint16_t hz = jlFrameHz(); - uint8_t now = (uint8_t)jlFrameCount(); - uint8_t frames = (uint8_t)(now - gGame.lastFrame); - uint8_t ticks = 0u; +// length just clamps to the catch-up cap. *c64Frames gets the C64 frames +// due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at +// 50/60 Hz, so the level intro shows every frame the C64 does -- the +// cab's up-and-down judder ($470D) is a one-frame pattern that stepping +// two frames at a time cancels out. +static uint8_t paceTicks(uint8_t *c64Frames) { + uint16_t hz = jlFrameHz(); + uint8_t now = (uint8_t)jlFrameCount(); + uint8_t frames = (uint8_t)(now - gGame.lastFrame); + uint8_t ticks = 0u; + uint16_t perTick = ST_TICK_HZ; gGame.lastFrame = now; if (frames > 16u) { frames = 16u; } if (hz == 50u || hz == 60u) { - gGame.paceAcc += (uint16_t)(frames * 35u); - hz = 70u; - } else { - gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ); + perTick = 35u; + hz = 70u; + } + gGame.paceAcc += (uint16_t)(frames * perTick); + gGame.frameAcc += (uint16_t)(frames * perTick * ST_C64_FRAMES_PER_TICK); + *c64Frames = 0u; + while (gGame.frameAcc >= hz && *c64Frames < ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK) { + gGame.frameAcc = (uint16_t)(gGame.frameAcc - hz); + (*c64Frames)++; + } + if (gGame.frameAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK)) { + gGame.frameAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK); } // Ticks not run this frame stay owed (a slow frame catches up over // the next ones); a stall longer than the cap's worth is forgiven. @@ -674,15 +698,42 @@ static void runDemoEnd(void) { } -// $4666 loop: two frames per game tick; joystick ends a demo. -static void runIntro(uint8_t ticks) { +// $4666 loop: once per C64 frame; joystick ends a demo. A port that shows +// every frame shows the cab's one-frame judder ($470D) as the C64 does. One +// that shows only some would sample it at a fixed phase -- every second frame +// shows -1, 0, -1, 0, a smooth climb -- so when two or more frames are due +// the batch is cut or stretched by one (evened out on the next batch) to end +// on the loop counts either side of the -2 jump: the frames shown then cross +// it and the cab judders, at the same average speed. +static void runIntro(uint8_t c64Frames) { + static const int8_t kTry[] = { 0, -1, 1 }; + int16_t due; + uint8_t steps; + uint8_t end; + uint8_t t; uint8_t k; if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { runDemoEnd(); return; } - for (k = 0u; k < (uint8_t)(ticks * 2u); k++) { + due = (int16_t)(c64Frames + gGame.introOwed); + gGame.introOwed = 0; + steps = (uint8_t)((due > 0) ? due : 0); + if (c64Frames >= ST_C64_FRAMES_PER_TICK) { + for (t = 0u; t < (uint8_t)sizeof(kTry); t++) { + if (due + kTry[t] < 1) { + continue; + } + end = (uint8_t)((gSim.introLoopCount + due + kTry[t]) & ST_INTRO_LOOP_MASK); + if (end == ST_INTRO_SHOW_BEFORE || end == ST_INTRO_SHOW_AFTER) { + steps = (uint8_t)(due + kTry[t]); + gGame.introOwed = (int8_t)(-kTry[t]); + break; + } + } + } + for (k = 0u; k < steps; k++) { if (stIntroStep(&gSim)) { jlMusicStop(); // $4709 stSimEnterLevel(&gSim, &gLevel); @@ -930,7 +981,8 @@ static void startIntro(void) { // $459C: the intro's tune plays under the fly-in, not blocking; the // C64 leaves it to the IRQ player and stops it at $4709. stAudioMusic(ST_SONG_INTRO, true); - gGame.state = ST_STATE_INTRO; + gGame.introOwed = 0; + gGame.state = ST_STATE_INTRO; } @@ -1018,6 +1070,7 @@ int main(void) { for (;;) { uint8_t ticks; + uint8_t c64Frames; if (gGame.fatal) { fatalHold(); @@ -1045,6 +1098,7 @@ int main(void) { gGame.paused = false; gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed gGame.paceAcc = 0u; + gGame.frameAcc = 0u; if (gGame.state == ST_STATE_INTRO) { stAudioMusic(ST_SONG_INTRO, true); // $46B5 } @@ -1058,7 +1112,7 @@ int main(void) { enterTitle(); } } - ticks = paceTicks(); + ticks = paceTicks(&c64Frames); switch (gGame.state) { case ST_STATE_TITLE: runTitle(ticks); @@ -1090,7 +1144,7 @@ int main(void) { } break; case ST_STATE_INTRO: - runIntro(ticks); + runIntro(c64Frames); break; case ST_STATE_PLAYING: runPlaying(ticks); diff --git a/examples/spacetaxi/stAudio.c b/examples/spacetaxi/stAudio.c index d1ed9cb..1967593 100644 --- a/examples/spacetaxi/stAudio.c +++ b/examples/spacetaxi/stAudio.c @@ -323,8 +323,8 @@ static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) { } n = (gSpeechStageCount < want) ? gSpeechStageCount : want; if (n != 0u) { - // jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there), - // in two runs when the unread bytes wrap the ring's end. + // jlMemCopy (MVN on the IIgs), in two runs when the unread bytes + // wrap the ring's end. uint8_t *to = (uint8_t *)dst; uint16_t left = n; diff --git a/examples/spacetaxi/stCels.c b/examples/spacetaxi/stCels.c index cb33c46..207f887 100644 --- a/examples/spacetaxi/stCels.c +++ b/examples/spacetaxi/stCels.c @@ -2,8 +2,9 @@ // See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the // cab set (flying, wreck and the intro decorations) and passenger set in // multicolour at the constant level colours (cab/passenger 6, shared -// $D025/$D026 = 2/7), the flame set in multicolour colour 7, then the -// hires intro star in colour 7. +// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No +// intro star: the level intro paints its starfield into the character +// screen (stTitle.c), so the star sprite is never drawn. #include "stCels.h" @@ -12,7 +13,7 @@ #define MC0 0x02u #define MC1 0x07u #define CAB 0x06u // cab and passenger per-sprite colour -#define FLM 0x07u // flame / star colour +#define FLM 0x07u // flame colour const StCelDefT kStCels[] = { // Cab: flying (gear up/down x left/right), wreck cels, intro cabs. @@ -37,8 +38,6 @@ const StCelDefT kStCels[] = { { 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 }, { 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 }, { 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, FLM, 1u, MC0, MC1 }, - // Intro star, hires colour 7. - { 0xDA, FLM, 0u, 0u, 0u }, }; const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0])); diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c index 3dd0e08..3d41935 100644 --- a/examples/spacetaxi/stRender.c +++ b/examples/spacetaxi/stRender.c @@ -37,10 +37,10 @@ // The IIgs reaches its globals DBR-relative, so all of BSS must fit the // entry bank below the I/O window; the caches are sized down there. The // sprite cache must still hold every cel stRenderPrewarm builds (the -// cab, exhaust and passenger sets plus the intro star) or a cel change -// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped -// by character, so the count is a power of two. -// Room for the baked cab/passenger/exhaust set (37) plus the largest +// cab, exhaust and passenger sets) or a cel change mid-play rebuilds +// and recompiles a sprite. Glyphs are direct-mapped by character, so +// the count is a power of two. +// Room for the baked cab/passenger/exhaust set (36) plus the largest // level's hook cels (I: 30) at once. #define ST_SPRITE_CACHE 80u #define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) @@ -66,6 +66,10 @@ typedef struct { uint8_t mc0; uint8_t mc1; uint8_t level; // level index the bitmap came from (level sprites) + uint8_t offX; // where the (cropped) cel sits in the 24x24 + uint8_t offY; // VIC frame, in pixels -- see celCreate + uint8_t w; // the cel's size in pixels + uint8_t h; bool used; bool pinned; // prewarmed moving cel: never evicted (its // compiled code is costly to re-emit on the 65816) @@ -78,6 +82,7 @@ typedef struct { // What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0). typedef struct { bool drawn; + bool trusted; // backed up by the compiled, unclipped save uint16_t x; uint8_t y; uint8_t ptr; @@ -92,8 +97,7 @@ typedef struct { // tile, animated by rewriting logoSlotMask[]'s palette slots. bool logoMode; bool starMode; // level intro: cycling the starfield - uint8_t starPhase; // which ring the bright band is on - uint8_t starTick; // frames until the band steps outward + uint8_t starPhase; // which step of every flight is lit bool logoTileOk; // the live glyph is one of the frames uint8_t logoSlotCount; uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in @@ -136,22 +140,18 @@ static const uint16_t kC64Palette[16] = { // four of these are used; the rest are headroom if the frames change. static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; -// Level-intro warp: how a star ring looks at each distance BEHIND the -// bright band, as C64 colour indices (white, light grey, grey, dark -// grey, then black). A ring this far behind the band or further is -// unlit, so a lit head with a short fading tail sweeps outward and the -// rest of the field stays dark. -static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u }; +// Level-intro warp: how a star step looks at each distance BEHIND the +// lit step, as C64 colour indices; further behind is unlit. One yellow +// star per ray, as the C64's star sprites (colour 7, $4523) fly with no +// trail. +static const uint8_t kStarRamp[] = { 7u }; #define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) -// Frames per ring step. The intro runs at the game's 30 Hz tick, so a -// step every other frame sweeps the eight rings in about half a second. -#define ST_STAR_STEP_FRAMES 2u -static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1); -static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx); +static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx); static void cacheCompile(StSpriteCacheT *e); static StSpriteCacheT *cacheFreeSlot(void); +static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1); static void levelCelsEvict(void); static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); @@ -173,26 +173,7 @@ static bool prewarmReport(uint16_t expect); static bool spriteOnScreen(int16_t px, int16_t py); static void starPaletteApply(void); static void tileFromChunky(jlTileT *out, const uint8_t *chunky); - - -// Paint a VIC sprite bitmap onto the scratch surface's top-left 3x3 -// tiles from the shared cel expansion (stCels.c, the same one the -// offline baker uses), so a cel built here for the interpreter fallback -// matches the precompiled .spc byte for byte. -static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) { - uint8_t blob[ST_CEL_BYTES]; - jlTileT tile; - uint8_t tx; - uint8_t ty; - - stCelBlob(bm, multi, color, mc0, mc1, blob); - for (ty = 0u; ty < ST_SPRITE_TILES; ty++) { - for (tx = 0u; tx < ST_SPRITE_TILES; tx++) { - tileFromChunky(&tile, &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]); - jlTilePaste(gRender.scratch, tx, ty, &tile); - } - } -} +static bool tileHasPixels(const uint8_t *chunky); // Compile a cached cel if it is not already, backing off after a failure @@ -210,9 +191,10 @@ static void cacheCompile(StSpriteCacheT *e) { } -// The JoeyLib sprite for hardware sprite `idx` of the current frame, -// built on first use from its VIC bitmap and colours. -static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { +// The cached cel for hardware sprite `idx` of the current frame, built +// on first use from its VIC bitmap and colours; NULL when there is +// nothing to draw. +static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) { uint8_t ptr = sim->frame.ptr[idx]; uint8_t color = drawColor(sim, idx); uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); @@ -227,14 +209,14 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { // Most frames a hardware sprite shows the cel it showed last time. if (e != 0 && e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { e->lastUse = gRender.cacheStamp; - return e->sprite; + return e; } for (k = 0u; k < ST_SPRITE_CACHE; k++) { e = &gRender.cache[k]; if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { e->lastUse = gRender.cacheStamp; gRender.lastHit[idx] = e; - return e->sprite; + return e; } // Pinned entries (prewarmed cab/exhaust/passenger cels) are the // costly-to-recompile ones and are never evicted; the least- @@ -264,9 +246,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { slot->used = false; slot->compiled = false; } - buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); - slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); - if (slot->sprite == 0) { + if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) { return 0; } // Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a @@ -293,7 +273,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { slot->used = true; slot->pinned = gPrewarming; gRender.lastHit[idx] = slot; - return slot->sprite; + return slot; } @@ -312,9 +292,61 @@ static const uint8_t kCellRow[125] = { }; -// Column of a cell whose row is known: cell - row * 40 through a table, -// because the two-shift form of the multiply came back as a __mulhi3 -// library call on the 65816. +// Build a VIC bitmap's cel into a cache entry from the shared cel +// expansion (stCels.c, the same one the offline baker uses), cropped to +// the tiles that hold pixels. Most VIC sprites leave whole tiles of their +// 24x21 frame empty, and every one of those would otherwise be saved, +// drawn, restored and presented each time the sprite moves. A bitmap +// with no pixels keeps the whole frame. +// +// The tiles go onto the scratch surface last to first: on the planar +// ports tileFromChunky converts through the scratch's top-left tile, so +// that tile has to be the one written last. +static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) { + uint8_t blob[ST_CEL_BYTES]; + jlTileT tile; + uint8_t txMin = ST_SPRITE_TILES; + uint8_t txMax = 0u; + uint8_t tyMin = ST_SPRITE_TILES; + uint8_t tyMax = 0u; + uint8_t tx; + uint8_t ty; + + stCelBlob(bm, multi, color, mc0, mc1, blob); + ty = ST_SPRITE_TILES; + while (ty > 0u) { + ty--; + tx = ST_SPRITE_TILES; + while (tx > 0u) { + const uint8_t *chunk; + + tx--; + chunk = &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]; + tileFromChunky(&tile, chunk); + jlTilePaste(gRender.scratch, tx, ty, &tile); + if (tileHasPixels(chunk)) { + txMin = (tx < txMin) ? tx : txMin; + txMax = (tx > txMax) ? tx : txMax; + tyMin = (ty < tyMin) ? ty : tyMin; + tyMax = (ty > tyMax) ? ty : tyMax; + } + } + } + if (txMin > txMax) { + txMin = 0u; + tyMin = 0u; + txMax = ST_SPRITE_TILES - 1u; + tyMax = ST_SPRITE_TILES - 1u; + } + e->offX = (uint8_t)(txMin * TILE_PIXELS_PER_SIDE); + e->offY = (uint8_t)(tyMin * TILE_PIXELS_PER_SIDE); + e->w = (uint8_t)((txMax - txMin + 1u) * TILE_PIXELS_PER_SIDE); + e->h = (uint8_t)((tyMax - tyMin + 1u) * TILE_PIXELS_PER_SIDE); + e->sprite = jlSpriteCreateFromSurface(gRender.scratch, (int16_t)e->offX, (int16_t)e->offY, (uint8_t)(e->w / TILE_PIXELS_PER_SIDE), (uint8_t)(e->h / TILE_PIXELS_PER_SIDE)); + return e->sprite != 0; +} + + // True for a cel that is MEANT to be built here rather than pre-listed, // so the warning below stays about real misses. Only the title screen's // decorations qualify: logoColorCycle recolours them every cycle step, @@ -335,6 +367,9 @@ static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) { } +// Column of a cell whose row is known: cell - row * 40 through a table, +// because the two-shift form of the multiply came back as a __mulhi3 +// library call on the 65816. static uint8_t cellCol(uint16_t cell, uint8_t row) { static const uint16_t kRowBase[ST_SCREEN_ROWS] = { 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, @@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) { } return; } - // A plain loop, not memset: 25 entries do not pay for the 65816 - // libc's far-called byte loop. + // One loop over both 25-entry arrays. for (row = 0u; row < ST_SCREEN_ROWS; row++) { gRender.dirtyColMin[row] = 0xFFu; gRender.dirtyColMax[row] = 0u; @@ -688,6 +722,19 @@ static void tileFromChunky(jlTileT *out, const uint8_t *chunky) { } +// True when a chunky 8x8 tile has any non-transparent (non-zero) pixel. +static bool tileHasPixels(const uint8_t *chunky) { + uint8_t k; + + for (k = 0u; k < TILE_BYTES; k++) { + if (chunky[k] != 0u) { + return true; + } + } + return false; +} + + // --------------------------------------------------------------------------- // Public // --------------------------------------------------------------------------- @@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { } } if (gRender.starMode) { - // The warp is the only thing animating here, and it animates in - // the palette: no cells change, no sprite moves for it, so the - // present that ends this frame copies no pixels at all. - gRender.starTick++; - if (gRender.starTick >= ST_STAR_STEP_FRAMES) { - gRender.starTick = 0u; - gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); + // The warp animates in the palette: no cells change and no sprite + // moves for it. The lit step follows the intro's own frame count, + // so the stars keep the C64's speed whatever the frame rate. + uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u)); + + if (phase != gRender.starPhase) { + gRender.starPhase = phase; starPaletteApply(); } } @@ -752,17 +799,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { for (k = ST_HW_SPRITES; k > first; k--) { StDrawnT *d = &gRender.slot[k - 1u]; if (d->drawn) { - jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); + if (d->trusted) { + jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]); + } else { + jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); + } d->drawn = false; } } paintCells(stage, sim); for (k = first; k < ST_HW_SPRITES; k++) { - uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); - StDrawnT *d = &gRender.slot[k]; - jlSpriteT *sp; - int16_t px; - int16_t py; + uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); + StDrawnT *d = &gRender.slot[k]; + StSpriteCacheT *e; + int16_t px; + int16_t py; if ((sim->frame.enableMask & kStBit[idx]) == 0u) { continue; } @@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { if (!spriteOnScreen(px, py)) { continue; } - sp = cachedSprite(sim, idx); - if (sp == 0) { + e = cachedSprite(sim, idx); + if (e == 0) { continue; } - jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]); + px = (int16_t)(px + e->offX); + py = (int16_t)(py + e->offY); + // A compiled cel wholly on the stage takes the trusted calls, + // which skip the per-call geometry checks; one that is clipped + // or interpreted needs the checked ones, and so does its restore. + d->trusted = e->compiled && px >= 0 && py >= 0 && px <= (int16_t)(SURFACE_WIDTH - e->w) && py <= (int16_t)(SURFACE_HEIGHT - e->h); + if (d->trusted) { + jlSpriteSaveAndDrawTrusted(stage, e->sprite, px, py, &gRender.backup[k]); + } else { + jlSpriteSaveAndDraw(stage, e->sprite, px, py, &gRender.backup[k]); + } d->drawn = true; d->x = sim->frame.x[idx]; d->y = sim->frame.y[idx]; @@ -824,14 +885,6 @@ void stRenderInit(jlSurfaceT *stage) { } -// Build (and compile) the cels the game draws every screen -- cab, -// exhaust, passenger, wreck, warp, intro cab and star -- up front, so -// no frame pays for a sprite build mid-play. A bar on the stage shows -// progress on the slower ports. -// Load the offline-baked, already-compiled cel bank into the pinned -// cache so no cel is JIT-compiled at boot (each 65816 compile costs -// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly -// as the play lookups: system ptr (level 0xFF), the level-wide colours. // The first unused cache slot, or NULL when the cache is full. static StSpriteCacheT *cacheFreeSlot(void) { uint8_t k; @@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) { } if (got == n) { slot->sprite = cels[k]; - slot->compiled = true; + slot->offX = 0u; + slot->offY = 0u; + slot->w = ST_SPRITE_PX; + slot->h = ST_SPRITE_PX; + slot->compiled = (jlSpriteCompiledSize(cels[k]) != 0u); } else { const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr); if (bm == 0) { continue; } - buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1); - slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); - if (slot->sprite == 0) { + if (!celCreate(slot, bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1)) { continue; } slot->compiled = false; @@ -975,6 +1030,10 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) { } +// Load the offline-baked, already-compiled cel bank into the pinned +// cache so no cel is JIT-compiled at boot (each 65816 compile costs +// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly +// as the play lookups: system ptr (level 0xFF), the level-wide colours. // Returns false if the .spc is missing or built for another target/shift // count (a stale bake), so the caller falls back to the runtime JIT // prewarm below. Every port stages its own bake (make/.mk). @@ -1002,6 +1061,10 @@ static bool loadPrecompiledCels(void) { e->mc0 = kStCels[i].mc0; e->mc1 = kStCels[i].mc1; e->level = 0xFFu; + e->offX = 0u; + e->offY = 0u; + e->w = ST_SPRITE_PX; + e->h = ST_SPRITE_PX; e->used = true; e->pinned = true; e->lastUse = 0u; @@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) { jlStagePresent(); } + +// Build (and compile) the cels the game draws every screen -- cab, +// exhaust, passenger, wreck and intro cab -- up front, so no frame pays +// for a sprite build mid-play. A bar on the stage shows progress on the +// slower ports. bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; - uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u); + uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs)); uint8_t done = 0u; bool ok; uint8_t k; @@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { done++; loadingBar(stage, done, total); } - // The intro star is a hires sprite in colour 7. Build it as a moving - // (idx <= 2) cel so it compiles: the level-name intro draws seven of - // them every frame, and the interpreter cannot keep up. - sim->frame.multiMask = 0u; - sim->frame.ptr[2] = 0xDAu; - sim->frame.color[2] = 0x07u; - (void)cachedSprite(sim, 2u); sim->frame = saved; gPrewarming = false; ok = prewarmReport(total); @@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *sim) { // Turn on the level intro's palette-cycled starfield. Call it after // stRenderSceneChanged (which turns it back off) on entry to the level // intro. The cells were painted by stIntroEnter; from here the warp is -// one palette write every ST_STAR_STEP_FRAMES frames. +// one palette write every ST_STAR_FRAMES_PER_STEP intro frames. void stRenderIntroStars(void) { gRender.starMode = true; gRender.starPhase = 0u; - gRender.starTick = 0u; starPaletteApply(); } diff --git a/examples/spacetaxi/stSim.h b/examples/spacetaxi/stSim.h index 9f3f541..5991fe3 100644 --- a/examples/spacetaxi/stSim.h +++ b/examples/spacetaxi/stSim.h @@ -100,8 +100,9 @@ // pixel rows. // // The contract between the two halves is just this colour range: -// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is -// the centre), and the renderer cycles that range. These are C64 colour +// stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its +// ray's flight (offset by the ray's launch delay), and the renderer +// cycles that range. These are C64 colour // indices the intro screen never uses -- it draws in 0, 3 and, in demo // mode, 4 and 5; the cab sprite adds 2, 6 and 7. #define ST_STAR_RING_FIRST 8u @@ -112,12 +113,14 @@ // something else must fail to compile here instead of quietly making // the warp skip rings. typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; -// Ring 0 is the centre, which in a warp is the VANISHING POINT: those -// stars are the far ones and get the small glyph. The big glyph belongs -// to the outer rings, the stars about to rush past the viewer. -#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings -#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings -#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant +// A C64 star flies $20 frames ($4500) and the painted field marks every +// ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on +// that many intro frames (introStepCount): the warp keeps the C64's speed at +// any frame rate. +#define ST_INTRO_STAR_FLIGHT 0x20u +#define ST_STAR_FRAMES_PER_STEP (ST_INTRO_STAR_FLIGHT / ST_STAR_RING_COUNT) +// Every star is the small glyph: the C64's star is a 5x3 speck. +#define ST_STAR_GLYPH 0x2Eu // '.' // VIC sprite block pointers the game selects by name. The cab's bit 0 is // the landing gear, so each facing is a pair. @@ -309,7 +312,11 @@ typedef struct { uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped } StLevelT; -// One VIC sprite as the game sees it through its shadow tables. +// One VIC sprite as the game sees it through its shadow tables. Padded +// to eight bytes: the 65816 compiler indexes a 6-byte element through +// a call to its 32-bit multiply routine (sim is a far pointer), and +// every sim->spr[idx] in the game paid for one; a power-of-two stride +// is three shifts. typedef struct { uint8_t col; // $7175,X X low byte uint8_t msb; // $7185,X X high bit (any non-zero value = set) @@ -317,7 +324,9 @@ typedef struct { uint8_t enable; // $718E,X uint8_t ptr; // $7197,X block pointer uint8_t color; // $719F,X + uint8_t strideToPow2[2]; } StSpriteT; +typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1]; // The frame the VIC last displayed ($4253 marshal + $4293 flush): the // renderer draws this and the collision test evaluates it. @@ -542,6 +551,7 @@ typedef struct { uint8_t logoCycleAux; // $48A5 uint8_t logoDiv; // $48A6 uint8_t introLoopCount; // $4508 + uint8_t introStepCount; // port: intro frames run, for the painted warp uint8_t introCabDx; // $450A uint8_t introCabDxTimer; // $450B uint8_t spriteMc0; // $D025 diff --git a/examples/spacetaxi/stTitle.c b/examples/spacetaxi/stTitle.c index ecc37d3..20beee3 100644 --- a/examples/spacetaxi/stTitle.c +++ b/examples/spacetaxi/stTitle.c @@ -14,8 +14,10 @@ // // The seven star sprites are replaced by a static starfield whose // colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and -// how. The cab still flies, and it is still what ends the intro, so the -// sequence keeps its length and its cue; only the warp changes. +// how. The stars sit on the seven rays the C64's stars fly, where those +// stars are every few frames of a flight, so the field plays the +// original's paths. The cab still flies, and it is still what ends the +// intro, so the sequence keeps its length and its cue. #include @@ -40,42 +42,35 @@ static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!"; #define ST_TITLE_SPARKLE_TICKS 0x5Au #define ST_TITLE_HOVER_COL 0x28u #define ST_INTRO_STAR_PTR 0xDAu +#define ST_INTRO_STAR_COL 0xAAu // where a star launches ($4523) +#define ST_INTRO_STAR_ROW 0x8Cu +#define ST_INTRO_STAR_DELAYS 0x20u // launch delays drawn from 0..$1F ($44F8) #define ST_INTRO_CAB_END_ROW 0x94u #define ST_INTRO_CAB_DX_RELOAD 0x28u -// The palette-cycled starfield. Seven stars per ring so the bright band -// stays the same weight all the way out, placed at fixed pseudo-random -// angles and clear of the three lines the intro screen writes (the level -// name on row 12, and the demo hints on rows 3 and 24). -typedef struct { - uint8_t col; - uint8_t row; - uint8_t ring; -} StStarT; +// The palette-cycled starfield. The C64 flies one star sprite along each +// of seven rays at these velocities ($450C / $4513, pixels a frame) for +// $20 frames ($4500), then launches it again from the start. The painted +// field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP +// frames; the renderer lights one step of every ray at a time, so a star +// hops out along its ray. A star is drawn where the sprite's image ($DA: +// a 5x3 speck centred on pixel 11, 9 of the sprite) would be. +#define ST_INTRO_STARS 7u +#define ST_INTRO_STAR_IMAGE_X 11 // the speck's centre in its sprite +#define ST_INTRO_STAR_IMAGE_Y 9 +#define ST_SCREEN_WIDTH_PX ((int16_t)(ST_SCREEN_COLS * 8u)) +#define ST_SCREEN_HEIGHT_PX ((int16_t)(ST_SCREEN_ROWS * 8u)) +#define ST_CELL_SHIFT 3 // 8 pixels a cell -static const StStarT kIntroStar[] = { - { 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, - { 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u }, - { 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u }, - { 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u }, - { 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u }, - { 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u }, - { 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u }, - { 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u }, - { 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u }, - { 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u }, - { 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u }, - { 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u }, - { 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u }, - { 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u }, -}; -#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0])) +static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 }; +static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 }; static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); static void logoColorCycle(StSimT *sim); static void logoFlip(StSimT *sim); -static void paintStarfield(StSimT *sim); +static void paintStarfield(StSimT *sim, const uint8_t *delay); +static bool rowHasText(const StSimT *sim, uint8_t row); // $4113 @@ -124,21 +119,60 @@ static void logoFlip(StSimT *sim) { } -// Paint the level-intro starfield into screen RAM, once. A star's cell -// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- -// the renderer animates the warp by cycling those colours, which costs -// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. -static void paintStarfield(StSimT *sim) { - const StStarT *star; - uint8_t one[2]; - uint8_t k; +// Paint the level-intro starfield into screen RAM, once. A star's cell is +// coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by +// its ray's launch delay so the rays fire out of step as on the C64, and is +// never touched again -- the renderer animates the warp by cycling those +// colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h). +// Step 0 is the launch point, which every ray shares, and steps off the +// screen are left out, as are rows that carry the intro's text: on the C64 +// the stars fly over the text, but a cell star there reads as part of it -- +// a gap in the level name, or a full stop after the demo hint. +static void paintStarfield(StSimT *sim, const uint8_t *delay) { + static const uint8_t one[2] = { ST_STAR_GLYPH, 0u }; // stSimDrawText stops below screen code 6 + bool textRow[ST_SCREEN_ROWS]; + int16_t originX; + int16_t originY; + int16_t x; + int16_t y; + uint8_t row; + uint8_t ray; + uint8_t step; + uint8_t slot; - one[1] = 0u; // stSimDrawText stops below screen code 6 - for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { - star = &kIntroStar[k]; - one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; - stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring)); + // Before any star goes down, or the first star on a row would hide the + // rest of that row's. + for (row = 0u; row < ST_SCREEN_ROWS; row++) { + textRow[row] = rowHasText(sim, row); } + originX = (int16_t)(ST_INTRO_STAR_COL - ST_SPRITE_X_ORIGIN + ST_INTRO_STAR_IMAGE_X); + originY = (int16_t)(ST_INTRO_STAR_ROW - ST_SPRITE_Y_ORIGIN + ST_INTRO_STAR_IMAGE_Y); + for (ray = 0u; ray < ST_INTRO_STARS; ray++) { + for (step = 1u; step < ST_STAR_RING_COUNT; step++) { + x = (int16_t)(originX + kIntroStarDx[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP)); + y = (int16_t)(originY + kIntroStarDy[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP)); + if (x < 0 || x >= ST_SCREEN_WIDTH_PX || y < 0 || y >= ST_SCREEN_HEIGHT_PX || textRow[y >> ST_CELL_SHIFT]) { + continue; + } + slot = (uint8_t)((step + delay[ray] / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u)); + stSimDrawText(sim, (uint8_t)(x >> ST_CELL_SHIFT), (uint8_t)(y >> ST_CELL_SHIFT), one, (uint8_t)(ST_STAR_RING_FIRST + slot)); + } + } +} + + +// True when screen row `row` holds anything but blanks. +static bool rowHasText(const StSimT *sim, uint8_t row) { + const uint8_t *cell; + uint8_t col; + + cell = &sim->screen[(uint16_t)row * ST_SCREEN_COLS]; + for (col = 0u; col < ST_SCREEN_COLS; col++) { + if (cell[col] != ST_CHAR_SPACE) { + return true; + } + } + return false; } @@ -148,6 +182,7 @@ static void paintStarfield(StSimT *sim) { // $4523..$4665 -- set up the level-name screen. void stIntroEnter(StSimT *sim, const StLevelT *level) { + uint8_t delay[ST_INTRO_STARS]; uint8_t k; sim->level = level; @@ -161,20 +196,19 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) { sim->spr[7].row = 0xE4u; sim->introCabDxTimer = 0x14u; sim->introCabDx = 0x01u; - for (k = 0u; k < 7u; k++) { - uint8_t idx = (uint8_t)(6u - k); - sim->spr[idx].row = 0x8Cu; + for (k = 0u; k < ST_INTRO_STARS; k++) { + uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k); + sim->spr[idx].row = ST_INTRO_STAR_ROW; sim->spr[idx].msb = 0u; - sim->spr[idx].col = 0xAAu; + sim->spr[idx].col = ST_INTRO_STAR_COL; sim->spr[idx].color = 0x07u; sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].enable = 0u; - // $44F8/$4500 held each star's spawn timer and reload. Nothing - // reads them now that the warp is painted rather than flown, but - // the seven RNG draws MUST stay: they advance the shared RNG and - // the level that follows draws from it, so dropping them would - // change the game that comes after the intro. - (void)stSimRng(sim, 0x20u); + // $44F8: each star's launch delay. The painted field uses it to + // put the rays out of step, and the seven RNG draws MUST stay in + // any case: they advance the shared RNG and the level that + // follows draws from it. + delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS); } sim->borderColor = 0u; sim->bgColor = 0u; @@ -188,12 +222,13 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) { sim->charDirtyAll = true; sim->charDirtyCount = 0u; stSimDrawText(sim, 10u, 12u, level->name, 3u); - paintStarfield(sim); if (sim->demoMode != 0u && sim->postMortem == 0u) { stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); } + paintStarfield(sim, delay); sim->introLoopCount = 0u; + sim->introStepCount = 0u; } @@ -203,6 +238,7 @@ bool stIntroStep(StSimT *sim) { uint8_t k; stSimMarshal(sim); + sim->introStepCount++; sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); if ((sim->introLoopCount & 1u) == 0u) { diff --git a/examples/uber/uber.c b/examples/uber/uber.c index e75477e..c9c0aca 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -170,6 +170,7 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t * uint16_t now; uint32_t startMillis; uint32_t endMillis; + uint16_t afterFirst; bool slowOp; count = 0UL; @@ -178,13 +179,20 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t * startFrame = jlFrameCount(); startMillis = jlMillisElapsed(); - // Calibrate: jlWaitVBL aligned us to a tick edge, so a sub-frame op - // cannot see the counter advance during a single call. If the first - // call DOES advance it, the op is slower than one frame -- poll the - // clock every call so a batch cannot overrun the window. + // Calibrate: an op slower than one frame advances the counter on + // every call -- poll the clock every call then, so a batch cannot + // overrun the window. One advance proves nothing: jlWaitVBL syncs to + // the display's retrace, but a port may count frames from another + // clock (DOS derives them from its millisecond clock), so a fast op's + // first call can straddle a frame edge, and polling every call of a + // fast op costs it up to 40% (rows on DOS came out 0.6x at random). A + // sub-half-frame op cannot advance the counter on two calls running. op(); count++; - slowOp = ((uint16_t)(jlFrameCount() - startFrame) != 0u); + afterFirst = jlFrameCount(); + op(); + count++; + slowOp = (afterFirst != startFrame) && (jlFrameCount() != afterFirst); for (;;) { now = jlFrameCount(); @@ -1032,6 +1040,84 @@ static void PROBE_ATTR op_probeVgaCheck(void) { #endif +#ifdef JOEYLIB_PLATFORM_DVX +extern uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); + +// P13: DVX display-side parity, P12's battery on the DVX present, which +// expands dirty rows through per-palette tables into the window's content +// buffer (the desktop's pixel format, at the window's scale) or the +// full-screen backbuffer. The golden hashes gate the stage, never those. +// dvxPortProbeDisplayMismatch derives every pixel from the stage's own +// palette entries (not the present's tables) and counts the ones that +// differ. A strip of one-pixel stripes in all 16 colours stays on screen, +// so every table entry, and both pixels of a byte, are checked; the small +// rect is on odd pixels. After P12's steps (tall band, small inner rect, +// settle, idle) two more work the tables: the strip's rows moved to another +// palette (an SCB change: every row again), then that palette changed +// (its rows again; on the whole screen at 8 bits only the screen's +// palette); then the SCBs and the palette go back. Every count must be 0. +#define PROBE_DVX_PALETTE 1u +#define PROBE_DVX_STRIP_H 10 +#define PROBE_DVX_SCB_LAST 99u + +static void PROBE_ATTR op_probeDvxDisplayCheck(void) { + static const uint16_t probePalette[SURFACE_COLORS_PER_PALETTE] = { + 0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF, + 0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444 + }; + static const uint16_t changedPalette[SURFACE_COLORS_PER_PALETTE] = { + 0x000, 0x123, 0x246, 0x369, 0x48C, 0x5AF, 0x6C2, 0x7E5, + 0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8 + }; + uint8_t savedScb[SURFACE_HEIGHT]; + uint16_t savedPalette[SURFACE_COLORS_PER_PALETTE]; + uint32_t afterB; + uint32_t afterSettle; + uint32_t afterIdle; + uint32_t afterScb; + uint32_t afterPalette; + uint32_t afterRestore; + uint32_t compared; + int16_t x; + uint16_t y; + + for (y = 0; y < SURFACE_HEIGHT; y++) { + savedScb[y] = jlScbGet(gStage, y); + } + jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); + jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette); + + jlSurfaceClear(gStage, 3); + for (x = 0; x < SURFACE_WIDTH; x++) { + jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % SURFACE_COLORS_PER_PALETTE)); + } + jlStagePresent(); + jlFillRect(gStage, 0, 50, 320, 100, 3); + jlStagePresent(); + jlFillRect(gStage, 101, 61, 37, 9, 9); + jlStagePresent(); + afterB = dvxPortProbeDisplayMismatch(gStage, &compared); + jlStagePresent(); + afterSettle = dvxPortProbeDisplayMismatch(gStage, &compared); + jlStagePresent(); + afterIdle = dvxPortProbeDisplayMismatch(gStage, &compared); + jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE); + jlStagePresent(); + afterScb = dvxPortProbeDisplayMismatch(gStage, &compared); + jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette); + jlStagePresent(); + afterPalette = dvxPortProbeDisplayMismatch(gStage, &compared); + for (y = 0; y < SURFACE_HEIGHT; y++) { + jlScbSet(gStage, y, savedScb[y]); + } + jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); + jlStagePresent(); + afterRestore = dvxPortProbeDisplayMismatch(gStage, &compared); + jlLogF("UBER-PROBE: dvxDisplayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterRestore=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterRestore, (unsigned long)compared); +} +#endif + + #if defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_IIGS) // P7-4 (perf-hunt-2 rank 4): gameFrameClean component split. The // golden row = 3 copyRect window erases + 3 sprite draws + present; @@ -1928,6 +2014,9 @@ static void __attribute__((noinline)) runAllTests(void) { #ifdef JOEYLIB_PLATFORM_DOS op_probeVgaCheck(); #endif +#ifdef JOEYLIB_PLATFORM_DVX + op_probeDvxDisplayCheck(); +#endif #endif jlLogF("UBER: ----- end -----"); diff --git a/include/joey/core.h b/include/joey/core.h index 112d1dd..3ad760c 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -46,9 +46,9 @@ void *jlAlloc(uint32_t bytes); // Release a buffer returned by jlAlloc. NULL is accepted and ignored. void jlFree(void *p); -// memcpy for big buffers. Same contract as memcpy (no overlap), but the -// IIgs runs it as MVN block moves (7 cycles a byte) instead of the libc -// byte loop, split at bank boundaries by the port. +// memcpy with a 32-bit count. Same contract as memcpy (no overlap); every +// port runs its libc memcpy (on the IIgs MVN block moves at 7 cycles a +// byte, split at bank boundaries). void jlMemCopy(void *dst, const void *src, uint32_t bytes); // Block the calling thread until the next display vertical blank. diff --git a/include/joey/platform.h b/include/joey/platform.h index 9d0a331..dae7512 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -78,20 +78,11 @@ #define JOEYLIB_NATIVE_CHUNKY #define JOEYLIB_PLATFORM_NAME "MS-DOS" #elif defined(JOEYLIB_PLATFORM_X68000) - // BRING-UP STATE: chunky stage in main RAM, expanded to GVRAM by jlpPresent. - // This is deliberately the slow-but-correct path -- it makes every one of the - // 772 lines of src/generic work unmodified, so the port renders correctly - // before a single native primitive exists. - // - // The native storage model is NOT yet decided. The two candidates are GVRAM - // (2 bytes/pixel, trivial primitives) and the text plane at $E00000 (four - // 1bpp planes 128 KB apart -- byte-for-byte the Amiga layout with stride 40 - // replaced by 128, so src/amiga adapts by changing one constant). Measured - // under MAME with the wait-state patch: TVRAM costs 1.92 cycles/access - // against GVRAM's 1.03, but needs only 0.25 word-writes per 16-colour pixel - // against GVRAM's 1.0 -- so the text plane still wins ~2x. See - // claudeDocs/x68000_mame_crtc_timing.md. Do not switch to JOEYLIB_NATIVE_PLANAR - // until that is settled and measured. + // The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c, + // the Amiga's layout), and a present copies its dirty rows into the text + // plane at $E00000, which is four 1bpp planes too. The text plane has one + // 16-colour palette, so per-line SCB palettes are reloaded as the beam scans + // by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s). #define JOEYLIB_CPU_68000 #define JOEYLIB_ENDIAN_BIG #define JOEYLIB_NATIVE_PLANAR @@ -136,9 +127,9 @@ #define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else #define JL_HAS_TILE_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else #define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro - #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: inline word copy for - // short spans (llvm-mos memcpy is a slow far - // byte-loop, finding #79), function + #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: iigsCopyRectRows + // [dp],y row copies (short rows lose to + // memcpy's per-call setup), function #define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro #define JL_HAS_SPRITE_SAVE // noop macro (codegen path saves) #define JL_HAS_SPRITE_RESTORE // noop macro diff --git a/make/amiga.mk b/make/amiga.mk index eb324a7..082230e 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -279,10 +279,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ -$(SPRITEBAKE_BIN): +$(SPRITEBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-amiga -$(STAXIBAKE_BIN): +$(STAXIBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake # The cel bank is generated from the game's cel table (stCels.c). diff --git a/make/atarist.mk b/make/atarist.mk index a22fbbd..5da213e 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -144,7 +144,17 @@ DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx # other targets' bakes it must refuse, and the expected pixels (common.mk). TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk -.PHONY: all atarist clean-atarist clean +# The dirty-band marker's on-target check: tests/host/dirtyMarkHost.c against +# src/m68k/stageBands.s, the asm the Amiga and X68000 share. Not a shipped +# program, so it builds beside bin/; scripts/verify-atarist-dirtymark.sh runs +# it under Hatari. The host check runs 200,000 scenarios; this many takes +# under a minute of emulated time. +DIRTYMARK_SRC := $(REPO_DIR)/tests/host/dirtyMarkHost.c +DIRTYMARK_BIN := $(BUILD)/test/DIRTYMRK.PRG +DIRTYMARK_SCENARIOS := 4000ul +DIRTYMARK_LOG := DIRTYMRK.TXT + +.PHONY: all atarist clean-atarist clean atarist-dirtymark atarist-verify-dirtymark all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @@ -224,6 +234,15 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +$(DIRTYMARK_BIN): $(DIRTYMARK_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MARK_LOG='"$(DIRTYMARK_LOG)"' $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) + +atarist-dirtymark: $(DIRTYMARK_BIN) + +atarist-verify-dirtymark: $(DIRTYMARK_BIN) + $(REPO_DIR)/scripts/verify-atarist-dirtymark.sh + $(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) @@ -271,10 +290,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ -$(SPRITEBAKE_BIN): +$(SPRITEBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-atarist -$(STAXIBAKE_BIN): +$(STAXIBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake # The cel bank is generated from the game's cel table (stCels.c). diff --git a/make/common.mk b/make/common.mk index 1ccae8b..b4ee01c 100644 --- a/make/common.mk +++ b/make/common.mk @@ -60,3 +60,16 @@ GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic # command-line hook for extra flags on every port (make EXTRA_CFLAGS=-DX); # make/iigs.mk, which does not use COMMON_CFLAGS, honours it too. COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 $(EXTRA_CFLAGS) + + +# Rules that hand a host tool's build to make/tools.mk depend on FORCE, so +# tools.mk -- which owns each tool's source list -- is always asked; it does +# nothing when the tool is current. Without it an existing tool binary looked +# up to date forever, and edits to its sources never reached it (staxibake +# kept running a stale binary after stCels.c changed). Defining it must not +# change any port's default goal (most include this file before their `all`), +# so the goal in force is saved and put back around it. +COMMON_DEFAULT_GOAL := $(.DEFAULT_GOAL) +.PHONY: FORCE +FORCE: +.DEFAULT_GOAL := $(COMMON_DEFAULT_GOAL) diff --git a/make/dos.mk b/make/dos.mk index d26536b..df66b28 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -300,10 +300,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ -$(SPRITEBAKE_BIN): +$(SPRITEBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-dos -$(STAXIBAKE_BIN): +$(STAXIBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake # The cel bank is generated from the game's cel table (stCels.c). @@ -320,7 +320,7 @@ $(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc @mkdir -p $(dir $@) cp $< $@ -$(MKSTLEVEL_BIN): +$(MKSTLEVEL_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk mkstlevel $(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod diff --git a/make/dvx.mk b/make/dvx.mk index 78df4db..330b4a8 100644 --- a/make/dvx.mk +++ b/make/dvx.mk @@ -130,6 +130,19 @@ staxi_HELP := Space Taxi: fly the taxi, pick up the fares, take them where t APPS := hello pattern draw keys serial sertest save tilebank joy sprite audio uber adv adv2 agi staxi +# UBER's probe build (part of all; scripts/verify-dvx-display.sh): UBER +# with its UBER_PROBE rows, the display parity row among them, as a program +# of its own packaged apart in build/dvx/probe, so neither bin/ (joey-run's +# tree) nor the goldened UBER changes. -DUBER_FRAMES=1 as the IIgs probe +# build: the run is wanted for its probe rows, not its timings. +# (Its name, like every program's, is a DOS 8.3 name: DVX runs on DOS.) +uberprob_SRCS := $(uber_SRCS) +uberprob_TITLE := Uber Probe +uberprob_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. +uberprob_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 +uberprob_BIN := $(BUILD)/probe +PROBE_APPS := uberprob + # Data the examples read from DATA/ beside the program: AUDIO its music and # sound effect; TILEBANK the DOS bake of the tile-bank fixture (the DOS # target byte: DVX shows the DOS stage), every other target's bake it must @@ -143,14 +156,16 @@ AUDIO_DATA_FILES := $(audio_DATA)/test.mod $(audio_DATA)/test.sfx TILEBANK_OWN := $(tilebank_DATA)/tilebank/own.tbk DOS_STAXI_DATA := $(REPO_DIR)/build/dos/bin/STAXI/DATA -.PHONY: all dvx dvx-lib dvx-apps clean-dvx clean $(addprefix app-,$(APPS)) +.PHONY: all dvx dvx-lib dvx-apps dvx-probe clean-dvx clean $(addprefix app-,$(APPS) $(PROBE_APPS)) -all dvx: dvx-lib dvx-apps +all dvx: dvx-lib dvx-apps dvx-probe dvx-lib: $(LIB) dvx-apps: $(addprefix app-,$(APPS)) +dvx-probe: $(addprefix app-,$(PROBE_APPS)) + $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ @@ -181,7 +196,8 @@ $(LIB): $(LIB_OBJS) # then let sdk.mk build and package it, and copy the package into bin/. # sdk.mk rebuilds only what changed; the project files are rewritten only # when their contents change, so an unchanged program is not relinked. -# $(1) = the program's name. +# $(1) = the program's name; $(1)_CFLAGS, if any, are added to its flags, +# and $(1)_BIN, if set, is where it goes instead of bin/. define appRule app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) @mkdir -p $(APPSDIR)/$(1) @@ -194,7 +210,7 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) 'VENDOR = $(VENDOR)' \ 'LIBS = $(LIB)' \ 'DATA = $$($(1)_DATA)' \ - "CFLAGS = $(APP_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \ + "CFLAGS = $(APP_CFLAGS) $$($(1)_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \ 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/Makefile @printf '%s\n' \ '.topic $(1).overview' \ @@ -217,11 +233,11 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) 'description text "JoeyLib example: $$($(1)_TITLE)"' \ | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res $$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package - @mkdir -p $(BINDIR) - rm -rf $(BINDIR)/apps/$(VENDOR)/$(1) - cp -R $(APPSDIR)/$(1)/pkg/apps $(BINDIR)/ + @mkdir -p $$(or $$($(1)_BIN),$(BINDIR)) + rm -rf $$(or $$($(1)_BIN),$(BINDIR))/apps/$(VENDOR)/$(1) + cp -R $(APPSDIR)/$(1)/pkg/apps $$(or $$($(1)_BIN),$(BINDIR))/ endef -$(foreach a,$(APPS),$(eval $(call appRule,$(a)))) +$(foreach a,$(APPS) $(PROBE_APPS),$(eval $(call appRule,$(a)))) .PHONY: audio-data tilebank-data staxi-data audio-data: $(AUDIO_DATA_FILES) diff --git a/make/iigs.mk b/make/iigs.mk index 042ab78..13d2481 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -146,10 +146,10 @@ $(STAXI_IIGS_LEVELS) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs STAXIBAKE := $(REPO_DIR)/build/tools/staxibake -$(SPRITEBAKE_IIGS): +$(SPRITEBAKE_IIGS): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-iigs -$(STAXIBAKE): +$(STAXIBAKE): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c diff --git a/make/x68000.mk b/make/x68000.mk index 1bffaf0..72f94ba 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -39,8 +39,9 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \ LDFLAGS := -lm PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c) -# Raw interrupt shims (timer tick, SCB raster, SCC receive): each saves every -# register and returns with RTE, which a C function cannot. +# Raw interrupt shims (timer tick, frame count, SCB raster, SCC receive): each +# saves every register and returns with RTE, which a C function cannot. And +# the present's copy (x68kPresent.s). PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s) # Sprite codegen is the shared 68k PLANAR emitter, unchanged. Its @@ -57,6 +58,7 @@ LIB_OBJS := \ $(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(BUILD)/obj/68k/planarFill.o \ $(BUILD)/obj/68k/planarPlot.o \ + $(BUILD)/obj/68k/stageBands.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -133,7 +135,7 @@ STAXI_DIR := $(BINDIR)/STAXI STAXI_BIN := $(STAXI_DIR)/STAXI.X STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean +.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-serial-scb x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean all x68000: x68000-lib x68000-examples @@ -259,10 +261,10 @@ $(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels -$(SPRITEBAKE_BIN): +$(SPRITEBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000 -$(STAXIBAKE_BIN): +$(STAXIBAKE_BIN): FORCE $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c @@ -294,6 +296,11 @@ x68000-verify-mouse: $(BINDIR)/KEYS.X x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X $(REPO_DIR)/scripts/verify-x68000-serial-mouse.sh +# Serial + mouse under per-line palette changes: the serial + mouse gate with +# examples/sermouse built for a dense SCB at 9600, 19200 and 38400 (~3 min). +x68000-verify-serial-scb: $(LIB) $(LIBXMP_AR) + $(REPO_DIR)/scripts/verify-x68000-serial-scb.sh + # Joystick gate: every direction, both triggers and every release on both # ports, held through MAME's pads and read back from JOY.X's display (~10 s). x68000-verify-joy: $(BINDIR)/JOY.X diff --git a/scripts/bench-dvx.sh b/scripts/bench-dvx.sh index f28bfbb..88cc065 100755 --- a/scripts/bench-dvx.sh +++ b/scripts/bench-dvx.sh @@ -18,8 +18,15 @@ # 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was # set: the same hashes, the full-screen present's timings. # +# --probe runs UBER's probe build instead (make dvx builds it: its UBER_PROBE +# rows, the display parity row among them, at one frame per timed op), its +# log copied to build/dvx/probe/joeylog.txt; scripts/verify-dvx-display.sh +# judges it. Either way DVX.LOG is copied beside the log, as dvx.log. The +# desktop is dvxGate.sh's (DVX_VIDEO_W/H/BPP). +# # scripts/bench-dvx.sh # default 600 s timeout # scripts/bench-dvx.sh --full-screen # on the whole screen +# scripts/bench-dvx.sh --probe # the probe build (with --full-screen too) # TIMEOUT=900 scripts/bench-dvx.sh # override set -euo pipefail @@ -31,19 +38,35 @@ joeyNeed DOSBOX dosbox joeyNeed DVX_ROOT dvx fullScreen=0 -if [ "${1:-}" = "--full-screen" ]; then - fullScreen=1 -elif [ $# -gt 0 ]; then - echo "usage: scripts/bench-dvx.sh [--full-screen]" >&2 - exit 2 -fi +program=uber +appDir=$(dvxAppDir uber) +out=$DVX_BIN/$DVX_LOG_NAME +for arg in "$@"; do + case "$arg" in + --full-screen) + fullScreen=1 + ;; + --probe) + program=uberprob + appDir=$DVX_PROBE_DIR/apps/$DVX_VENDOR/$program + out=$DVX_PROBE_DIR/$DVX_LOG_NAME + ;; + *) + echo "usage: scripts/bench-dvx.sh [--full-screen] [--probe]" >&2 + exit 2 + ;; + esac +done timeoutSeconds=${TIMEOUT:-600} # The emulator's hard cap, a little past the wait, whatever this script does. wallSeconds=$((timeoutSeconds + 30)) -out=$DVX_BIN/$DVX_LOG_NAME +dvxLogOut=${out%/*}/dvx.log doneLine="press any key to exit" -dvxNeedBuild uber +if [ ! -f "$appDir/$program.app" ]; then + echo "bench-dvx: $appDir/$program.app is missing; run: make dvx" >&2 + exit 2 +fi work=$(mktemp -d -t joeylib-bench-dvx.XXXXXX) guard="" @@ -56,20 +79,20 @@ cleanup() { } trap cleanup EXIT -dvxStage "$work/c" "$(dvxAppDir uber)" +dvxStage "$work/c" "$appDir" if [ "$fullScreen" = 1 ]; then - dvxFullScreen "$work/c" uber + dvxFullScreen "$work/c" "$program" fi dvxDosboxConf "$work/dosbox.conf" "$work/c" mkdir -p "$work/xdg" -rm -f "$out" +rm -f "$out" "$dvxLogOut" display=":$((RANDOM % 400 + 1700))" Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & xvfb=$! sleep 1 -echo "bench-dvx: running UBER in DVX headless (timeout ${timeoutSeconds}s)..." >&2 +echo "bench-dvx: running $program in DVX headless on ${DVX_VIDEO_W}x${DVX_VIDEO_H}x${DVX_VIDEO_BPP} (timeout ${timeoutSeconds}s)..." >&2 XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ timeout -s KILL "$wallSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & @@ -77,13 +100,13 @@ guard=$! dosbox=$(joeyChild "$guard" || true) start=$SECONDS -log=$(dvxAwaitLog "$work/c" uber "$doneLine" "$timeoutSeconds" "$guard" || true) +log=$(dvxAwaitLog "$work/c" "$program" "$doneLine" "$timeoutSeconds" "$guard" || true) waited=$((SECONDS - start)) if [ -z "$log" ]; then - log=$(dvxProgramLog "$work/c" uber) + log=$(dvxProgramLog "$work/c" "$program") fi if [ -z "$log" ]; then - echo "bench-dvx: FAIL -- UBER wrote no joeylog.txt (DVX.LOG follows)" >&2 + echo "bench-dvx: FAIL -- $program wrote no joeylog.txt (DVX.LOG follows)" >&2 dvxDvxLog "$work/c" >&2 exit 1 fi @@ -91,6 +114,7 @@ tr -d '\r' < "$log" > "$out" joeyStop "$dosbox" "$guard" 2> /dev/null dosbox="" guard="" +dvxDvxLogCopy "$work/c" "$dvxLogOut" if [ "$fullScreen" = 1 ] && ! dvxDvxLog "$work/c" 200 | grep -q "$DVX_FULL_SCREEN_LOG"; then echo "bench-dvx: FAIL -- --full-screen, but DVX.LOG has no \"$DVX_FULL_SCREEN_LOG\" (DVX.LOG follows)" >&2 diff --git a/scripts/check-dirtymark.sh b/scripts/check-dirtymark.sh new file mode 100755 index 0000000..51baee6 --- /dev/null +++ b/scripts/check-dirtymark.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# check-dirtymark.sh - Host-cc gate for the stage's dirty-band writers: the +# multi-row mark (surfaceMarkDirtyRows, whose four-row groups reuse a group's +# result for the next group that held the same bytes) and the whole-stage +# fills, held byte for byte to the plain per-row widen over randomized band +# states and marks of every height and phase (tests/host/dirtyMarkHost.c). +# +# This is the C marker, which the x86 ports run. The 68000 ports run its twin +# in src/m68k/stageBands.s; scripts/verify-atarist-dirtymark.sh runs the same +# harness against that under Hatari. A wrong mark is invisible to the UBER +# hashes (they hash the surface, not the bands), so this is where it shows. +# +# Built as a whole BLANK-port program (the check-blank.sh source set plus the +# harness). Exits nonzero on any mismatch. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cc=${CC:-cc} +work=$(mktemp -d -t joeylib-dirtymark.XXXXXX) +trap 'rm -rf "$work"' EXIT + +cd "$repo" +"$cc" -O2 -DJOEYLIB_PLATFORM_BLANK \ + -Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Wall -Wextra -Werror \ + src/core/*.c src/generic/*.c src/blank/blank.c tests/host/dirtyMarkHost.c \ + -o "$work/dirtyMarkHost" +"$work/dirtyMarkHost" diff --git a/scripts/check-fileroots.sh b/scripts/check-fileroots.sh new file mode 100755 index 0000000..84e4feb --- /dev/null +++ b/scripts/check-fileroots.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# check-fileroots.sh - Host-cc gate for the run-time file roots +# (src/core/fileRoot.h, JL_HAS_FILE_ROOTS, which the DVX port defines): with +# the flag, DATA/ is read under the port's read root and SAVES/ and +# joeylog.txt are written under its write root; without it the layout every +# other port has, all relative to the current directory, is unchanged. +# +# Builds the BLANK port (the check-blank.sh source set) plus +# tests/host/fileRootHost.c twice, with the POSIX save HAL flags check-save.sh +# uses: once with -DJL_HAS_FILE_ROOTS (the harness is the port: its roots are +# folders in a temp dir) and once without. Each runs in an empty current +# directory of its own, and the flagless program must not contain +# jlLogClose, which exists only under the flag. Exits nonzero on any failed +# check. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cc=${CC:-cc} +work=$(mktemp -d -t joeylib-fileroots.XXXXXX) +trap 'rm -rf "$work"' EXIT + +cd "$repo" +build() { + "$cc" -DJOEYLIB_PLATFORM_BLANK -DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE "$@" \ + -Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Wall -Wextra \ + src/core/*.c src/generic/*.c src/blank/blank.c tests/host/fileRootHost.c +} +build -DJL_HAS_FILE_ROOTS -o "$work/rootsHost" +build -o "$work/plainHost" + +if nm "$work/plainHost" | grep -qw jlLogClose; then + echo "check-fileroots: FAIL - jlLogClose is in the build without JL_HAS_FILE_ROOTS" >&2 + exit 1 +fi + +mkdir -p "$work/roots" "$work/plain" +fails=0 +( cd "$work/roots" && "$work/rootsHost" "$work/readRoot/" "$work/writeRoot/" ) || fails=1 +( cd "$work/plain" && "$work/plainHost" ) || fails=1 +if [ "$fails" != 0 ]; then + echo "check-fileroots: FAIL" >&2 + exit 1 +fi +echo "check-fileroots: PASS (roots honoured under JL_HAS_FILE_ROOTS; the flagless layout unchanged)" diff --git a/scripts/lib/dvxGate.sh b/scripts/lib/dvxGate.sh index b40d10a..dc66e31 100644 --- a/scripts/lib/dvxGate.sh +++ b/scripts/lib/dvxGate.sh @@ -8,6 +8,8 @@ DVX_GATE="$(basename "$0" .sh)" DVX_BIN="$JOEY_REPO/build/dvx/bin" +# UBER's probe build (make dvx-probe), packaged apart from bin/. +DVX_PROBE_DIR="$JOEY_REPO/build/dvx/probe" # The folder a program installs in under apps/, and the name of its log. DVX_VENDOR=joeylib DVX_LOG_NAME=joeylog.txt @@ -19,6 +21,9 @@ DVX_SETTINGS_FULL_SCREEN=fullScreen # DVX.LOG's line when a program's full-screen mode is set (the port asks for # the stage's size at 8 bits a pixel). DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp" +# DVX.LOG's line when a program cannot be loaded (a missing file, a name +# that is not 8.3, an unresolved import). +DVX_LOAD_FAILED_LOG="DXE load failed" # The runtime parts of toolchains/opt/dvx (all but the SDK). DVX_RUNTIME="dvx.exe config system libs widgets apps" # The desktop the gates run: 640x480 in 16-bit colour, DVX's own DOS test @@ -163,8 +168,8 @@ dvxProgramLog() { # dvxAwaitLog DIR PROGRAM REGEX SECONDS PID -- wait up to SECONDS (or until PID -# exits) for PROGRAM's log to hold a line matching REGEX; print its path if -# it does. +# exits, or DVX.LOG says a program failed to load) for PROGRAM's log to hold +# a line matching REGEX; print its path if it does. dvxAwaitLog() { local log local waited @@ -175,6 +180,7 @@ dvxAwaitLog() { return 0 fi joeyAlive "$5" || return 1 + dvxDvxLog "$1" 400 | grep -q "$DVX_LOAD_FAILED_LOG" && return 1 sleep 1 done return 1 @@ -201,3 +207,13 @@ dvxDvxLog() { [ -n "$log" ] && tr -d '\r' < "$log" | tail -"${2:-15}" return 0 } + + +# dvxDvxLogCopy DIR FILE -- the staged DVX's whole DVX.LOG as FILE (Unix line +# ends); FILE is left out when there is none. +dvxDvxLogCopy() { + local log + log="$(find "$1/DVX" -maxdepth 1 -iname dvx.log -print -quit)" + [ -n "$log" ] && tr -d '\r' < "$log" > "$2" + return 0 +} diff --git a/scripts/lib/joyScreen.py b/scripts/lib/joyScreen.py new file mode 100644 index 0000000..d85df0d --- /dev/null +++ b/scripts/lib/joyScreen.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""joyScreen.py - read both joysticks' state off a screenshot of JOY. + +JOY (examples/joy) shows each stick in a frame: a dot that sits in the +middle at rest and moves DOT_TRAVEL stage pixels per full axis +(jlJoystickX/Y * DOT_TRAVEL / 127, grey at rest, green away from it), two +button squares (yellow while down) and a patch under them, green when the +port reports a joystick and red when it does not. This finds the stage in +a screenshot (on a desktop the window is wherever DVX put it, at the +window's scale) by the two patches, and prints each stick's state as five +characters: connected (1/0), X and Y (- 0 +: the dot at least half its +travel left or up, in the middle, at least half its travel right or +down), buttons 0 and 1 (1/0); '?' for anything else (a dot part way from +the middle, a colour that is not JOY's). Half way, as DVX's own joystick +tests judge an axis: an uncalibrated DVX axis ranges itself as it moves, +so an emulated stick's full push reads a little short of the end. + + joyScreen.py PNG [--scale S] + +Prints "A= B= scale= dots= "; exit 1 +when the stage is not found or a stick cannot be read. +""" + +import sys + +from PIL import Image + +# examples/joy/joy.c: its layout (stage pixels) and colours ($0RGB). +STICK_LEFT = (24, 216) +STICK_TOP = 16 +FRAME_SIZE = 80 +FRAME_INSET = 2 +DOT_SIZE = 8 +DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 - FRAME_INSET +BUTTON_W = 24 +BUTTON_H = 16 +BUTTON_GAP = 4 +ROW_GAP = 6 +BUTTON_TOP = STICK_TOP + FRAME_SIZE + ROW_GAP +STATUS_W = 80 +STATUS_H = 4 +STATUS_TOP = BUTTON_TOP + BUTTON_H + ROW_GAP +COLOURS = { + "background": 0x000, "frame": 0x444, "dotOff": 0x666, "dotOn": 0x0F0, + "buttonOff": 0x333, "buttonOn": 0xFF0, "disconnected": 0xF00, +} +# The patch's green is the dot's (COLOR_CONNECTED = COLOR_DOT_ON). +CONNECTED = "dotOn" +# A 15-bit desktop is off by at most 8 a channel, the 6-bit VGA DAC by at +# most 3; JOY's colours sit much further apart. (All are exact in DVX's +# 256-colour desktop palette.) +MAX_DIST = 3 * 9 ** 2 +SCALES = (1, 2, 3) +UNKNOWN = "?" + + +def rgb(v): + return (((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) + + +PALETTE = {name: rgb(v) for name, v in COLOURS.items()} + + +class Screen: + def __init__(self, img): + self.w, self.h = img.size + self.pixels = list(img.getdata()) + self.names = {} + + def name(self, x, y): + """JOY's colour name at screen (x, y), or None.""" + if not (0 <= x < self.w and 0 <= y < self.h): + return None + c = self.pixels[y * self.w + x] + if c not in self.names: + best, bestDist = None, MAX_DIST + 1 + for n, p in PALETTE.items(): + d = sum((a - b) ** 2 for a, b in zip(c, p)) + if d < bestDist: + best, bestDist = n, d + self.names[c] = best + return self.names[c] + + +def findStage(scr, scale): + """The screen position of stage (0, 0), found by the two status patches: + runs of STATUS_W stage pixels in either patch colour on one row, as far + apart as the two frames.""" + run = STATUS_W * scale + gap = (STICK_LEFT[1] - STICK_LEFT[0]) * scale + patch = (CONNECTED, "disconnected") + for y in range(scr.h - STATUS_H * scale + 1): + x = 0 + while x < scr.w: + if scr.name(x, y) not in patch: + x += 1 + continue + start = x + while x < scr.w and scr.name(x, y) in patch: + x += 1 + if x - start != run: + continue + other = start + gap + if all(scr.name(other + i, y) in patch for i in range(run)) and scr.name(other + run, y) not in patch: + ox = start - STICK_LEFT[0] * scale + oy = y - STATUS_TOP * scale + if ox >= 0 and oy >= 0: + return ox, oy + return None + + +def stagePixel(scr, origin, scale, x, y): + """JOY's colour name of stage pixel (x, y): its block's middle.""" + return scr.name(origin[0] + x * scale + scale // 2, origin[1] + y * scale + scale // 2) + + +def axisChar(offset): + if offset == 0: + return "0" + if offset <= -DOT_TRAVEL // 2: + return "-" + if offset >= DOT_TRAVEL // 2: + return "+" + return UNKNOWN + + +def stick(scr, origin, scale, left): + """The five-character state of the stick whose frame starts at left, and + its dot's offset from the middle ("x,y", "?" when there is no dot).""" + patch = stagePixel(scr, origin, scale, left + STATUS_W // 2, STATUS_TOP + STATUS_H // 2) + connected = {CONNECTED: "1", "disconnected": "0"}.get(patch, UNKNOWN) + + dots = [(x, y, stagePixel(scr, origin, scale, x, y)) + for y in range(STICK_TOP, STICK_TOP + FRAME_SIZE) + for x in range(left, left + FRAME_SIZE)] + dots = [(x, y, n) for x, y, n in dots if n in ("dotOn", "dotOff")] + ax = ay = UNKNOWN + offset = UNKNOWN + if dots: + x0 = min(d[0] for d in dots) + y0 = min(d[1] for d in dots) + whole = len(dots) == DOT_SIZE * DOT_SIZE and max(d[0] for d in dots) == x0 + DOT_SIZE - 1 and max(d[1] for d in dots) == y0 + DOT_SIZE - 1 + colours = set(d[2] for d in dots) + dx = x0 - (left + FRAME_SIZE // 2 - DOT_SIZE // 2) + dy = y0 - (STICK_TOP + FRAME_SIZE // 2 - DOT_SIZE // 2) + offset = "%+d,%+d" % (dx, dy) + if whole and len(colours) == 1: + ax = axisChar(dx) + ay = axisChar(dy) + # Green away from the middle, grey in it. + atRest = ax == "0" and ay == "0" + if colours != ({"dotOff"} if atRest else {"dotOn"}): + ax = ay = UNKNOWN + + centre = left + FRAME_SIZE // 2 + buttons = "" + for bx in (centre - BUTTON_W - BUTTON_GAP // 2, centre + BUTTON_GAP // 2): + n = stagePixel(scr, origin, scale, bx + BUTTON_W // 2, BUTTON_TOP + BUTTON_H // 2) + buttons += {"buttonOn": "1", "buttonOff": "0"}.get(n, UNKNOWN) + return connected + ax + ay + buttons, offset + + +def main(argv): + if len(argv) < 2: + sys.stderr.write(__doc__) + return 2 + scales = SCALES + args = argv[2:] + while args: + if args[0] == "--scale" and len(args) > 1: + scales = (int(args[1]),) + else: + sys.stderr.write(__doc__) + return 2 + args = args[2:] + + scr = Screen(Image.open(argv[1]).convert("RGB")) + for scale in scales: + origin = findStage(scr, scale) + if origin: + break + if not origin: + print("joyScreen: FAIL - JOY's two status patches are not on the screen") + return 1 + read = [stick(scr, origin, scale, left) for left in STICK_LEFT] + print("A=%s B=%s scale=%d dots=%s %s" % (read[0][0], read[1][0], scale, read[0][1], read[1][1])) + return 1 if any(UNKNOWN in state for state, offset in read) else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/lib/keysRow.py b/scripts/lib/keysRow.py index e82df42..43d6df3 100755 --- a/scripts/lib/keysRow.py +++ b/scripts/lib/keysRow.py @@ -4,14 +4,18 @@ KEYS (examples/keys) draws every character it received on its row 198 as pixel nibbles: the sentinel A 5 A 5, the count byte, a zero byte, then each character, two pixels a byte, high nibble first. This finds that row in a -screenshot (on a desktop the window is wherever DVX put it) and checks it -holds exactly TARGET. +screenshot (on a desktop the window is wherever DVX put it, at the window's +scale: every stage pixel a SCALE x SCALE block) and checks it holds exactly +TARGET, and that every block of it is whole: each of its screen pixels the +same colour. - keysRow.py PNG TARGET [--size WxH] [--at X,Y] + keysRow.py PNG TARGET [--size WxH] [--at X,Y] [--scale S] --size: the screenshot must be that size (the full-screen mode, not the desktop). --at: the row must start exactly there (the stage's own row 198 -at 1x on a full screen). Prints the row it found; exit 0 when it is right. +at 1x on a full screen). --scale: the row must be at that scale (else the +first of 1, 2, 3 it is found at). Prints the row it found; exit 0 when it +is right. """ import sys @@ -24,11 +28,56 @@ PAL12 = [(i << 8) | (i << 4) | i for i in range(16)] PAL12[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF PAL12[15] = 0xF00 PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12] -# A 16-bit desktop (5-6-5) is off by at most 8 a channel after the round +# A 15- or 16-bit desktop is off by at most 8 a channel after the round # trip, the 6-bit VGA DAC by at most 3, while the palette entries sit 17 -# apart: nearest is exact. +# apart: nearest is exact. (Every KEYS colour is also one of DVX's 256-colour +# desktop palette, its cube or its grey ramp, so 8 bits is exact too.) MAX_DIST = 3 * 9 ** 2 +NO_NIBBLE = -1 SENTINEL = (0xA, 0x5, 0xA, 0x5) +SCALES = (1, 2, 3) + + +def nibbleMap(img): + """Every pixel's KEYS palette index, NO_NIBBLE where no entry is near.""" + nearest = {} + for rgb in set(img.getdata()): + best, bestDist = NO_NIBBLE, MAX_DIST + 1 + for n, pal in enumerate(PAL_RGB): + d = sum((c - p) ** 2 for c, p in zip(rgb, pal)) + if d < bestDist: + best, bestDist = n, d + nearest[rgb] = best + return [nearest[rgb] for rgb in img.getdata()] + + +def findRow(nibs, pixels, w, h, scale, expect): + """The (x, y, bytes) of the first row at scale whose header matches.""" + header = expect[:4] + span = 2 * len(expect) * scale + for y in range(h): + base = y * w + for x in range(w - span + 1): + if nibs[base + x] != SENTINEL[0]: + continue + if tuple(nibs[base + x + i * scale] for i in range(len(SENTINEL))) != SENTINEL: + continue + row = bytes(((nibs[base + x + 2 * i * scale] & 0xF) << 4) | (nibs[base + x + (2 * i + 1) * scale] & 0xF) for i in range(len(expect))) + if row[:4] == header: + return x, y, row + return None + + +def splitBlocks(pixels, w, h, x, y, scale, count): + """How many of the count stage pixels from (x, y) are not whole blocks.""" + split = 0 + for i in range(count): + left = x + i * scale + want = pixels[y * w + left] + block = [pixels[(y + dy) * w + left + dx] for dy in range(scale) if y + dy < h for dx in range(scale)] + if len(block) != scale * scale or any(p != want for p in block): + split += 1 + return split def main(argv): @@ -38,12 +87,15 @@ def main(argv): png, target = argv[1], argv[2] size = None at = None + scales = SCALES args = argv[3:] while args: if args[0] == "--size" and len(args) > 1: size = tuple(int(n) for n in args[1].split("x")) elif args[0] == "--at" and len(args) > 1: at = tuple(int(n) for n in args[1].split(",")) + elif args[0] == "--scale" and len(args) > 1: + scales = (int(args[1]),) else: sys.stderr.write(__doc__) return 2 @@ -51,43 +103,30 @@ def main(argv): img = Image.open(png).convert("RGB") w, h = img.size - px = img.load() if size is not None and img.size != size: print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1])) return 1 - nearest = {} - - def nib(x, y): - rgb = px[x, y] - if rgb not in nearest: - best, bestDist = 0, 1 << 30 - for n, pal in enumerate(PAL_RGB): - d = sum((c - p) ** 2 for c, p in zip(rgb, pal)) - if d < bestDist: - best, bestDist = n, d - nearest[rgb] = best if bestDist <= MAX_DIST else -1 - return nearest[rgb] - + pixels = list(img.getdata()) + nibs = nibbleMap(img) expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii") found = None - for y in range(h): - for x in range(w - 2 * len(expect)): - if tuple(nib(x + i, y) for i in range(len(SENTINEL))) == SENTINEL: - row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect))) - if row[:4] == expect[:4]: - found = (x, y, row) - break + for scale in scales: + found = findRow(nibs, pixels, w, h, scale, expect) if found: break if not found: - print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen" % len(target)) + print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen at %s" % (len(target), " or ".join("%dx" % s for s in scales))) return 1 x, y, row = found - print("keysRow: row at (%d,%d)=%s" % (x, y, row.hex().upper())) + print("keysRow: row at (%d,%d) at %dx=%s" % (x, y, scale, row.hex().upper())) if row != expect: print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper())) return 1 + split = splitBlocks(pixels, w, h, x, y, scale, 2 * len(expect)) + if split: + print("keysRow: FAIL - %d of the row's %d stage pixels are not whole %dx%d blocks" % (split, 2 * len(expect), scale, scale)) + return 1 if at is not None and (x, y) != at: print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1])) return 1 diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index abe82e5..ec6dd97 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -5,14 +5,20 @@ # # Kinds, in the order `all` runs them: # host the shared layer on the host, no emulator: the new-port -# template (blank), input, millis, save, serial, sfxmix, tilebank +# template (blank), file roots, the dirty-band marker, input, +# millis, save, serial, sfxmix, tilebank # golden UBER on each port, its 51 per-op surface hashes against # tests/goldens/uber (x68000 against the IIgs's, its own gate; # dvx against the DOS port's, whose stage it shows) +# atarist: also the dirty-band marker the three 68000 ports share +# (src/m68k/stageBands.s), held to the per-row widen under Hatari # visual iigs: every launchable example on joey.2mg boots and draws # dvx: full screen -- UBER's hashes through a suspend and a # resume, and KEYS's row read off the 320x200 VGA screen, then # in its window, then on the whole screen again +# dvx: also the display -- UBER's probe build on nine desktop +# depths and scales and the full screen, every displayed pixel +# checked against the stage # x68000: the SCB -- every text-palette reload of a 147-change # screen lands in the blanking before its band, timed on the beam # tilebank each port: examples/tilebank loads the port's own .tbk bake @@ -20,9 +26,10 @@ # gate's surface hash, and refuses every other target's bake (the # host half is check-tilebank.sh under `host`) # input each port: a typed string reaches jlInputGetChar byte-exact -# (dvx: and the present shows it, decoded from the desktop) -# x68000: also both joysticks -- every direction, both triggers -# and every release, read back from examples/joy's display +# (dvx: and the present shows it, decoded from the desktop, also +# at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program) +# x68000 and dvx: also both joysticks -- every direction, both +# buttons and every release, read back from examples/joy's display # audio six gates, two kinds: # iigs DOC wavetable + sound IRQ vector (register tap) # x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) @@ -44,6 +51,8 @@ # save iigs: a SAVES/ blob written and read back through GS/OS # loss iigs: 4096 bytes through full-screen presents, lossless on the # modem port at 9600 and a Super Serial Card at 2400 +# x68000: both serial streams byte-exact and the mouse right while +# the SCB changes palette on every line of a run (9600, 19200, 38400) # disk iigs: the one-disk STAXI (ROM 3 and ROM 01) and AGI images boot # and draw # @@ -91,6 +100,8 @@ AMIGA_UBER_SECONDS=400 # kind port name command...: every gate, in run order within its kind. GATES=$(cat < build/atarist/test/DIRTYMRK.PRG) and run under Hatari, +# so the randomized marks and fills go through src/m68k/stageBands.s -- the asm +# all three 68000 ports share -- and are held byte for byte to the plain +# per-row widen. scripts/check-dirtymark.sh is the same harness on the host +# against the C marker the x86 ports run. +# +# A wrong mark does not change the UBER hashes (they hash the surface, not the +# bands); it shows as rows left stale on the screen, or copied for nothing. +# +# Usage: scripts/verify-atarist-dirtymark.sh +# Env: TIMEOUT seconds to wait for the result (default 120) +# Exit: 0 PASS, 1 FAIL, 2 SKIP (Hatari or EmuTOS not installed) +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +timeout_s=${TIMEOUT:-120} +prg=$repo/build/atarist/test/DIRTYMRK.PRG +result=DIRTYMRK.TXT + +joeyNeed HATARI hatari +joeyNeed EMUTOS_IMG emutos +make -s -f "$repo/make/atarist.mk" atarist-dirtymark || { + echo "verify-atarist-dirtymark: FAIL - building DIRTYMRK.PRG failed" >&2 + exit 1 +} + +work=$(mktemp -d -t joeylib-dirtymark.XXXXXX) +cp "$prg" "$work/" +display=:$((90 + RANDOM % 9)) +Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 & +xvfb_pid=$! +hat_pid= +cleanup() { + [[ -n $hat_pid ]] && joeyStop "$hat_pid" + kill "$xvfb_pid" 2>/dev/null || true + wait 2>/dev/null || true + rm -rf "$work" +} +trap cleanup EXIT +sleep 0.4 + +DISPLAY=$display "$HATARI" \ + --tos "$EMUTOS_IMG" \ + --harddrive "$work" \ + --gemdos-drive C \ + --drive-a off --drive-b off \ + --sound off \ + --fast-forward on \ + --frameskips 4 \ + --auto "C:\\DIRTYMRK.PRG" >"$work/hatari.log" 2>&1 & +hat_pid=$! + +waited=0 +while (( waited < timeout_s )); do + if [[ -s $work/$result ]]; then + break + fi + sleep 1 + (( waited++ )) || true +done +if [[ ! -s $work/$result ]]; then + echo "verify-atarist-dirtymark: FAIL - no $result after ${timeout_s}s" >&2 + exit 1 +fi +line=$(head -1 "$work/$result") +echo "$line" +if [[ $line =~ ^dirtyMark:\ ([0-9]+)\ passed,\ 0\ failed ]] && (( BASH_REMATCH[1] > 0 )); then + echo "verify-atarist-dirtymark: PASS (${waited}s)" + exit 0 +fi +echo "verify-atarist-dirtymark: FAIL" >&2 +exit 1 diff --git a/scripts/verify-dvx-display.sh b/scripts/verify-dvx-display.sh new file mode 100755 index 0000000..88ad4e7 --- /dev/null +++ b/scripts/verify-dvx-display.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# verify-dvx-display.sh - Display parity gate for DVX: what the present puts +# where the stage is shown must be the stage, on every desktop depth, at +# every window scale and on the full screen. +# +# The golden hashes gate the stage, never the display. UBER's probe build +# (make dvx builds it: examples/uber with UBER_PROBE) ends with a row for DVX +# (P13) that draws one-pixel stripes in all 16 colours, bands and a small rect, moves +# rows to another palette and changes that palette, presenting after each, +# and asks the port after every present how many pixels differ from the +# stage (dvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content +# buffer in the desktop's own pixel format, every screen pixel of a stage +# pixel's scale x scale block, or the full-screen backbuffer and its +# palette; each pixel derived from the stage's palette entries, not through +# the present's tables. Each run is scripts/bench-dvx.sh --probe on one +# desktop (DVX.INI's video mode), about 8 s. Every count must be 0, every +# pixel must have been compared (64000 x scale^2, plus the 256 palette +# entries on the full screen), and DVX.LOG must show the program on the +# desktop asked for, at the scale asked for. +# +# The default desktops cover each pixel size the present writes (1, 2 and +# 4 bytes) at each window scale (1x, 2x, 3x), the 8-bit desktop's nearest +# colours and both 16-bit packings (5-5-5 and 5-6-5), then the full screen +# (320x200 at 8 bits, its palette the stage's 16). +# +# Usage: scripts/verify-dvx-display.sh [WxHxBPP:SCALE | full]... +# Requires: DOSBox Staging, Xvfb, the dvx component; the probe build +# (make dvx builds it, into build/dvx/probe). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/dvxGate.sh" +joeyNeed DOSBOX dosbox +joeyNeed DVX_ROOT dvx + +program=uberprob +stageWidth=320 +stageHeight=200 +stagePixels=$((stageWidth * stageHeight)) +paletteEntries=256 +probeLine="UBER-PROBE: dvxDisplayMismatch" +fullSpec=full +benchSeconds=120 # one probe run, boot to done line +defaults=(640x480x8:1 800x600x8:2 1024x768x8:3 + 640x480x16:1 800x600x15:2 1024x768x16:3 + 640x480x32:1 800x600x32:2 1024x768x32:3 + "$fullSpec") +specs=("$@") +[ ${#specs[@]} -gt 0 ] || specs=("${defaults[@]}") + +if [ ! -f "$DVX_PROBE_DIR/apps/$DVX_VENDOR/$program/$program.app" ]; then + echo "verify-dvx-display: the probe build is missing; run: make dvx" >&2 + exit 2 +fi + +log=$DVX_PROBE_DIR/$DVX_LOG_NAME +dvxLog=$DVX_PROBE_DIR/dvx.log +fails="" +passed=0 + + +# fail SPEC WHAT -- note a failed run; the gate goes on with the rest. +fail() { + echo "verify-dvx-display: FAIL - $1: $2" + fails="${fails:+$fails; }$1" +} + + +for spec in "${specs[@]}"; do + benchArgs=(--probe) + if [ "$spec" = "$fullSpec" ]; then + w=$DVX_VIDEO_W + h=$DVX_VIDEO_H + bpp=$DVX_VIDEO_BPP + scale=1 + want=$((stagePixels + paletteEntries)) + benchArgs+=(--full-screen) + else + if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then + echo "verify-dvx-display: bad desktop \"$spec\" (WxHxBPP:SCALE or $fullSpec)" >&2 + exit 2 + fi + w=${BASH_REMATCH[1]} + h=${BASH_REMATCH[2]} + bpp=${BASH_REMATCH[3]} + scale=${BASH_REMATCH[4]} + want=$((stagePixels * scale * scale)) + fi + DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp TIMEOUT=$benchSeconds \ + "$repo/scripts/bench-dvx.sh" "${benchArgs[@]}" > /dev/null 2>&1 + row=$(grep -F "$probeLine" "$log" 2> /dev/null | tail -1) + if [ -z "$row" ]; then + fail "$spec" "no \"$probeLine\" line (DVX.LOG follows)" + tail -n 15 "$dvxLog" 2> /dev/null + continue + fi + counts=${row#*"$probeLine" } + echo "verify-dvx-display: $spec: $counts" + if ! grep -qF "JoeyLib: Uber Probe: $((stageWidth * scale))x$((stageHeight * scale)) at ${scale}x on a ${w}x${h} ${bpp}-bit desktop" "$dvxLog"; then + fail "$spec" "DVX.LOG does not show the window at ${scale}x on a ${w}x${h} ${bpp}-bit desktop: $(grep -F 'JoeyLib: Uber Probe:' "$dvxLog")" + continue + fi + if [ "$spec" = "$fullSpec" ] && ! grep -qF "$DVX_FULL_SCREEN_LOG" "$dvxLog"; then + fail "$spec" "DVX.LOG has no \"$DVX_FULL_SCREEN_LOG\"" + continue + fi + bad=$(printf '%s\n' "$counts" | tr ' ' '\n' | grep '^after' | grep -v '=0$' | tr '\n' ' ') + compared=$(printf '%s\n' "$counts" | sed -n 's/.*compared=\([0-9]*\).*/\1/p') + if [ -n "$bad" ]; then + fail "$spec" "pixels differ from the stage: $bad" + elif [ "$compared" != "$want" ]; then + fail "$spec" "compared $compared, want $want" + else + passed=$((passed + 1)) + fi +done + +if [ -n "$fails" ]; then + echo "verify-dvx-display: FAIL - $fails" + exit 1 +fi +echo "verify-dvx-display: PASS ($passed desktops: every pixel where the stage is shown matches it after every present)" diff --git a/scripts/verify-dvx-input.sh b/scripts/verify-dvx-input.sh index efe7e16..3912cbb 100755 --- a/scripts/verify-dvx-input.sh +++ b/scripts/verify-dvx-input.sh @@ -3,18 +3,26 @@ # (examples/keys) as the desktop of a scratch DVX in DOSBox Staging under # Xvfb and types the RetroNet definition-of-done string with xdotool (real X # key events: ':' needs Shift, which DVX's keyboard layout applies before the -# window's onKey hands the character to jlInputGetChar). Two checks: -# 1. the present: a raw screenshot of the desktop must show KEYS's +# window's onKey hands the character to jlInputGetChar). Three checks: +# 1. DVX's reserved chord: Ctrl+Esc (the Task Manager) must not reach KEYS +# as Esc, which would end it (its typed line appears in its log); the +# Task Manager's Switch To gives KEYS the focus back; +# 2. the present: a raw screenshot of the desktop must show KEYS's # verification scanline (row 198: sentinel, count, every received # character as two pixel nibbles) somewhere -- the window's content, -# expanded from the stage through the port's palette tables into DVX's -# 16-bit desktop -- decoding to exactly the typed bytes +# expanded from the stage through the port's palette tables into the +# desktop's pixel format at the window's scale -- decoding to exactly +# the typed bytes, every stage pixel a whole scale x scale block # (scripts/lib/keysRow.py); -# 2. the typed history: after Escape, KEYS logs every received character +# 3. the typed history: after Escape, KEYS logs every received character # as hex to its joeylog.txt (in its settings folder), which must equal # the typed ASCII. +# Each desktop given (default 640x480 at 16 bits, where the window is 1x) is +# a run of its own; DVX.LOG must show KEYS's window on that desktop at the +# scale given. # -# Usage: scripts/verify-dvx-input.sh +# Usage: scripts/verify-dvx-input.sh [WxHxBPP:SCALE]... +# e.g. scripts/verify-dvx-input.sh 800x600x8:2 1024x768x32:3 # Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component; # the keys program (make dvx). set -uo pipefail @@ -27,10 +35,16 @@ joeyNeed DVX_ROOT dvx program=keys target="192.168.1.10:6510" +stageWidth=320 +stageHeight=200 wall=180 # hard cap on DOSBox, whatever happens to this script readySeconds=60 # DVX boot until its shell reports ready settleSeconds=3 # then KEYS opening its window and taking the focus +stepSeconds=2 # a key, then DVX and KEYS acting on it +holdSeconds=0.3 # a key held that long: a quick tap can fall between two polls logSeconds=30 # Escape until KEYS's typed line is in its log +specs=("$@") +[ ${#specs[@]} -gt 0 ] || specs=(640x480x16:1) dvxNeedBuild "$program" @@ -38,6 +52,8 @@ work=$(mktemp -d -t joeylib-dvx-input.XXXXXX) guard="" dosbox="" xvfb="" +win="" +fails="" # 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". cleanup() { joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null @@ -45,72 +61,135 @@ cleanup() { } trap cleanup EXIT -dvxStage "$work/c" "$(dvxAppDir "$program")" -dvxDosboxConf "$work/dosbox.conf" "$work/c" -printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$work/shots" >> "$work/dosbox.conf" -mkdir -p "$work/xdg" "$work/shots" + +# fail SPEC WHAT -- note a failed check; the gate goes on to report the rest. +fail() { + echo "verify-dvx-input: FAIL - $1: $2" + fails="${fails:+$fails; }$1: $2" +} + + +# hold KEY... -- press the keys in order, each held holdSeconds before the +# next (a chord's modifier goes down first), release them in reverse, then +# give DVX and KEYS time to act. +hold() { + local k + local i + for k in "$@"; do + DISPLAY="$display" xdotool keydown --window "$win" "$k" + sleep "$holdSeconds" + done + for ((i = $#; i >= 1; i--)); do + DISPLAY="$display" xdotool keyup --window "$win" "${!i}" + done + sleep "$stepSeconds" +} + + +# run SPEC -- one desktop: boot, type, Ctrl+Esc, screenshot, Escape, judge. +run() { + local spec="$1" + local dir="$work/$1" + local w + local h + local bpp + local scale + local ready="" + local waited + local log + local png + + if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then + echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE)" >&2 + exit 2 + fi + w=${BASH_REMATCH[1]} + h=${BASH_REMATCH[2]} + bpp=${BASH_REMATCH[3]} + scale=${BASH_REMATCH[4]} + DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp dvxStage "$dir/c" "$(dvxAppDir "$program")" + dvxDosboxConf "$dir/dosbox.conf" "$dir/c" + printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf" + mkdir -p "$dir/xdg" "$dir/shots" + + XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$dir/dosbox.conf" > "$dir/dosbox.log" 2>&1 & + guard=$! + dosbox=$(joeyChild "$guard") + + for ((waited = 0; waited < readySeconds; waited++)); do + if dvxDvxLog "$dir/c" 200 | grep -q "DVX Shell ready"; then + ready=1 + break + fi + joeyAlive "$guard" || break + sleep 1 + done + if [ -z "$ready" ]; then + fail "$spec" "DVX did not come up within ${readySeconds}s (DVX.LOG follows)" + dvxDvxLog "$dir/c" >&2 + joeyStop "$dosbox" "$guard" 2> /dev/null + return + fi + sleep "$settleSeconds" + + win=$(joeyFindWindow "$display" "$dosbox") + if [ -z "$win" ]; then + fail "$spec" "DOSBox window not found" + joeyStop "$dosbox" "$guard" 2> /dev/null + return + fi + DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true + DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target" + sleep "$stepSeconds" + # DVX's Task Manager chord, then its Switch To back to KEYS. + hold ctrl Escape + log=$(dvxProgramLog "$dir/c" "$program") + if [ -n "$log" ] && tr -d '\r' < "$log" | grep -q '^typed='; then + fail "$spec" "Ctrl+Esc reached KEYS as Esc (it ended)" + fi + hold alt s + # Ctrl+F5 = DOSBox screenshot hotkey. + DISPLAY="$display" xdotool key --window "$win" ctrl+F5 + sleep "$stepSeconds" + DISPLAY="$display" xdotool key --window "$win" Escape + + log=$(dvxAwaitLog "$dir/c" "$program" '^typed=' "$logSeconds" "$guard") + cp "$log" "$dir/joeylog.txt" 2> /dev/null + joeyStop "$dosbox" "$guard" 2> /dev/null + dosbox="" + guard="" + + if ! dvxDvxLog "$dir/c" 400 | grep -qF "JoeyLib: Keys: $((stageWidth * scale))x$((stageHeight * scale)) at ${scale}x on a ${w}x${h} ${bpp}-bit desktop"; then + fail "$spec" "DVX.LOG does not show KEYS at ${scale}x on a ${w}x${h} ${bpp}-bit desktop: $(dvxDvxLog "$dir/c" 400 | grep -F 'JoeyLib: Keys:')" + fi + png=$(ls "$dir"/shots/*.png 2> /dev/null | head -1) + if [ -z "$png" ]; then + fail "$spec" "no screenshot captured" + else + python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" --scale "$scale" || fail "$spec" "the present" + fi + if [ -z "$log" ]; then + fail "$spec" "KEYS logged no typed line within ${logSeconds}s of Escape" + else + dvxKeysTyped "$dir/joeylog.txt" "$target" || fail "$spec" "the log" + fi +} + display=":$((RANDOM % 400 + 2500))" Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & xvfb=$! sleep 1 -XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ - timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ - --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & -guard=$! -dosbox=$(joeyChild "$guard") - -ready="" -for ((waited = 0; waited < readySeconds; waited++)); do - if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then - ready=1 - break - fi - joeyAlive "$guard" || break - sleep 1 +for spec in "${specs[@]}"; do + echo "verify-dvx-input: $spec" + run "$spec" done -if [ -z "$ready" ]; then - echo "verify-dvx-input: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2 - dvxDvxLog "$work/c" >&2 - exit 1 -fi -sleep "$settleSeconds" -win=$(joeyFindWindow "$display" "$dosbox") -if [ -z "$win" ]; then - echo "verify-dvx-input: FAIL - DOSBox window not found" >&2 - exit 1 -fi -DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true -DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target" -sleep 2 -# Ctrl+F5 = DOSBox screenshot hotkey. -DISPLAY="$display" xdotool key --window "$win" ctrl+F5 -sleep 2 -DISPLAY="$display" xdotool key --window "$win" Escape - -log=$(dvxAwaitLog "$work/c" "$program" '^typed=' "$logSeconds" "$guard") -cp "$log" "$work/joeylog.txt" 2> /dev/null -joeyStop "$dosbox" "$guard" 2> /dev/null -dosbox="" -guard="" - -png=$(ls "$work"/shots/*.png 2> /dev/null | head -1) -if [ -z "$png" ]; then - echo "verify-dvx-input: FAIL - no screenshot captured" >&2 - exit 1 -fi -if [ -z "$log" ]; then - echo "verify-dvx-input: FAIL - KEYS logged no typed line within ${logSeconds}s of Escape" >&2 - exit 1 -fi - -fails="" -python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" || fails="the present" -dvxKeysTyped "$work/joeylog.txt" "$target" || fails="${fails:+$fails, }the log" if [ -n "$fails" ]; then echo "verify-dvx-input: FAIL - $fails" exit 1 fi -echo "verify-dvx-input: PASS (typed \"$target\": all ${#target} chars reached jlInputGetChar, and the present shows them)" +echo "verify-dvx-input: PASS (${specs[*]}: typed \"$target\", all ${#target} chars reached jlInputGetChar, Ctrl+Esc did not end KEYS, and the present shows them)" diff --git a/scripts/verify-dvx-joy.sh b/scripts/verify-dvx-joy.sh new file mode 100755 index 0000000..aedd23f --- /dev/null +++ b/scripts/verify-dvx-joy.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# verify-dvx-joy.sh - Joystick gate for DVX: both sticks, every direction, +# both buttons and every release, driven through DOSBox Staging's emulated +# game port and read back from what examples/joy draws; and no joystick at +# all when the game port is empty, though DVX then offers its keyboard +# joystick. +# +# The port (src/dvx/input.c) reads DVX's joystick library: its first two +# devices that are real joysticks, each axis -32768..32767 >> 8, clamped to +# -127..127. DOSBox Staging's joysticktype 4axis puts four axes and four +# buttons on the game port with no host joystick, which DVX's game port +# driver (two-stick mode, its default) reads as two sticks, A and B, exactly +# as a Y cable would give it. A mapper file binds host keys to DOSBox's +# joystick events (as DVX's own tests do, src/test/dos/joyDrive.py), and +# xdotool holds them; the raw screenshot is bound to a key of its own, so no +# Ctrl reaches the program. With no host joystick nothing else moves the +# stick: it rests in the middle, buttons up. +# +# JOY (examples/joy) draws, for each stick, a dot (in the middle at rest, +# grey; green when moved), two button squares and a patch that says whether +# the port reports a stick; scripts/lib/joyScreen.py reads the WHOLE state of +# BOTH sticks off each screenshot: each direction of each stick alone, two +# diagonals each, each button alone and both together, with a release after +# every press so a latched line shows up, and a step holding different +# lines on both sticks at once so crosstalk shows up too. Axes are judged +# as DVX's own tests judge them, beyond half way: an uncalibrated DVX axis +# ranges itself as it moves, so a full push reads a little short of the +# end. Escape then ends JOY, which must leave DVX cleanly ("reaped"). +# +# Second run, joysticktype disabled: DVX finds no game port stick and falls +# back to its keyboard joystick (the arrows, LCtrl, Space...), which the +# port leaves out, as every port reports no joystick then: JOY must show +# both sticks disconnected at rest while the keyboard joystick's Left and +# Space are held. +# +# Usage: scripts/verify-dvx-joy.sh +# Env: DVX_JOY_SHOTS also copy the screenshots into this folder +# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component; +# the joy program (make dvx). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/dvxGate.sh" +joeyNeed DOSBOX dosbox +joeyNeed DVX_ROOT dvx + +program=joy +wall=240 # hard cap on each DOSBox run, whatever happens to this script +readySeconds=60 # DVX boot until its shell reports ready +settleSeconds=3 # then JOY opening its window and taking the focus +pressSeconds=1 # keys held (or let go), then DVX's poll, JOY and the compositor catching up +shotSeconds=5 # the screenshot key until DOSBox has written the file +exitSeconds=10 # Escape until DVX reaps JOY +rest="10000" # a stick connected, in the middle, buttons up +absent="00000" # no stick +# DVX.LOG when the game port gives two sticks, and when it has none and the +# keyboard joystick stands in. +twoSticksLog="Joystick: 2 device(s) found" +keyboardLog="Joystick: keyboard0" +reapedLog="Shell: reaped app 'Joy'" + +# Host key (xdotool name), its SDL scancode, and the DOSBox mapper event it +# drives. Stick A = the game port's first two axes and buttons, stick B = +# its last two (DOSBox 4axis: the first joystick's axes 2-3, buttons 2-3). +mapKeys=( + "j 13 jaxis_0_0-" "l 15 jaxis_0_0+" "i 12 jaxis_0_1-" "k 14 jaxis_0_1+" + "a 4 jbutton_0_0" "s 22 jbutton_0_1" + "f 9 jaxis_0_2-" "h 11 jaxis_0_2+" "t 23 jaxis_0_3-" "g 10 jaxis_0_3+" + "q 20 jbutton_0_2" "w 26 jbutton_0_3" + "F11 68 hand_rawshot" + "Escape 41 key_esc" +) +shotKey=F11 + +# name, keys held, stick A's state, stick B's state (joyScreen.py's +# characters: connected, X, Y, button 0, button 1). +steps=( + "A left|j|1-000|$rest" "A right|l|1+000|$rest" + "A up|i|10-00|$rest" "A down|k|10+00|$rest" + "A up-left|j i|1--00|$rest" "A down-right|l k|1++00|$rest" + "A button 0|a|10010|$rest" "A button 1|s|10001|$rest" + "A both buttons|a s|10011|$rest" + "B left|f|$rest|1-000" "B right|h|$rest|1+000" + "B up|t|$rest|10-00" "B down|g|$rest|10+00" + "B up-right|h t|$rest|1+-00" "B down-left|f g|$rest|1-+00" + "B button 0|q|$rest|10010" "B button 1|w|$rest|10001" + "B both buttons|q w|$rest|10011" + "A left+button 1, B right+button 0|j s h q|1-001|1+010" +) + +dvxNeedBuild "$program" + +work=$(mktemp -d -t joeylib-dvx-joy.XXXXXX) +guard="" +dosbox="" +xvfb="" +win="" +fails="" +checked=0 +# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". +cleanup() { + joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null + rm -rf "$work" +} +trap cleanup EXIT + + +# boot RUN JOYTYPE [MAPPER] -- DVX staged in $work/RUN with JOY its desktop, +# DOSBox's joysticktype JOYTYPE and, if given, its mapper file; sets guard, +# dosbox and win. +boot() { + local dir="$work/$1" + local waited + dvxStage "$dir/c" "$(dvxAppDir "$program")" + dvxDosboxConf "$dir/dosbox.conf" "$dir/c" + printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf" + printf '\n[joystick]\njoysticktype = %s\n' "$2" >> "$dir/dosbox.conf" + if [ -n "${3:-}" ]; then + printf '\n[sdl]\nmapperfile = %s\n' "$3" >> "$dir/dosbox.conf" + fi + mkdir -p "$dir/xdg" "$dir/shots" + XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$dir/dosbox.conf" > "$dir/dosbox.log" 2>&1 & + guard=$! + dosbox=$(joeyChild "$guard") + for ((waited = 0; waited < readySeconds; waited++)); do + if dvxDvxLog "$dir/c" 200 | grep -q "DVX Shell ready"; then + sleep "$settleSeconds" + win=$(joeyFindWindow "$display" "$dosbox") + [ -n "$win" ] || return 1 + DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true + return 0 + fi + joeyAlive "$guard" || return 1 + sleep 1 + done + return 1 +} + + +# check RUN NAME HOTKEY WANT -- a screenshot (HOTKEY, e.g. F11 or ctrl+F5) +# as $work/RUN/NAME.png, whose two sticks must read WANT ("A=... B=..."). +check() { + local dir="$work/$1" + local before + local png="" + local got + local waited + before=$(ls "$dir/shots" | wc -l) + DISPLAY="$display" xdotool key --window "$win" "$3" + for ((waited = 0; waited < shotSeconds * 10; waited++)); do + if [ "$(ls "$dir/shots" | wc -l)" -gt "$before" ]; then + sleep 0.5 + png=$(ls -t "$dir"/shots/*.png | head -1) + break + fi + sleep 0.1 + done + checked=$((checked + 1)) + if [ -z "$png" ]; then + fail "$2: no screenshot" + return + fi + mv "$png" "$dir/$(printf '%02d' "$checked") ${2}.png" + got=$(python3 "$repo/scripts/lib/joyScreen.py" "$dir/$(printf '%02d' "$checked") ${2}.png" --scale 1) + echo "verify-dvx-joy: $2: $got" + [ "${got% scale=*}" = "$4" ] || fail "$2: read $got, want $4" +} + + +# fail WHAT -- note a failed check; the gate goes on to report the rest. +fail() { + echo "verify-dvx-joy: FAIL - $1" + fails="${fails:+$fails; }$1" +} + + +# keys DOWN|UP KEY... -- press or let go of the keys. +keys() { + local action="key$1" + local k + shift + for k in "$@"; do + DISPLAY="$display" xdotool "$action" --window "$win" "$k" + done +} + + +# stop -- end the DOSBox run. +stop() { + joeyStop "$dosbox" "$guard" 2> /dev/null + dosbox="" + guard="" +} + + +display=":$((RANDOM % 400 + 4300))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +# 1. Both sticks on the game port, moved through the mapper. +mapper="$work/mapper.txt" +for entry in "${mapKeys[@]}"; do + read -r name code event <<< "$entry" + printf '%s "key %s"\n' "$event" "$code" +done > "$mapper" +if ! boot sticks 4axis "$mapper"; then + echo "verify-dvx-joy: FAIL - DVX with JOY did not come up (DVX.LOG follows)" >&2 + dvxDvxLog "$work/sticks/c" >&2 + exit 1 +fi +dvxDvxLog "$work/sticks/c" 400 | grep -q "$twoSticksLog" || fail "DVX.LOG has no \"$twoSticksLog\": $(dvxDvxLog "$work/sticks/c" 400 | grep 'Joystick:' | tr '\n' ' ')" +check sticks "rest" "$shotKey" "A=$rest B=$rest" +for step in "${steps[@]}"; do + IFS='|' read -r name held wantA wantB <<< "$step" + read -r -a heldKeys <<< "$held" + keys down "${heldKeys[@]}" + sleep "$pressSeconds" + check sticks "$name" "$shotKey" "A=$wantA B=$wantB" + keys up "${heldKeys[@]}" + sleep "$pressSeconds" + check sticks "$name released" "$shotKey" "A=$rest B=$rest" +done +keys down Escape +sleep 0.3 +keys up Escape +for ((waited = 0; waited < exitSeconds; waited++)); do + dvxDvxLog "$work/sticks/c" 400 | grep -qF "$reapedLog" && break + sleep 1 +done +dvxDvxLog "$work/sticks/c" 400 | grep -qF "$reapedLog" || fail "Escape did not end JOY (no \"$reapedLog\" in DVX.LOG)" +stop + +# 2. No stick on the game port: DVX's keyboard joystick stands in, and the +# port must leave it out. Plain DOSBox keys (no mapper), the default +# screenshot hotkey. +if ! boot none disabled; then + echo "verify-dvx-joy: FAIL - DVX with JOY and no joystick did not come up (DVX.LOG follows)" >&2 + dvxDvxLog "$work/none/c" >&2 + exit 1 +fi +dvxDvxLog "$work/none/c" 400 | grep -q "$keyboardLog" || fail "DVX.LOG has no \"$keyboardLog\" (the keyboard joystick): $(dvxDvxLog "$work/none/c" 400 | grep 'Joystick:' | tr '\n' ' ')" +check none "no joystick" ctrl+F5 "A=$absent B=$absent" +keys down Left space +sleep "$pressSeconds" +check none "no joystick, keyboard joystick held" ctrl+F5 "A=$absent B=$absent" +keys up Left space +stop + +if [ -n "${DVX_JOY_SHOTS:-}" ]; then + mkdir -p "$DVX_JOY_SHOTS" + cp "$work"/sticks/*.png "$work"/none/*.png "$DVX_JOY_SHOTS/" +fi +if [ -n "$fails" ]; then + echo "verify-dvx-joy: FAIL - $fails" + exit 1 +fi +echo "verify-dvx-joy: PASS ($checked screenshots: both sticks, every direction, both buttons and every release; no joystick without one, the keyboard joystick left out)" diff --git a/scripts/verify-x68000-joy.sh b/scripts/verify-x68000-joy.sh index 8689fdd..002feac 100755 --- a/scripts/verify-x68000-joy.sh +++ b/scripts/verify-x68000-joy.sh @@ -77,12 +77,14 @@ local TVRAM, PLANE_STEP, TVRAM_STRIDE, PLANES = 0xE00000, 0x20000, 128, 4 local STICK_LEFT = { 24, 216 } local STICK_TOP = 16 local FRAME_SIZE = 80 +local FRAME_INSET = 2 local DOT_SIZE = 8 -local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 +local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 - FRAME_INSET local BUTTON_W, BUTTON_H, BUTTON_GAP = 24, 16, 4 -local BUTTON_TOP = STICK_TOP + FRAME_SIZE + 6 +local ROW_GAP = 6 +local BUTTON_TOP = STICK_TOP + FRAME_SIZE + ROW_GAP local STATUS_W, STATUS_H = 80, 4 -local STATUS_TOP = BUTTON_TOP + BUTTON_H + 6 +local STATUS_TOP = BUTTON_TOP + BUTTON_H + ROW_GAP local COLOR_BACKGROUND, COLOR_DOT_OFF, COLOR_DOT_ON = 0, 2, 3 local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5 local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7 diff --git a/scripts/verify-x68000-serial-mouse.sh b/scripts/verify-x68000-serial-mouse.sh index b921eab..9f26194 100755 --- a/scripts/verify-x68000-serial-mouse.sh +++ b/scripts/verify-x68000-serial-mouse.sh @@ -64,6 +64,18 @@ # PASS: every Lua check right, the peer's stream PASS, at least MIN_BYTES each # way, and the guest's quiet counts among the peer's quiet snapshots. # +# Variants, built here into the work directory from examples/sermouse: +# X68K_SERMOUSE_BAUD=19200|38400 the line rate (default 9600, MAME's +# null_modem default); RSDRV.SYS, which the +# boot disk loads, is what reaches past 9600 +# X68K_SERMOUSE_DENSE=n stage lines 0 .. n-1 change palette on +# every line (default 0: one palette), so +# the SCB raster interrupt holds IPL 6 for +# the whole run every frame while the +# streams and the mouse run +# scripts/verify-x68000-serial-scb.sh runs the dense combinations this port +# guarantees; see "Serial under a dense SCB" in claudeDocs/x68000_port.md. +# # Requires: ./install.sh install x68000 -- the patched MAME # (installer/patches/mame: 0002 for the FIFO depth, 0004 for the # RS-232C slot and the mouse) and the Human68k system disk. @@ -81,8 +93,11 @@ PORT="${X68K_SERMOUSE_PORT:-6801}" WALL="${X68K_SERMOUSE_WALL:-600}" PASSES="${X68K_SERMOUSE_PASSES:-3}" MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}" +BAUD="${X68K_SERMOUSE_BAUD:-9600}" +DENSE="${X68K_SERMOUSE_DENSE:-0}" PROG="$repo/build/x68000/bin/SERMOUSE.X" FIFO_DEPTH=3 +DEFAULT_BAUD=9600 # The peer outlives MAME by this much, so it always sees the link close, and # is given this long to report once MAME is gone. PEER_MARGIN=30 @@ -101,12 +116,27 @@ cleanup() { } trap cleanup EXIT +# A variant is examples/sermouse built against the same library with its +# line rate and SCB set (see the header). +if [ "$BAUD" != "$DEFAULT_BAUD" ] || [ "$DENSE" != 0 ]; then + joeyNeed X68K_ROOT toolchain:x68000 + joeyNeed LIBXMP_DIR libxmp + "$X68K_ROOT/bin/m68k-xelf-gcc" -O2 -m68000 -fomit-frame-pointer \ + -DJOEYLIB_PLATFORM_X68000 -DLINE_BAUD="${BAUD}u" -DDENSE_SCB_LINES="${DENSE}u" \ + -I"$repo/include" -I"$LIBXMP_DIR/include" \ + "$repo/examples/sermouse/sermouse.c" \ + "$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \ + -o "$work/SERMOUSE.X" || exit 1 + PROG="$work/SERMOUSE.X" +fi x68kBootDisk "$work/boot.xdf" 'A:\SERMOUSE.X' || exit 1 x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1 { printf 'local PASSES = %d\n' "$PASSES" printf 'local MIN_BYTES = %d\n' "$MIN_BYTES" + printf 'local BAUD = %d\n' "$BAUD" + printf 'local DENSE = %d\n' "$DENSE" cat <<'LUA' local cpu = manager.machine.devices[":maincpu"] local mem = cpu.spaces["program"] @@ -293,8 +323,8 @@ end) local function finish(verdict) done = true local s = prev or {} - io.write(string.format("VERIFY-X68K-SERMOUSE %s checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n", - verdict, checks, #fails, s.rx or 0, s.tx or 0, + io.write(string.format("VERIFY-X68K-SERMOUSE %s baud=%d dense=%d checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n", + verdict, BAUD, DENSE, checks, #fails, s.rx or 0, s.tx or 0, quiet and string.format("%d:%d", quiet.rx, quiet.tx) or "none", iackFixed, #fails > 0 and (" detail=" .. table.concat(fails, ",")) or "")) io.flush() @@ -490,6 +520,11 @@ done # MAME itself, which cleanup stops first (stopping only the timeout process # could leave MAME running). cd "$work" || exit 1 +if [ "$BAUD" != "$DEFAULT_BAUD" ]; then + mkdir -p "$work/cfg" + python3 "$repo/scripts/lib/mameNullModemCfg.py" x68000 :rs232c:null_modem "$BAUD" \ + > "$work/cfg/x68000.cfg" || exit 2 +fi JOEY_Z80SCC_RX_FIFO=$FIFO_DEPTH timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \ -rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \ @@ -538,4 +573,4 @@ fi if [ "$fail" -ne 0 ]; then exit 1 fi -echo "verify-x68000-serial-mouse: PASS (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)" +echo "verify-x68000-serial-mouse: PASS at $BAUD baud, $DENSE dense-SCB lines (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)" diff --git a/scripts/verify-x68000-serial-scb.sh b/scripts/verify-x68000-serial-scb.sh new file mode 100755 index 0000000..f2f6eda --- /dev/null +++ b/scripts/verify-x68000-serial-scb.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# verify-x68000-serial-scb.sh - X68000 RS-232C receive (and the mouse) under a +# per-line palette change: the SCB raster interrupt and the SCC together. +# +# The X68000 port honours the SCB by reloading the text plane's one palette as +# the beam scans (src/x68000/x68kRasterIsr.s). A palette change on every line +# keeps that interrupt at IPL 6 for the whole run, about one raster (31.8 us) +# a change plus two rasters of lead, and the SCC is IPL 5, so every byte that +# arrives in the run waits in the Z8530's receive FIFO. No handler can let the +# SCC in between the lines of such a run: a line has at most ~100 cycles spare +# after the reload and the SCC receive handler takes ~340, so receive is +# loss-free only while a run is shorter than the FIFO lasts at the line rate +# (see "Serial under a dense SCB" in claudeDocs/x68000_port.md for the limits +# and the measurements). +# +# This runs verify-x68000-serial-mouse.sh -- both streams byte-exact, the mouse +# corners and buttons, the pause and resume -- with examples/sermouse built +# for a dense SCB (stage lines 0 .. n-1 alternating between two palettes) at +# 9600, 19200 and 38400 baud, each with a run of back-to-back changes well +# inside what the gate's emulated FIFO survives (JOEY_Z80SCC_RX_FIFO=3, which +# holds two bytes in MAME 0.264's z80scc -- one fewer than the chip; the +# measured edge is 64, 16 and 8 lines passing, 96, 32 and 16 failing): +# 9600 baud 48 lines ~1.6 ms held a frame +# 19200 baud 16 lines ~0.6 ms +# 38400 baud 8 lines ~0.3 ms +# +# Usage: scripts/verify-x68000-serial-scb.sh +# Env: X68K_SERIAL_SCB_PORT first TCP port for the null_modem (default 6802; +# each rate takes the next) +# Requires: what verify-x68000-serial-mouse.sh requires, and the X68000 +# toolchain (it builds its variants of examples/sermouse). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +port="${X68K_SERIAL_SCB_PORT:-6802}" + +# rate, back-to-back palette changes +runs="9600 48 +19200 16 +38400 8" + +fail=0 +skip=0 +while read -r baud dense; do + echo "=== verify-x68000-serial-scb: $baud baud, $dense lines changing palette every line ===" + X68K_SERMOUSE_PORT="$port" X68K_SERMOUSE_BAUD="$baud" X68K_SERMOUSE_DENSE="$dense" \ + bash "$repo/scripts/verify-x68000-serial-mouse.sh" + rc=$? + if [ "$rc" -eq 2 ]; then + skip=1 + elif [ "$rc" -ne 0 ]; then + fail=1 + fi + port=$((port + 1)) +done <<< "$runs" + +if [ "$fail" -ne 0 ]; then + echo "verify-x68000-serial-scb: FAIL" >&2 + exit 1 +fi +if [ "$skip" -ne 0 ]; then + echo "verify-x68000-serial-scb: SKIP - a prerequisite is missing (see above)" >&2 + exit 2 +fi +echo "verify-x68000-serial-scb: PASS (receive byte-exact and the mouse right under per-line palette changes at 9600, 19200 and 38400)" diff --git a/src/core/init.c b/src/core/init.c index aedd0c8..090f090 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -86,7 +86,7 @@ void *jlAlloc(uint32_t bytes) { void jlMemCopy(void *dst, const void *src, uint32_t bytes) { - jlpMemCopy(dst, src, bytes); + memcpy(dst, src, (size_t)bytes); } diff --git a/src/core/palette.c b/src/core/palette.c index 651cc2f..24970b0 100644 --- a/src/core/palette.c +++ b/src/core/palette.c @@ -98,11 +98,11 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16) // avoids a multiply helper. The caller contract (joey/palette.h) // guarantees $0RGB input, so copy the row wholesale and force // color 0 to black -- the only documented silent rewrite. The copy - // is an unrolled sequence of word assignments, NOT memcpy: the - // clang IIgs libc memcpy is a ~30 cyc/byte byte loop (finding #79) - // and measurably REGRESSED this op when tried (1443 -> 603 - // ops/sec); 15 direct word stores beat both that and the old - // masked loop on every port. + // is an unrolled sequence of word assignments, NOT memcpy: 15 direct + // word stores beat the old masked loop on every port, and on the + // IIgs beat memcpy too (when libc's was a byte loop it cut this op + // from 1443 to 603 ops/sec; its short-copy loop still costs ~10 + // cycles a byte at this size). row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5)); row[0] = 0x0000; row[1] = colors16[1]; diff --git a/src/core/port.h b/src/core/port.h index d4197c1..96e43ab 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -288,9 +288,6 @@ extern jlSurfaceT *gStage; extern uint16_t iigsGetTickWord(void); #define jlpFrameCount() iigsGetTickWord() -// jlMemCopy: MVN block moves, split at bank boundaries in hal.c. -void iigsMemCopy(void *dst, const void *src, uint32_t bytes); -#define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n)) // The one Memory Manager owner ID every IIgs allocation uses (hal.c). uint16_t iigsMemUserId(void); // Interrupt-latency bound of the long interrupts-off paths (hal.c): a @@ -777,9 +774,6 @@ uint16_t jlpGenericFrameHz(void); #endif uint16_t jlpGenericFrameCount(void); -#if !defined(jlpMemCopy) - #define jlpMemCopy(_d, _s, _n) memcpy((_d), (_s), (size_t)(_n)) -#endif #if !defined(jlpFrameCount) #if defined(JL_HAS_FRAME_COUNT) uint16_t jlpFrameCount(void); diff --git a/src/core/scb.c b/src/core/scb.c index b1452d5..ec93285 100644 --- a/src/core/scb.c +++ b/src/core/scb.c @@ -55,24 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t return; } -#if defined(JOEYLIB_PLATFORM_IIGS) - // llvm-mos lowers memset to `jsl memset` -- libc's `while (n--) - // *d++ = c` byte loop behind a far call (the finding-#79 class; the - // old comment claiming an MVP/PEI lowering was wrong). That one call - // was essentially the whole cost of this op on the IIgs, and a C - // word-fill fares no better (llvm-mos re-derives the far pointer - // every store). One JSL into the asm [dp],y word fill instead. - { - uint16_t fill = (uint16_t)((uint16_t)paletteIndex | ((uint16_t)paletteIndex << 8)); - - iigsByteFill(&s->scb[firstLine], fill, (uint16_t)(last - firstLine + 1u)); - } -#else // memset is far cheaper than the per-iter loop (scb is uint8_t, - // sizeof(uint8_t)==1, so the call form below is exact); Amiga/ST/DOS - // libc memset is already a vectorized fill. + // sizeof(uint8_t)==1, so the call form below is exact); every port's + // libc memset is a tight fill (the IIgs's 16-bit stores since the + // 2026-10-04 runtime, 5.9 cycles a byte). memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1)); -#endif if (s == gStage) { gStageScbDirty = true; } diff --git a/src/core/sprite.c b/src/core/sprite.c index 163c02c..8a3bfd8 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -473,8 +473,9 @@ static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1 return; } // Clipped interpreter marks and unusual sprite heights: the shared - // multi-row widen (per-port loop shape in surface.c) -- one source - // of truth, no second rolled loop here. h == 1 also lands here (a + // multi-row widen (the 68000 ports' src/m68k/stageBands.s, the C + // twin in surface.c elsewhere) -- one source of truth, no second + // rolled loop here. h == 1 also lands here (a // sprite clipped to one row); the extra call vs the old macro's // inline single-row arm is confined to that edge case. surfaceMarkDirtyRows((uint16_t)y, (uint16_t)(y + h), minWord, maxWord); diff --git a/src/core/surface.c b/src/core/surface.c index 8ae9f1e..b2411ba 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -13,10 +13,25 @@ #define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t)) #define SURFACE_FILE_BYTES (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES) +// A band byte repeated in all four bytes of a long, for the 68000 fill. +#define STAGE_BAND_FILL(_b) ((uint32_t)(_b) * 0x01010101ul) + +#if defined(JOEYLIB_CPU_68000) +// src/m68k/stageBands.s carries these as constants of its own. +typedef char stageBandsRows[(SURFACE_HEIGHT == 200) ? 1 : -1]; +typedef char stageBandsGroupRows[(STAGE_DIRTY_GROUP4_ROWS == 4u) ? 1 : -1]; +typedef char stageBandsGroupMinRows[(STAGE_MARK_GROUP_MIN_ROWS == 9u) ? 1 : -1]; +#endif + // ----- Prototypes ----- // (public API declared in joey/surface.h) // (internal prototypes declared in surfaceInternal.h) +#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_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); +static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord); +#endif // ----- Module state ----- @@ -60,9 +75,85 @@ uint16_t gStagePaletteDirtyMask = 0xFFFFu; // ----- Internal helpers (alphabetical) ----- -// (surfaceMarkDirtyRows inlines the per-row widen with walking pointers -// -- see below. The single-row widen is inlined by the surfaceMarkDirtyRect -// macro, and the IIgs multi-row path is the asm marker.) +#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000) +// Widen the STAGE_DIRTY_GROUP4_ROWS rows at minPtr / maxPtr. (Spelling the +// four rows out benched slower on DOS than this loop.) +static inline void markGroup(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord) { + uint16_t row; + + for (row = 0u; row < STAGE_DIRTY_GROUP4_ROWS; row++) { + markRow(&minPtr[row], &maxPtr[row], minWord, maxWord); + } +} + + +// surfaceMarkDirtyRows's group path, for at least STAGE_MARK_GROUP_MIN_ROWS +// rows: the C twin of src/m68k/stageBands.s -- the same even-row groups and +// the same reuse of a group's result, which tests/host/dirtyMarkHost.c holds +// to the plain per-row widen. A function of its own so the short-mark +// path in surfaceMarkDirtyRows stays a tight loop; gcc inlines it there +// (forcing it out of line cost DOS's 16-row fills 2%). +static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { + uint8_t *minPtr; + uint8_t *maxPtr; + StageBandGroupT *minGroup; + StageBandGroupT *maxGroup; + uint32_t keyMin; + uint32_t keyMax; + uint32_t bandMin; + uint32_t bandMax; + uint16_t rows; + uint16_t groups; + + minPtr = &gStageMinWord[y]; + maxPtr = &gStageMaxWord[y]; + rows = (uint16_t)(yEnd - y); + if ((y & 1u) != 0u) { + markRow(minPtr++, maxPtr++, minWord, maxWord); + rows--; + } + groups = (uint16_t)(rows >> STAGE_DIRTY_GROUP4_SHIFT); + rows = (uint16_t)(rows & (STAGE_DIRTY_GROUP4_ROWS - 1u)); + minGroup = (StageBandGroupT *)(void *)minPtr; + maxGroup = (StageBandGroupT *)(void *)maxPtr; + keyMin = *minGroup; + keyMax = *maxGroup; + markGroup((uint8_t *)(void *)minGroup, (uint8_t *)(void *)maxGroup, minWord, maxWord); + bandMin = *minGroup++; + bandMax = *maxGroup++; + while (--groups != 0u) { + if (*minGroup == keyMin && *maxGroup == keyMax) { + *minGroup = bandMin; + *maxGroup = bandMax; + } else { + keyMin = *minGroup; + keyMax = *maxGroup; + markGroup((uint8_t *)(void *)minGroup, (uint8_t *)(void *)maxGroup, minWord, maxWord); + bandMin = *minGroup; + bandMax = *maxGroup; + } + minGroup++; + maxGroup++; + } + minPtr = (uint8_t *)(void *)minGroup; + maxPtr = (uint8_t *)(void *)maxGroup; + while (rows != 0u) { + markRow(minPtr++, maxPtr++, minWord, maxWord); + rows--; + } +} + + +// Widen one row's band to cover [minWord, maxWord]. +static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord) { + if (minWord < *minPtr) { + *minPtr = minWord; + } + if (maxWord > *maxPtr) { + *maxPtr = maxWord; + } +} +#endif // ----- Public API (alphabetical) ----- @@ -282,104 +373,45 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) { if (s != gStage) { return; } - // Whole-stage mark is a constant fill of both band arrays. On the - // IIgs, memset lowers to `jsl memset` -- a far-call byte loop (a C - // word-fill fared no better: llvm-mos re-derives the far pointer - // every store), so the fill is one JSL into the asm word-store - // loop; the other ports' libc memsets are already tight fills. -#if defined(JOEYLIB_PLATFORM_IIGS) - iigsStageBandsMarkAll(); + // Whole-stage mark is a constant fill of both band arrays. The 68000 + // ports share stageDirtyClearAll's one-pass fill; the x86 and IIgs + // memsets are already tight (the IIgs's matched the asm word-store + // loop it replaced, 2026-10-04). +#if defined(JOEYLIB_CPU_68000) + stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(0u), STAGE_BAND_FILL(STAGE_DIRTY_FULL_MAX)); #else memset(gStageMinWord, 0, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT); #endif #if defined(JOEYLIB_PLATFORM_IIGS) - { - uint8_t grp; - - for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) { - gStageDirtyGroup[grp] = 1u; - } - } + memset(gStageDirtyGroup, 1, STAGE_DIRTY_GROUPS); #endif } // Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h): -// widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never -// compile this -- the macro routes them straight to the asm marker -// iigsMarkDirtyRowsInner instead. -#ifndef JOEYLIB_PLATFORM_IIGS +// widen rows y..yEnd-1 to cover [minWord, maxWord]. The IIgs and the 68000 +// ports never compile this -- the macro routes them to their asm markers -- +// so it is the x86 ports' and the host's. Tall marks go four rows at a time +// (markGroups); shorter ones walk the rows here, with the light entry the +// old per-row loop had. (Whole-tile marks never come here: tile.c's +// tileMarkDirty unrolls its constant 8 rows inline.) +#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000) void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { uint8_t *minPtr; uint8_t *maxPtr; + uint8_t *minEnd; - // Straight-line inlined widen (no per-row helper call) with walking - // pointers, so a multi-row stage op -- sprite draw, fillRect -- - // pays one tight loop instead of yEnd-y function calls. This is - // the ST/Amiga counterpart to the IIgs asm marker. (Whole-tile - // marks bypass this entirely since NATIVE-PERF Phase 2 R1 -- - // tile.c's tileMarkDirty unrolls its constant 8 rows inline.) - // - // Loop shape is PER-PORT, decided by measurement (Phase 2 Step C, - // 2026-07-07): the x2 unroll lifted Amiga sprite gate rows +4-6% - // but cost the ST -5% in BOTH the pointer-walking and counted - // forms (gcc-mint materializes four live pointers either way), - // and the simple loop re-measured exactly at the ST baseline. - // DOS was flat either way, so only the Amiga takes the unroll. - stageMarkDirtyGroups(y, yEnd); + if ((uint16_t)(yEnd - y) >= STAGE_MARK_GROUP_MIN_ROWS) { + markGroups(y, yEnd, minWord, maxWord); + return; + } minPtr = &gStageMinWord[y]; maxPtr = &gStageMaxWord[y]; -#if defined(JOEYLIB_PLATFORM_AMIGA) - { - uint16_t rows; - uint16_t pairs; - - rows = (uint16_t)(yEnd - y); - pairs = (uint16_t)(rows >> 1); - while (pairs != 0u) { - if (minWord < minPtr[0]) { - minPtr[0] = minWord; - } - if (maxWord > maxPtr[0]) { - maxPtr[0] = maxWord; - } - if (minWord < minPtr[1]) { - minPtr[1] = minWord; - } - if (maxWord > maxPtr[1]) { - maxPtr[1] = maxWord; - } - minPtr += 2; - maxPtr += 2; - pairs--; - } - if ((rows & 1u) != 0u) { - if (minWord < *minPtr) { - *minPtr = minWord; - } - if (maxWord > *maxPtr) { - *maxPtr = maxWord; - } - } + minEnd = &gStageMinWord[yEnd]; + while (minPtr < minEnd) { + markRow(minPtr++, maxPtr++, minWord, maxWord); } -#else - { - uint8_t *minEnd; - - minEnd = &gStageMinWord[yEnd]; - while (minPtr < minEnd) { - if (minWord < *minPtr) { - *minPtr = minWord; - } - if (maxWord > *maxPtr) { - *maxPtr = maxWord; - } - minPtr++; - maxPtr++; - } - } -#endif } #endif @@ -439,12 +471,16 @@ bool stageAlloc(void) { void stageDirtyClearAll(void) { // Constant fill of both band arrays, same form as its sibling - // surfaceMarkDirtyAll. This runs after EVERY jlStagePresent, so on - // the IIgs the two memset far-call byte loops here were ~10 ms of - // every present's fixed base; the asm word-store loop fixes that. - // The other ports keep memset (their libc fills are already tight). + // surfaceMarkDirtyAll. This runs after EVERY jlStagePresent. The IIgs + // clears only the 8-row groups its group map says were marked + // (joeyDraw.s), which no fill can match on an idle present. The + // 68000 ports fill both arrays in one pass of long stores (about half + // what two libc memsets cost there); the x86 ports keep memset, whose + // rep stos is already the fastest fill they have. #if defined(JOEYLIB_PLATFORM_IIGS) iigsStageBandsClearAll(); +#elif defined(JOEYLIB_CPU_68000) + stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MIN), STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MAX)); #else memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT); diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index 6e98ad2..2b92b42 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -78,6 +78,20 @@ typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : extern uint8_t gStageMinWord[SURFACE_HEIGHT]; extern uint8_t gStageMaxWord[SURFACE_HEIGHT]; +// Four rows' clean test in one load, for presents on CPUs with a 32-bit +// load (DOS, DVX): a clean row's min word is the CLEAN sentinel (0xFF) and a +// touched row's is at most STAGE_DIRTY_FULL_MAX, so rows 4g .. 4g+3 are all +// clean exactly when their four min-word bytes read 0xFFFFFFFF. It reads the +// per-row bands themselves, so marking has nothing extra to keep in step -- +// unlike the IIgs's gStageDirtyGroup below, which exists only because the +// 65816 has no 32-bit load. gStageMinWord is aligned(2), which x86 (and a +// 68000 long load) accepts. +#define STAGE_DIRTY_GROUP4_SHIFT 2 +#define STAGE_DIRTY_GROUP4_ROWS (1u << STAGE_DIRTY_GROUP4_SHIFT) +#define STAGE_DIRTY_GROUP4_ALL_CLEAN 0xFFFFFFFFul +typedef uint32_t __attribute__((may_alias)) StageBandGroupT; +#define STAGE_DIRTY_GROUP4_CLEAN(_g) (((const StageBandGroupT *)(const void *)gStageMinWord)[_g] == STAGE_DIRTY_GROUP4_ALL_CLEAN) + // Present-path telemetry (NATIVE-PERF Phase 6 W0, IIgs added W2). // The presents accumulate how many bytes they moved toward the // display and how many band rows they scanned; the UBER_PROBE build @@ -159,19 +173,29 @@ extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; // Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1 // (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes -// straight to the asm marker so a multi-row mark stays a single JSL; -// elsewhere it is the C loop in surface.c. +// straight to the asm marker so a multi-row mark stays a single JSL; the +// 68000 ports share an asm marker too (src/m68k/stageBands.s), handed the +// rows' addresses because that file cannot spell the arrays' C symbols for +// all three toolchains; elsewhere it is the C loop in surface.c. The 68000 +// and C markers take the rows four at a time from an even row, and reuse a +// group's result for the next group that held the same bytes (groups of +// STAGE_DIRTY_GROUP4_ROWS, above, as one long each). +// Two whole groups after at most one row to reach an even row: the least that +// lets a group reuse the one before it. Below this the rows go one by one. +#define STAGE_MARK_GROUP_MIN_ROWS (2u * STAGE_DIRTY_GROUP4_ROWS + 1u) #ifdef JOEYLIB_PLATFORM_IIGS void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); #define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) iigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max)) -// Constant-fill both band arrays without libc memset (llvm-mos lowers -// memset to a far-call byte loop -- ~10 ms per present; joeyDraw.s). -// ClearAll = CLEAN sentinels (post-present); MarkAll = every row fully -// dirty. iigsByteFill is the same fix for arbitrary far-pointer fills -// (SCB range): value replicated in both bytes of `fill`, count >= 1. +// Reset the bands of every 8-row group the group map says was marked to +// the CLEAN sentinels, and clear the map (joeyDraw.s): what +// stageDirtyClearAll does after each present, skipping clean groups. void iigsStageBandsClearAll(void); -void iigsStageBandsMarkAll(void); -void iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count); +#elif defined(JOEYLIB_CPU_68000) +// src/m68k/stageBands.s. Fill stores minFill / maxFill (a byte repeated four +// times) over every row of the two arrays. +void stageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, uint32_t minFill, uint32_t maxFill); +void stageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord); +#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) stageBandsMark68k(&gStageMinWord[_y], &gStageMaxWord[_y], (uint16_t)((_yEnd) - (_y)), (uint8_t)(_min), (uint8_t)(_max)) #else void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); #endif diff --git a/src/dos/audio.c b/src/dos/audio.c index a28ca93..f88e861 100644 --- a/src/dos/audio.c +++ b/src/dos/audio.c @@ -39,6 +39,7 @@ #include "port.h" #include "audioSfxMixInternal.h" +#include "dosTimer.h" #include "joey/audio.h" // ----- SB I/O port offsets from base ----- @@ -79,8 +80,6 @@ // ----- PIC ----- -#define PIC1_CMD 0x20 -#define PIC1_IMR 0x21 #define PIC2_CMD 0xA0 #define PIC2_IMR 0xA1 #define PIC_EOI 0x20 @@ -881,35 +880,28 @@ static void adlibNoiseInit(void) { // constraints have to hold afterward: // // 1. BIOS tick counter at $0040:$006C must keep advancing at the -// canonical 18.2 Hz so anything that reads it (DOS time() / -// uclock() / RTC-less time-of-day tracking) stays right. We +// canonical 18.2 Hz so anything that reads it (DOS time(), +// RTC-less time-of-day tracking) stays right. We // use an accumulator: each IRQ adds the PIT divisor to it, // and every time it crosses 65536 we bump the BIOS counter // and subtract -- the math comes out to exactly 18.2065 Hz // regardless of our own IRQ rate. // -// 2. jlpMillisElapsed must report wall-clock ms even though -// uclock()'s sub-tick PIT read is now wrong (PIT cycles -// 0..gPitDivisor, not 0..65535 as uclock assumes). We expose -// an own-ISR-counter-driven ms via the gAudioIsr* globals -// below; src/dos/hal.c::jlpMillisElapsed prefers it -// whenever gAudioIsrActive is true. +// 2. jlpMillisElapsed must report wall-clock ms even though the +// PIT's sub-tick count now spans gPitDivisor, not a BIOS tick. +// We expose an own-ISR-counter-driven ms via the gAudioIsr* +// globals below; src/dos/hal.c::jlpMillisElapsed prefers it +// whenever gAudioIsrActive is true, continuing its timeline. // // Literal "chain to old INT $08" doesn't work cleanly under DPMI // because the old PM handler ends in IRET, which corrupts the // stack frame of any CALL into it. INT $1C (user timer hook) and // floppy motor timeout are dropped; nothing AGI cares about uses // either. -#define PIT_CTRL_PORT 0x43u -#define PIT_TIMER0_PORT 0x40u -#define PIT_CTRL_T0_MODE3 0x36u // counter 0, R/W LSB then MSB, mode 3 (square wave), binary -#define PIT_CLOCK_HZ 1193182uL -#define BIOS_TICK_PERIOD 65536uL -#define BIOS_TICK_ADDR 0x46Cu #define IRQ0_INT_VECTOR 0x08 // Exposed (extern in hal.c) so jlpMillisElapsed can read wall-clock -// ms straight from the ISR counter and bypass the broken uclock. +// ms straight from the ISR counter while the PIT runs at its rate. volatile uint32_t gAudioIsrFires = 0; uint16_t gAudioIsrHz = 0; volatile bool gAudioIsrActive = false; @@ -940,15 +932,6 @@ static void audioTickIsr(void) { } -static void programPit0(uint16_t divisor) { - // divisor == 0 in the I/O port writes means "65536" to the - // hardware -- the BIOS default that gives 18.2 Hz. - outportb(PIT_CTRL_PORT, PIT_CTRL_T0_MODE3); - outportb(PIT_TIMER0_PORT, (uint8_t)(divisor & 0xFFu)); - outportb(PIT_TIMER0_PORT, (uint8_t)((divisor >> 8) & 0xFFu)); -} - - bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { uint32_t divisor; @@ -957,7 +940,7 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { __asm__ __volatile__("cli" ::: "memory"); gAudioTickFn = NULL; gAudioIsrActive = false; - programPit0(0u); // restore BIOS-default 18.2 Hz + dosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); // BIOS rate, in the mode the clock reads _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec); __asm__ __volatile__("sti" ::: "memory"); _go32_dpmi_free_iret_wrapper(&gNewTimerVec); @@ -995,7 +978,7 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { gAudioIsrHz = hz; gAudioIsrActive = true; _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gNewTimerVec); - programPit0((uint16_t)divisor); + dosPitProgram(PIT_CTRL_T0_MODE2, (uint16_t)divisor); __asm__ __volatile__("sti" ::: "memory"); return true; } diff --git a/src/dos/dosTimer.h b/src/dos/dosTimer.h new file mode 100644 index 0000000..f9370d3 --- /dev/null +++ b/src/dos/dosTimer.h @@ -0,0 +1,36 @@ +// DOS timer hardware shared by the port's clock (hal.c) and its audio +// dispatcher (audio.c): the 8253/8254 PIT counter 0 that drives IRQ0, the +// master 8259 PIC, and the BIOS tick count IRQ0 advances. +// +// Counter 0 always runs in MODE 2 (rate generator). Its IRQ rate is the +// same as the BIOS's mode 3 at any divisor, but in mode 2 the count falls +// once per period instead of twice, so a latched count is the position +// inside the current tick -- what hal.c's millisecond clock reads. + +#ifndef JOEYLIB_DOS_TIMER_H +#define JOEYLIB_DOS_TIMER_H + +#include + +#define PIT_CTRL_PORT 0x43u +#define PIT_TIMER0_PORT 0x40u +#define PIT_CTRL_T0_MODE2 0x34u // counter 0, R/W LSB then MSB, mode 2 (rate generator), binary +#define PIT_CTRL_T0_MODE3 0x36u // the BIOS's own setting (square wave), restored at exit +#define PIT_CTRL_T0_LATCH 0x00u // counter 0, latch the count +#define PIT_CLOCK_HZ 1193182uL +#define PIT_BIOS_DIVISOR 0u // 65536: the BIOS's 18.2065 Hz + +#define BIOS_TICK_PERIOD 65536uL // PIT clocks per BIOS tick +#define BIOS_TICK_ADDR 0x46Cu // $0040:$006C, ticks since midnight +#define BIOS_TICKS_PER_DAY 0x1800B0uL // where the BIOS count wraps at midnight + +#define PIC1_CMD 0x20u +#define PIC1_IMR 0x21u +#define PIC_OCW3_READ_IRR 0x0Au // the next read of PIC1_CMD returns the IRR +#define PIC_IRQ0_BIT 0x01u + +// Program counter 0 with a PIT_CTRL_T0_* mode and a divisor (0 = 65536). +// The caller keeps interrupts off around it. src/dos/hal.c. +void dosPitProgram(uint8_t ctrl, uint16_t divisor); + +#endif diff --git a/src/dos/hal.c b/src/dos/hal.c index 2ec706c..f42e22c 100644 --- a/src/dos/hal.c +++ b/src/dos/hal.c @@ -15,15 +15,16 @@ #include #include #include -#include // uclock / UCLOCKS_PER_SEC #include #include #include #include #include +#include #include +#include "dosTimer.h" #include "port.h" #include "surfaceInternal.h" #include "x86Expand.h" @@ -39,10 +40,13 @@ // ----- Prototypes ----- -static void crashHandler(int sig); -static void installCrashLog(void); -static void uploadPalette(const jlSurfaceT *src); -static void uploadPaletteIfNeeded(const jlSurfaceT *src); +static void crashHandler(int sig); +void dosPitProgram(uint8_t ctrl, uint16_t divisor); +static void installCrashLog(void); +static uint64_t pitClocksNow(void); +static void pitClockStart(void); +static void uploadPalette(const jlSurfaceT *src); +static void uploadPaletteIfNeeded(const jlSurfaceT *src); // ----- Module state ----- @@ -50,6 +54,15 @@ static uint8_t *gVgaMem = NULL; static bool gNearEnabled = false; static FILE *gCrashLog = NULL; +// The millisecond clock (pitClocksNow, jlpMillisElapsed). +static uint64_t gPitStart = 0u; // PIT clocks at jlpInit +static uint64_t gPitDayClocks = 0u; // PIT clocks of the midnights passed +static uint32_t gPitLastTicks = 0u; // BIOS ticks at the last read +static uint32_t gMillisLast = 0u; // value returned last +static uint32_t gMillisRebase = 0u; // added to the current source's ms +static uint16_t gMillisLastHz = 0u; // source of gMillisLast: 0 = PIT, else the audio ISR's rate +static uint32_t gMillisLastIsr = 0u; // audio ISR fires at the last read + // (The old $3DA edge-poll frame-counter state lived here; removed with // the poll-rate-dependent counter itself -- see jlpFrameCount.) @@ -140,6 +153,14 @@ static void crashHandler(int sig) { } +// Program PIT counter 0 (dosTimer.h). Shared with src/dos/audio.c. +void dosPitProgram(uint8_t ctrl, uint16_t divisor) { + outportb(PIT_CTRL_PORT, ctrl); + outportb(PIT_TIMER0_PORT, (uint8_t)(divisor & 0xFFu)); + outportb(PIT_TIMER0_PORT, (uint8_t)((divisor >> 8) & 0xFFu)); +} + + // Open CRASH.LOG in cwd (DOSBox's mounted dir on the host) and install // signal handlers that route fault reports there. This runs before the // VGA mode change so its file handle is established while the screen @@ -167,6 +188,62 @@ static void installCrashLog(void) { // memcmp per present (NATIVE-PERF Phase 2 R9). The SCB flag is not // consumed here: mode 13h has one DAC; the per-line SCB choice is // applied in the pixel expansion, which reads src->scb directly. +// PIT clocks (1.193182 MHz) since the BIOS's midnight, plus the midnights +// seen: BIOS ticks * 65536 and the position inside the current tick from +// counter 0 (mode 2, dosTimer.h). All three are read with interrupts off, +// and a tick IRQ0 has already raised but the BIOS has not yet counted is +// added by hand. DJGPP's uclock() missed that case, so its time stepped +// back a tick and forward again; a frame difference taken across the step +// came out huge or short depending only on when the call landed, and UBER +// rows measured on DOS ended early by a code-layout lottery. +static uint64_t pitClocksNow(void) { + uint32_t ticks; + uint16_t count; + uint16_t intoTick; + uint8_t irr; + int wasEnabled; + + wasEnabled = disable(); + outportb(PIT_CTRL_PORT, PIT_CTRL_T0_LATCH); + count = inportb(PIT_TIMER0_PORT); + count |= (uint16_t)((uint16_t)inportb(PIT_TIMER0_PORT) << 8); + ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR); + outportb(PIC1_CMD, PIC_OCW3_READ_IRR); + irr = inportb(PIC1_CMD); + if (wasEnabled) { + enable(); + } + // Mode 2 counts 65536 (read as 0), 65535 .. 1, so this is the PIT + // clocks since the tick began. + intoTick = (uint16_t)(0u - count); + // IRQ0 pending with the count early in a tick: the wrap came before + // the latch and the BIOS has not counted it. (Pending with the count + // late in a tick means it wrapped after the latch: nothing to add.) + if ((irr & PIC_IRQ0_BIT) != 0u && intoTick < (uint16_t)(BIOS_TICK_PERIOD / 2u)) { + ticks++; + } + if (ticks < gPitLastTicks) { + gPitDayClocks += (uint64_t)BIOS_TICKS_PER_DAY * BIOS_TICK_PERIOD; + } + gPitLastTicks = ticks; + return gPitDayClocks + (uint64_t)ticks * BIOS_TICK_PERIOD + intoTick; +} + + +// Put counter 0 in mode 2 at the BIOS's own rate (the IRQ0 rate does not +// change) and take the clock's zero. +static void pitClockStart(void) { + int wasEnabled; + + wasEnabled = disable(); + dosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); + if (wasEnabled) { + enable(); + } + gPitStart = pitClocksNow(); +} + + static void uploadPaletteIfNeeded(const jlSurfaceT *src) { if (!gStagePaletteDirty) { return; @@ -184,6 +261,7 @@ bool jlpInit(const jlConfigT *config) { (void)config; installCrashLog(); + pitClockStart(); memset(®s, 0, sizeof(regs)); regs.x.ax = 0x0013; @@ -224,25 +302,21 @@ void jlpPresent(const jlSurfaceT *src) { // bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4 // gives the byte range x86ExpandRow needs. // Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed - // ST/Amiga idle-scan trick): a clean row's minWord is the 0xFF - // CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX - // (79), so 4 clean rows read exactly 0xFFFFFFFF and the whole - // group skips. A failing group keeps the per-row CLEAN test, so a - // clean row INSIDE a dirty group is skipped exactly as before -- - // written bytes and counters are byte-identical to the row loop. + // ST/Amiga idle-scan trick; STAGE_DIRTY_GROUP4_CLEAN). A failing + // group keeps the per-row CLEAN test, so a clean row INSIDE a dirty + // group is skipped exactly as before -- written bytes and counters are + // byte-identical to the row loop. { - typedef uint32_t __attribute__((may_alias)) BandGroupT; - const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord; - uint8_t g; + uint8_t g; - for (g = 0u; g < (uint8_t)(SURFACE_HEIGHT / 4u); g++) { + for (g = 0u; g < (uint8_t)(SURFACE_HEIGHT >> STAGE_DIRTY_GROUP4_SHIFT); g++) { int16_t yEnd; - if (minGroups[g] == 0xFFFFFFFFul) { + if (STAGE_DIRTY_GROUP4_CLEAN(g)) { continue; } - yEnd = (int16_t)((uint16_t)g * 4u + 4u); - for (y = (int16_t)((uint16_t)g * 4u); y < yEnd; y++) { + yEnd = (int16_t)(((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT) + STAGE_DIRTY_GROUP4_ROWS); + for (y = (int16_t)((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT); y < yEnd; y++) { // Canonical clean-row test (finding #47): min > max is the // CLEAN sentinel; see STAGE_DIRTY_ROW_CLEAN. if (STAGE_DIRTY_ROW_CLEAN(y)) { @@ -319,8 +393,8 @@ void jlpWaitVBL(void) { } -// Frame count derived from the millisecond clock (PIT ISR fires or -// uclock -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The +// Frame count derived from the millisecond clock (PIT clocks or audio +// ISR fires -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The // old implementation edge-polled $3DA bit 3 and only advanced when a // call happened to land DURING the ~1ms retrace pulse, so any caller // doing more than ~1ms of work between polls silently lost frames @@ -340,56 +414,58 @@ uint16_t jlpFrameHz(void) { } -// Wall-clock ms has two possible sources: +// Wall-clock ms has two possible sources, joined into one timeline: // -// 1. When the audio HAL has reprogrammed PIT counter 0 for the -// 60 Hz sound dispatcher, uclock()'s sub-tick read is wrong -// (uclock assumes 0..65535 cycles; ours is 0..gPitDivisor). -// We bypass uclock and derive ms from the ISR fire counter, -// which advances exactly once per 1/gAudioIsrHz second wall. +// 1. pitClocksNow (above), from jlpInit's zero. +// 2. While the audio HAL drives counter 0 at its own rate the count no +// longer spans a BIOS tick, so the audio ISR's fire counter is used +// instead: each fire is 1000/hz ms, hardware-IRQ-locked. // -// 2. Otherwise (audio ISR not installed), uclock works correctly -// because PIT 0 is at the BIOS default and DJGPP's sub-tick -// math holds. +// When the source changes -- audio starts, stops, re-registers at another +// rate or restarts its counter -- the new source is rebased onto the last +// value returned, so the clock never jumps back. // // gAudioIsr* are owned by src/dos/audio.c. extern volatile uint32_t gAudioIsrFires; extern uint16_t gAudioIsrHz; extern volatile bool gAudioIsrActive; -static uclock_t gMillisBase = 0; -static bool gMillisBaseSet = false; - uint32_t jlpMillisElapsed(void) { - uclock_t delta; + uint32_t ms; + uint32_t fires; + uint16_t hz; - if (gAudioIsrActive && gAudioIsrHz > 0u) { - // Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so - // wall-clock-correct and monotonic. uint64 promotion in - // the multiply keeps 60 Hz * 4M ticks (~18 hours) from - // overflowing. - return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL) - / (uint32_t)gAudioIsrHz); + hz = (gAudioIsrActive && gAudioIsrHz > 0u) ? gAudioIsrHz : 0u; + if (hz != 0u) { + // uint64 promotion keeps 60 Hz * 4M fires (~18 hours) in range. + fires = gAudioIsrFires; + ms = (uint32_t)(((uint64_t)fires * 1000uL) / (uint32_t)hz); + } else { + // Pre-scale by 1000 before dividing by the PIT rate: dividing + // by a truncated clocks-per-ms (1193) would run ~0.015% fast. + fires = 0u; + ms = (uint32_t)(((pitClocksNow() - gPitStart) * 1000uL) / PIT_CLOCK_HZ); } - // Use an explicit one-shot flag rather than gMillisBase == 0, so a - // legitimate uclock() value of 0 on the first call cannot re-arm - // the base every call. - if (!gMillisBaseSet) { - gMillisBase = uclock(); - gMillisBaseSet = true; - return 0u; + if (hz != gMillisLastHz || fires < gMillisLastIsr) { + gMillisRebase = gMillisLast - ms; + gMillisLastHz = hz; } - // Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of - // dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not - // 1193.18), which would run the clock ~0.015% fast. The uint64 - // intermediate keeps delta*1000 from overflowing. - delta = uclock() - gMillisBase; - return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC); + gMillisLastIsr = fires; + gMillisLast = ms + gMillisRebase; + return gMillisLast; } void jlpShutdown(void) { __dpmi_regs regs; + int wasEnabled; + + // Counter 0 back to the BIOS's own mode 3 at its own rate. + wasEnabled = disable(); + dosPitProgram(PIT_CTRL_T0_MODE3, PIT_BIOS_DIVISOR); + if (wasEnabled) { + enable(); + } if (gNearEnabled) { __djgpp_nearptr_disable(); diff --git a/src/dvx/audio.c b/src/dvx/audio.c index 6c84ba7..d28db3b 100644 --- a/src/dvx/audio.c +++ b/src/dvx/audio.c @@ -42,7 +42,6 @@ // ----- Constants ----- #define NO_VOICE 0 // audio.lib voice ids are > 0 -#define NO_TIMER (-1) // JoeyLib SFX are signed 8-bit; DVX's 8-bit PCM is unsigned. #define PCM8_SIGN_FLIP 0x80u @@ -69,10 +68,6 @@ #define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ #define NOISE_MIN_HZ (NOISE_CURVE_TOP_HZ - NOISE_PITCH_MAX * NOISE_CURVE_STEP_HZ) -// Pinned for the timer interrupt: the tick counter's code (one small -// function; a page is as fine a grain as pinning has) and data. -#define TICK_ISR_LOCK_BYTES 4096u - // ----- Types ----- // One SFX slot: a one-shot sample or a stream (or neither), its voice and @@ -142,9 +137,8 @@ static uint32_t gPcmSize = 0; // jlAudioTickRegister: the callback, its DVX timer and the ticks the timer // has counted that are not delivered yet. static jlAudioTickFnT gTickFn = NULL; -static int32_t gTickTimer = NO_TIMER; +static int32_t gTickTimer = DVX_PORT_NO_TIMER; static volatile uint32_t gTicksDue = 0; -static bool gTickPinned = false; // ----- Internal helpers (alphabetical) ----- @@ -275,17 +269,12 @@ static void tickIsr(void *userData) { static void tickStop(void) { - if (gTickTimer != NO_TIMER) { - platformTimerRemove(gTickTimer); - gTickTimer = NO_TIMER; + if (gTickTimer != DVX_PORT_NO_TIMER) { + dvxPortTimerStop(gTickTimer, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue)); + gTickTimer = DVX_PORT_NO_TIMER; } gTickFn = NULL; gTicksDue = 0; - if (gTickPinned) { - (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); - (void)platformUnlockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false); - gTickPinned = false; - } } @@ -462,19 +451,10 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { if (fn == NULL) { return true; } - if (platformLockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true) != 0) { - return false; - } - if (platformLockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false) != 0) { - (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); - return false; - } - gTickPinned = true; - gTickFn = fn; - gTickTimer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, tickIsr, NULL); - if (gTickTimer < 0) { - gTickTimer = NO_TIMER; - tickStop(); + gTickFn = fn; + gTickTimer = dvxPortTimerStart(hz, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue)); + if (gTickTimer == DVX_PORT_NO_TIMER) { + gTickFn = NULL; return false; } return true; diff --git a/src/dvx/dvxPort.h b/src/dvx/dvxPort.h index 63a27fb..86ddfa2 100644 --- a/src/dvx/dvxPort.h +++ b/src/dvx/dvxPort.h @@ -13,6 +13,8 @@ #include #include +#include "joey/surface.h" + // The program's task stack. DVX's default (32 KB) is far below what a DOS // program gets, and JoeyLib programs keep sizable buffers on the stack. #define DVX_PORT_STACK_BYTES (256 * 1024) @@ -40,6 +42,15 @@ void dvxPortExit(int status) __attribute__((noreturn)); // main's status. int32_t dvxPortRun(DxeAppContextT *ctx); +// A timer-service callback at hz with its code (the page(s) from fn on) and +// dataBytes of data pinned, as DVX requires of anything its timer interrupt +// runs. Returns the timer, or DVX_PORT_NO_TIMER with nothing left pinned. +// dvxPortTimerStop removes the timer (if any) and unpins both. +#define DVX_PORT_NO_TIMER (-1) +#define DVX_PORT_ISR_LOCK_BYTES 4096u +int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); +void dvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); + // ----- hal.c ----- extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown @@ -52,6 +63,11 @@ void dvxPortEndIfClosed(void); // Alt+Enter); the choice is kept for the program's next run. void dvxPortFullScreenToggle(void); +// UBER_PROBE's display parity row (examples/uber declares it itself): the +// pixels where the stage is shown that differ from the stage, and how many +// were compared; 0xFFFFFFFF with nowhere to show it. +uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); + // Called where a JoeyLib program comes back to the port every frame (the // input poll, the present): yield to the desktop if the program has not // this virtual frame, then dvxPortEndIfClosed. diff --git a/src/dvx/hal.c b/src/dvx/hal.c index 737fbca..af5e4ba 100644 --- a/src/dvx/hal.c +++ b/src/dvx/hal.c @@ -75,6 +75,7 @@ // $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF). #define RGB12_RED_SHIFT 8 #define RGB12_GREEN_SHIFT 4 +#define RGB12_BLUE_SHIFT 0 #define RGB12_CHANNEL_MASK 0x0Fu #define RGB12_TO_RGB24 17u @@ -88,6 +89,9 @@ #define PALETTES_NONE 0x0000u #define PALETTES_ALL 0xFFFFu +// dvxPortProbeDisplayMismatch with nowhere the stage is shown. +#define PROBE_NO_TARGET 0xFFFFFFFFu + // Game menu commands. #define CMD_GAME_FULL_SCREEN 100 #define CMD_GAME_HELP 101 @@ -126,6 +130,7 @@ typedef struct { static uint64_t clockFrame(void); static uint64_t clockMicros(void); static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1); +static void frameTickIsr(void *userData); static void fullSet(bool on); static void lutBuild(const jlSurfaceT *src, uint16_t palettes); static LutKindE lutKind(const DisplayT *d); @@ -135,23 +140,29 @@ static void onFullChange(WindowT *win, ExclusiveStateE state, void *u static void onMenu(WindowT *win, int32_t menuId); static void onPaint(WindowT *win, RectT *dirty); static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show); +static uint32_t probePixelRead(const uint8_t *p, int32_t bytesPerPixel); +static uint32_t probeStagePixel(const DisplayT *d, LutKindE kind, const jlSurfaceT *src, int32_t x, int32_t y); static void redrawAll(void); +static uint8_t rgb12Channel(uint16_t rgb, uint8_t shift); static bool settingFullScreenLoad(void); static void settingFullScreenSave(bool on); static const DisplayT *targetDisplay(void); static bool targetGet(PresentTargetT *t); static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h); static bool windowCreate(AppContextT *ac); +static void yieldMark(uint64_t frame); // ----- Module state ----- WindowT *gDvxPortWindow = NULL; -static int32_t gScale = 1; // window pixels per stage pixel, each way -static GameFramePacerT gPacer; -static bool gClockStarted = false; -static bool gCloseRequested = false; -static uint64_t gYieldFrame = 0; // the frame the program last yielded in +static int32_t gScale = 1; // window pixels per stage pixel, each way +static GameFramePacerT gPacer; +static bool gClockStarted = false; +static bool gCloseRequested = false; +static uint64_t gYieldNextUs = 0; // no frame timer: the clock time the next frame starts +static int32_t gFrameTimer = DVX_PORT_NO_TIMER; +static volatile uint8_t gYieldDue = 0; // set by the frame timer each virtual frame // Full screen: whether the program holds it (DVX's owner: the user's choice, // until the mode can no longer be set) and whether the screen is its own now @@ -273,6 +284,14 @@ static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, } +// The frame timer (DVX_FRAME_HZ, in DVX's timer interrupt): the next +// dvxPortService yields. +static void frameTickIsr(void *userData) { + (void)userData; + gYieldDue = 1u; +} + + // Full screen on or off; the Game menu's check mark follows. On: DVX sets // the mode and tells onFullChange (Active) before this returns. Off: the // desktop comes back, and the window is redrawn. @@ -315,9 +334,9 @@ static void lutBuild(const jlSurfaceT *src, uint16_t palettes) { } for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) { rgb = src->palette[p][c]; - r = (uint8_t)(((rgb >> RGB12_RED_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); - g = (uint8_t)(((rgb >> RGB12_GREEN_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); - b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); + r = rgb12Channel(rgb, RGB12_RED_SHIFT); + g = rgb12Channel(rgb, RGB12_GREEN_SHIFT); + b = rgb12Channel(rgb, RGB12_BLUE_SHIFT); index = (uint8_t)((p << NIBBLE_BITS) | c); switch (gLutKind) { case LUT_INDEXED: @@ -446,6 +465,7 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { int32_t runY0; int32_t runX0; int32_t runX1; + int32_t clean; uint8_t maxWord; if (!targetGet(&t) || t.pixels == NULL) { @@ -455,15 +475,25 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { runX0 = 0; runX1 = 0; for (y = 0; y < t.rows; y++) { - if ((palettes & (1u << src->scb[y])) != 0) { - x0 = 0; - x1 = SURFACE_WIDTH; - } else if (STAGE_DIRTY_ROW_CLEAN(y)) { + // Clean rows end the run: four at a time where no palette change + // forces a redraw (STAGE_DIRTY_GROUP4_CLEAN), else one. + clean = 0; + if (palettes == PALETTES_NONE && (y & (int32_t)(STAGE_DIRTY_GROUP4_ROWS - 1u)) == 0 && y + (int32_t)STAGE_DIRTY_GROUP4_ROWS <= t.rows && STAGE_DIRTY_GROUP4_CLEAN(y >> STAGE_DIRTY_GROUP4_SHIFT)) { + clean = (int32_t)STAGE_DIRTY_GROUP4_ROWS; + } else if ((palettes & (1u << src->scb[y])) == 0 && STAGE_DIRTY_ROW_CLEAN(y)) { + clean = 1; + } + if (clean != 0) { if (runY0 >= 0 && show) { targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0); } runY0 = -1; + y += clean - 1; continue; + } + if ((palettes & (1u << src->scb[y])) != 0) { + x0 = 0; + x1 = SURFACE_WIDTH; } else { // Clamp a band past the row end, as the DOS present does. maxWord = gStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? gStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1); @@ -492,6 +522,46 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { } +// A pixel of the target, bytesPerPixel wide, as expandRow stores them. +static uint32_t probePixelRead(const uint8_t *p, int32_t bytesPerPixel) { + switch (bytesPerPixel) { + case 1: + return *p; + + case 2: + return *(const uint16_t *)p; + + default: + return *(const uint32_t *)p; + } +} + + +// The pixel stage pixel (x, y) must be on display d with tables of the +// given kind, derived from its palette entry rather than through gLut. +static uint32_t probeStagePixel(const DisplayT *d, LutKindE kind, const jlSurfaceT *src, int32_t x, int32_t y) { + uint8_t scb; + uint8_t packed; + uint8_t index; + uint16_t rgb; + uint8_t r; + uint8_t g; + uint8_t b; + + scb = src->scb[y]; + packed = src->pixels[y * SURFACE_BYTES_PER_ROW + x / STAGE_PIXELS_PER_BYTE]; + index = (x % STAGE_PIXELS_PER_BYTE) != 0 ? (uint8_t)(packed & NIBBLE_MASK) : (uint8_t)(packed >> NIBBLE_BITS); + if (kind == LUT_INDEXED) { + return (uint32_t)((scb << NIBBLE_BITS) | index); + } + rgb = src->palette[scb][index]; + r = rgb12Channel(rgb, RGB12_RED_SHIFT); + g = rgb12Channel(rgb, RGB12_GREEN_SHIFT); + b = rgb12Channel(rgb, RGB12_BLUE_SHIFT); + return kind == LUT_NEAREST ? gamePalNearest(d->palette, r, g, b) : packColor(d, r, g, b); +} + + // Full screen came or went: new tables, and every row again, wherever the // stage goes now. Before jlInit has the stage the first present does it. static void redrawAll(void) { @@ -504,6 +574,12 @@ static void redrawAll(void) { } +// Channel shift of a $0RGB colour, widened to 8 bits. +static uint8_t rgb12Channel(uint16_t rgb, uint8_t shift) { + return (uint8_t)(((rgb >> shift) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); +} + + // The saved choice: whether the program was on the whole screen when the // user last switched. static bool settingFullScreenLoad(void) { @@ -663,6 +739,15 @@ static bool windowCreate(AppContextT *ac) { } +// The program has yielded in frame `frame`: the next yield is due when frame +// + 1 starts (rounded up to the microsecond, so the frame number there is +// already frame + 1), or when the frame timer next ticks. +static void yieldMark(uint64_t frame) { + gYieldDue = 0u; + gYieldNextUs = ((frame + 1u) * PLATFORM_US_PER_SEC + DVX_FRAME_HZ - 1u) / DVX_FRAME_HZ; +} + + // ----- Port API (alphabetical) ----- void dvxPortEndIfClosed(void) { @@ -679,13 +764,88 @@ void dvxPortFullScreenToggle(void) { } -void dvxPortService(void) { - uint64_t frame; +// UBER_PROBE's display parity row (examples/uber): the pixels of where the +// stage is shown now -- the window's content buffer, or the full-screen +// backbuffer while the program has the screen -- that differ from the +// stage, every screen pixel of each stage pixel's scale x scale block +// counted; on the whole screen at 8 bits also the screen's palette entries +// that differ from the stage's palettes (a palette change there moves no +// pixel). *compared gets how many pixels and entries were checked. What a +// pixel must be comes from the stage's own palette entry, not through the +// present's tables, so a stale or wrong table, row or scale shows; where +// the stage goes is the present's own targetGet, which the screenshot +// gates check. Always compiled: the probe build links the stock library. +uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { + PresentTargetT t; + const DisplayT *d; + const uint8_t *row; + const uint8_t *entry; + LutKindE kind; + uint32_t want; + uint32_t bad; + int32_t x; + int32_t y; + int32_t sx; + int32_t sy; + uint8_t p; + uint8_t c; + uint16_t rgb; - frame = clockFrame(); - if (frame != gYieldFrame) { - gYieldFrame = frame; - tsYield(); + *compared = 0u; + if (src == NULL || !targetGet(&t) || t.pixels == NULL) { + return PROBE_NO_TARGET; + } + d = targetDisplay(); + kind = lutKind(d); + bad = 0u; + for (y = 0; y < t.rows; y++) { + for (x = 0; x < t.cols; x++) { + want = probeStagePixel(d, kind, src, x, y); + for (sy = 0; sy < t.scale; sy++) { + row = t.pixels + (y * t.scale + sy) * t.pitch + x * t.scale * t.bytesPerPixel; + for (sx = 0; sx < t.scale; sx++) { + if (probePixelRead(row + sx * t.bytesPerPixel, t.bytesPerPixel) != want) { + bad++; + } + (*compared)++; + } + } + } + } + if (kind == LUT_INDEXED) { + for (p = 0; p < SURFACE_PALETTE_COUNT; p++) { + for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) { + rgb = src->palette[p][c]; + entry = &d->palette[((p << NIBBLE_BITS) | c) * PAL_CHANNELS]; + if (entry[0] != rgb12Channel(rgb, RGB12_RED_SHIFT) || entry[1] != rgb12Channel(rgb, RGB12_GREEN_SHIFT) || entry[2] != rgb12Channel(rgb, RGB12_BLUE_SHIFT)) { + bad++; + } + (*compared)++; + } + } + } + return bad; +} + + +// Yield at most once a frame. With the frame timer this is a byte test: a +// clock read costs ~5 us under DOSBox (a PIT latch), paid by every present, +// input poll and serial call. Without it, a clock read and a compare, the +// division to the frame number once a frame (yieldMark). +void dvxPortService(void) { + uint64_t now; + + if (gFrameTimer != DVX_PORT_NO_TIMER) { + if (gYieldDue != 0u) { + gYieldDue = 0u; + tsYield(); + } + } else { + now = clockMicros(); + if (now >= gYieldNextUs) { + yieldMark(now * DVX_FRAME_HZ / PLATFORM_US_PER_SEC); + tsYield(); + } } dvxPortEndIfClosed(); } @@ -725,6 +885,8 @@ bool jlpInit(const jlConfigT *config) { if (!windowCreate(gDvxPortProgram->shellCtx)) { return false; } + // Paces dvxPortService's yields; without it the service reads the clock. + gFrameTimer = dvxPortTimerStart(DVX_FRAME_HZ, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); if (settingFullScreenLoad()) { fullSet(true); } @@ -767,6 +929,10 @@ void jlpPresent(const jlSurfaceT *src) { // Destroying the window gives the screen back too when the program has it // (DVX tells onFullChange nothing then). void jlpShutdown(void) { + if (gFrameTimer != DVX_PORT_NO_TIMER) { + dvxPortTimerStop(gFrameTimer, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); + gFrameTimer = DVX_PORT_NO_TIMER; + } if (gDvxPortWindow != NULL) { dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow); gDvxPortWindow = NULL; @@ -785,11 +951,11 @@ void jlpWaitVBL(void) { if (gFullActive) { tsYield(); dvxExclusiveWaitRetrace(); - gYieldFrame = clockFrame(); + yieldMark(clockFrame()); } else { gPacer.frame = clockFrame(); (void)gameFrameWait(&gPacer); - gYieldFrame = gPacer.frame; + yieldMark(gPacer.frame); } dvxPortEndIfClosed(); } diff --git a/src/dvx/program.c b/src/dvx/program.c index 026cf0a..e19968e 100644 --- a/src/dvx/program.c +++ b/src/dvx/program.c @@ -151,6 +151,37 @@ int32_t dvxPortRun(DxeAppContextT *ctx) { } +// DVX runs timer callbacks inside its timer interrupt, so their code and the +// data they touch must be pinned first; DVX removes the timer itself when the +// program ends. +int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { + int32_t timer; + + if (platformLockRegion((const void *)fn, DVX_PORT_ISR_LOCK_BYTES, true) != 0) { + return DVX_PORT_NO_TIMER; + } + if (platformLockRegion(data, dataBytes, false) != 0) { + (void)platformUnlockRegion((const void *)fn, DVX_PORT_ISR_LOCK_BYTES, true); + return DVX_PORT_NO_TIMER; + } + timer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, fn, NULL); + if (timer < 0) { + dvxPortTimerStop(DVX_PORT_NO_TIMER, fn, data, dataBytes); + return DVX_PORT_NO_TIMER; + } + return timer; +} + + +void dvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { + if (timer != DVX_PORT_NO_TIMER) { + platformTimerRemove(timer); + } + (void)platformUnlockRegion((const void *)fn, DVX_PORT_ISR_LOCK_BYTES, true); + (void)platformUnlockRegion(data, dataBytes, false); +} + + // ----- HAL API (alphabetical) ----- const char *jlpReadRoot(void) { diff --git a/src/generic/genericTile.c b/src/generic/genericTile.c index a6abf85..69d3b36 100644 --- a/src/generic/genericTile.c +++ b/src/generic/genericTile.c @@ -20,8 +20,7 @@ // purpose: the previous byte-at-a-time form let gcc assume d/s alias, which // blocked store merging; memcpy asserts non-overlap and compiles to one // 32-bit move per row on DOS (DJGPP). Only DOS/BLANK run these generics -- -// IIgs uses asm macros and Amiga/ST override every tile op -- so the IIgs -// byte-loop-libc memcpy caveat (PERF-AUDIT #79) does not apply here. +// IIgs uses asm macros and Amiga/ST override every tile op. #define GENERIC_TILE_COPY_ROWS(_d, _dStride, _s, _sStride) do { \ uint8_t copyRow_; \ for (copyRow_ = 0; copyRow_ < TILE_PIXELS_PER_SIDE; copyRow_++) { \ diff --git a/src/iigs/hal.c b/src/iigs/hal.c index dee52d4..9673bfa 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -66,13 +66,6 @@ typedef void *Handle; extern uint16_t iigsGetTickWord(void); extern uint16_t iigsVblInstall(void); extern uint16_t iigsVblRemove(void); -// MVN block copy (peislam.s): one run that crosses no bank on either side. -typedef struct { - uint32_t dst; - uint32_t src; - uint16_t len; -} IigsMvnArgsT; -extern void iigsMvnCopy(const IigsMvnArgsT *args); /* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */ extern uint16_t iigsReadHzParam(void); static uint16_t gFrameHz = 60u; @@ -266,10 +259,9 @@ volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group // start true (forces the very first present to upload), get set true // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // get cleared here after upload. -// Word stores, NOT memcpy: the clang IIgs libc memcpy is a ~30 cyc/byte -// byte loop (finding #79 in palette.c), so the 712 bytes here cost ~21k -// cycles -- 7.6 ms, a third of a frame -- every time both flags fire. -// 16-bit stores land at ~8 cyc/word, ~2.8k cycles for the pair. +// Word stores, NOT memcpy: 16-bit stores land at ~8 cycles a word, ~2.8k +// cycles for the pair, where libc memcpy's MVN moves a byte at a time at +// 7 cycles a byte (~5k). static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) { uint16_t i; @@ -455,39 +447,6 @@ void iigsIrqLatencyTighten(void) { } -// jlMemCopy: MVN moves of up to a bank at a time. MVN's X and Y wrap -// inside their banks, so a run ends where either side would cross one; -// Memory Manager blocks are free to straddle banks. -void iigsMemCopy(void *dst, const void *src, uint32_t bytes) { - static IigsMvnArgsT args; - uint32_t d = (uint32_t)dst; - uint32_t s = (uint32_t)src; - - while (bytes != 0u) { - uint32_t run = bytes; - uint32_t toD = 0x10000UL - (d & 0xFFFFu); - uint32_t toS = 0x10000UL - (s & 0xFFFFu); - - if (run > toD) { - run = toD; - } - if (run > toS) { - run = toS; - } - if (run > 0xFFFFu) { - run = 0xFFFFu; - } - args.dst = d; - args.src = s; - args.len = (uint16_t)run; - iigsMvnCopy(&args); - d += run; - s += run; - bytes -= run; - } -} - - // The Memory Manager owner ID for every JoeyLib allocation on the IIgs // (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates // a fresh application ID per call, so it is called exactly once here @@ -617,7 +576,9 @@ void jlpStageFreePixels(uint8_t *pixels) { // jlpSurfaceCopyRect override (NATIVE-PERF Phase 7; asm since hunt-3 // rank 1). History of this function IS the llvm-mos lesson arc: the -// generic per-row memcpy was a far byte-loop CALL (finding #79); the +// generic per-row memcpy was a far byte-loop CALL (finding #79; libc's +// memcpy is MVN now, but its per-call setup still loses on rows of a few +// words); the // Phase-7 replacement inlined a C word copy, which the -S output at // the time showed as lda [dp],y / sta -- but the hunt-3 probe measured // the SHIPPED build at ~5.3 ms per 16x16 erase, and the current -S diff --git a/src/iigs/input.c b/src/iigs/input.c index e876f25..a14e8a6 100644 --- a/src/iigs/input.c +++ b/src/iigs/input.c @@ -44,7 +44,6 @@ #include "port.h" #include "inputInternal.h" #include "joey/surface.h" -#include "surfaceInternal.h" // iigsByteFill (hunt-3 rank 5) // ----- Hardware registers ----- @@ -617,10 +616,9 @@ void jlpInputPoll(void) { // bit-7 read will clear normally. kbdStrb = *IIGS_KBDSTRB; if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) { - // iigsByteFill, not memset: llvm-mos lowers memset to a - // far-call byte loop (finding #79), and this branch runs on - // nearly EVERY real game frame (idle keyboard). Hunt-3 rank 5. - iigsByteFill(gKeyState, 0u, (uint16_t)sizeof(gKeyState)); + // Runs on nearly every real game frame (idle keyboard), so it is + // libc's 16-bit store fill, not a loop. Hunt-3 rank 5. + memset(gKeyState, 0, sizeof(gKeyState)); } readModifierKeys(); diff --git a/src/iigs/joeyDraw.s b/src/iigs/joeyDraw.s index bb42758..a7d9927 100644 --- a/src/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -5424,26 +5424,18 @@ mdrExit: ; ==================================================================== -; iigsStageBandsClearAll(void) / iigsStageBandsMarkAll(void) +; iigsStageBandsClearAll(void) ; -; Constant-fill BOTH dirty-band arrays (gStageMinWord/gStageMaxWord, -; 200 bytes each) without libc memset. llvm-mos lowers memset to -; `jsl memset` -- libc's `while (n--) *d++ = c` byte loop behind a far -; call (finding #79 class) -- and stageDirtyClearAll runs after EVERY -; present, so the two memsets were ~10 ms of every present's fixed -; base. A C word-fill rewrite still emitted ~35 instructions per store -; (llvm-mos re-derives the far-pointer address each iteration), so the -; fill lives here: 100 descending abs,x word stores per array, -; ~12 cyc/word. +; Reset the dirty bands (gStageMinWord/gStageMaxWord, 200 bytes each) to +; the CLEAN sentinels after a present -- min = $FF (STAGE_DIRTY_CLEAN_MIN), +; max = $00 (CLEAN_MAX) -- for only the 8-row groups gStageDirtyGroup says +; were marked, clearing those group bytes too. (It began as a blind fill +; when libc memset was a far-call byte loop; libc's memset is a tight +; 16-bit fill now, but no fill can skip the clean groups.) ; -; ClearAll: min = $FF (STAGE_DIRTY_CLEAN_MIN), max = $00 (CLEAN_MAX) -; MarkAll: min = $00, max = $4F (STAGE_DIRTY_FULL_MAX = 79) -; -; ABI: no args, no return. Same DBR-inherited abs,x write path as +; ABI: no args, no return. Same DBR-inherited abs,x/abs,y write path as ; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT -; phk/plb). php/plp preserves caller widths; A/X are cdecl scratch. -; SURFACE_HEIGHT = 200 -> last word offset 198; `bpl` exits after the -; offset-0 store wraps X to $FFFE (N set). +; phk/plb). php/plp preserves caller widths; A/X/Y are cdecl scratch. ; ==================================================================== .section .text.iigsStageBands,"ax" .globl iigsStageBandsClearAll @@ -5521,97 +5513,6 @@ isbcDone: plp rtl -.globl iigsStageBandsMarkAll -iigsStageBandsMarkAll: - php - rep #0x30 - .a16 - .i16 - ldx #198 - lda #0 ; minWord 0 in both bytes -isbmMinLoop: - sta gStageMinWord,x - dex - dex - bpl isbmMinLoop - ldx #198 - lda #0x4F4F ; STAGE_DIRTY_FULL_MAX in both bytes -isbmMaxLoop: - sta gStageMaxWord,x - dex - dex - bpl isbmMaxLoop - plp - rtl - - -; ==================================================================== -; iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count) -; -; Fill `count` bytes at far pointer `dst` with the byte value -; replicated in BOTH bytes of `fill`. Replaces llvm-mos memset -; lowering (`jsl memset` far-call byte loop) for the SCB range fill; -; [dp],y long-indirect stores ignore DBR and the 65816 has no -; alignment fault, so any dst parity works. Odd trailing byte is -; stored first in M=8, then descending word stores. count >= 1 -; (caller guards the empty range) and count < 32768 (the descending -; loop exits on N -- fine for the 200-byte SCB and any surface row). -; -; ABI: arg0 dst in A(lo16):X(bank). Stack after JSL: fill @4,s, -; count @6,s; after php+phd+phx+pha (+7): fill @11,s, count @13,s. -; D = SP via tsc/tcd, and SP points at the next FREE byte, so the -; pushed dst long pointer's bytes (lo, hi, bank -- pha last, phx -; first) sit at D+1..D+3: ibfDst = 1, NOT 0. (The 0 equate read one -; garbage byte into the pointer's low byte and slid the rest, writing -; the fill into a layout-roulette address -- DRAW happened to survive -; it, UBER crashed at boot.) -; ==================================================================== -.globl iigsByteFill -iigsByteFill: -ibfDst = 1 - - php - phd - rep #0x30 - .a16 - .i16 - phx ; dst bank word -> D+2..3 - pha ; dst lo16 -> D+0..1 - tsc - tcd ; D = SP: [ibfDst] = dst far ptr - lda 13,s ; count - tay - lsr a - bcc ibfEven ; even count - dey ; Y = count-1 = offset of odd tail - sep #0x20 - .a8 - lda 11,s ; fill value (low byte) - sta [ibfDst],y - rep #0x20 - .a16 - tya ; A = even remainder - bra ibfWords -ibfEven: - tya ; A = count -ibfWords: - beq ibfDone ; count was 1 -> only the tail - tay - dey - dey ; Y = last word offset - lda 11,s ; fill word -ibfLoop: - sta [ibfDst],y - dey - dey - bpl ibfLoop -ibfDone: - pla ; drop dst lo16 - plx ; drop dst bank - pld - plp - rtl - ; ==================================================================== ; iigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank, diff --git a/src/iigs/peislam.s b/src/iigs/peislam.s index 488ba05..146d7f2 100644 --- a/src/iigs/peislam.s +++ b/src/iigs/peislam.s @@ -121,57 +121,3 @@ iigsReadHzParam: jsl 0xe10000 pla ; A = result (return value) rtl - -; ---------------------------------------------------------------- -; void iigsMvnCopy(const IigsMvnArgsT *args) A = args low, X = bank -; -; +0 uint32_t dst 24-bit destination -; +4 uint32_t src 24-bit source -; +8 uint16_t len bytes, 1..65535, and hal.c guarantees neither -; [dst, dst+len) nor [src, src+len) crosses a bank -; (MVN's X/Y wrap inside one bank). -; -; One MVN: 7 cycles a byte against libc memcpy's byte loop of ~40. The -; two bank operands are patched into the instruction itself (this -; segment is RAM; the write goes through B = K, same section), then -; B is restored -- MVN leaves B pointing at the destination bank. -; ---------------------------------------------------------------- - .section .text.iigsMvnCopy,"ax" - .globl iigsMvnCopy -iigsMvnCopy: - php - rep #0x30 - .a16 - .i16 - phb - sta 0xe0 ; args low - txa - sta 0xe2 ; args bank - phk - plb ; B = K for the operand patch - ldy #6 - lda [0xe0],y ; src bank (byte at +6, junk high) - sep #0x20 - .a8 - sta mvnOp+2 ; MVN encodes source bank second - ldy #2 - lda [0xe0],y ; dst bank - sta mvnOp+1 ; destination bank first - rep #0x20 - .a16 - ldy #8 - lda [0xe0],y - dec a ; A = len - 1 - pha - ldy #4 - lda [0xe0],y ; src low - tax - ldy #0 - lda [0xe0],y ; dst low - tay - pla -mvnOp: - mvn 0, 0 ; patched: dst bank, src bank (54 dd ss) - plb - plp - rtl diff --git a/src/m68k/stageBands.s b/src/m68k/stageBands.s new file mode 100644 index 0000000..4bf043e --- /dev/null +++ b/src/m68k/stageBands.s @@ -0,0 +1,152 @@ +| Shared 68000 stage dirty bands: the multi-row mark and the whole-stage fill. +| +| Used by all three 68000 ports (Atari ST, Amiga, X68000). gStageMinWord and +| gStageMaxWord hold each stage row's first and last dirty 16-bit word, 0xFF / +| 0x00 when the row is clean (src/core/surfaceInternal.h). Both are aligned(2) +| byte arrays, so a row's two bytes sit at addresses of the same parity, and +| four consecutive rows starting on an even row read and write as one long. +| +| The C here was the cost: gcc's per-row loop is 60-76 cycles a row, and a +| 16x200 jlFillRect spent ~1.6 ms of an X68000's time marking its rows. gcc +| will not keep the group loop below in registers (it re-derives the key from +| the previous group every pass, ~124 cycles a group) and turns a fused clear +| back into two memset calls, so both are written out here. +| +| Both routines take the arrays as arguments rather than naming them: the C +| symbol is `_gStageMinWord` to gcc-mint and gcc-amigaos and `gStageMinWord` to +| elf2x68k, and this file is plain GAS with no preprocessor to choose. +| +| cdecl: every argument in a 4-byte stack slot, a uint16_t in its low word and +| a uint8_t in its low byte; d0-d1/a0-a1 are scratch, d2-d7/a2-a6 are saved. + + .equ ROWS, 200 | SURFACE_HEIGHT + .equ GROUP_ROWS, 4 | rows (bytes) in a long + .equ BAND_LONGS, ROWS / GROUP_ROWS +| The fewest rows that still hold TWO whole groups after the one row that may +| be needed to reach an even address: a second group is the first that can +| reuse the first group's result. Below that the rows go one by one. + .equ GROUP_MIN_ROWS, 2 * GROUP_ROWS + 1 + + .text + .even + .globl stageBandsFill68k + .globl _stageBandsFill68k + .globl stageBandsMark68k + .globl _stageBandsMark68k + +| Widen the row at (a0) / (a1) to cover [d2, d3], and step both to the next. +| cmp.b (An)+,Dn computes Dn - byte: carry clear means minWord >= the row's +| min, and "lower or same" means maxWord <= its max -- nothing to store. +.macro WIDEN_ROW + cmp.b (%a0)+,%d2 + bcc.s 1f + move.b %d2,-1(%a0) +1: cmp.b (%a1)+,%d3 + bls.s 2f + move.b %d3,-1(%a1) +2: +.endm + + +| void stageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, +| uint32_t minFill, uint32_t maxFill); +| +| Every row's min byte from minFill and max byte from maxFill, each a byte +| repeated four times: the clean sentinels after a present, or the whole row +| after jlSurfaceClear. Straight long stores to both arrays, ~1,240 cycles; +| the two libc memsets it replaces were ~1,900 (mintlib) to ~2,500 (elf2x68k). + .equ FILL_MIN_BANDS, 4 + .equ FILL_MAX_BANDS, 8 + .equ FILL_MIN_FILL, 12 + .equ FILL_MAX_FILL, 16 +stageBandsFill68k: +_stageBandsFill68k: + movea.l FILL_MIN_BANDS(%sp),%a0 + movea.l FILL_MAX_BANDS(%sp),%a1 + move.l FILL_MIN_FILL(%sp),%d0 + move.l FILL_MAX_FILL(%sp),%d1 + .rept BAND_LONGS + move.l %d0,(%a0)+ + move.l %d1,(%a1)+ + .endr + rts + + +| void stageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, +| uint8_t minWord, uint8_t maxWord); +| +| Widen `rows` rows (at least 2; a one-row mark is inline in C) from minRow / +| maxRow to cover [minWord, maxWord]. Byte-for-byte what the per-row loop +| does; tests/host/dirtyMarkHost.c checks the C twin of this routine against +| that loop, and scripts/verify-atarist-dirtymark.sh this one. +| +| GROUPS. Past one row to an even address, the rows go four at a time. A group +| is widened row by row once, and what it held before (the key) and after +| (the band) are kept. A later group whose rows hold exactly the key again +| must come out as exactly the band, so it takes two long stores instead: +| 82 cycles for four rows against ~250. Rows that were clean, and rows that +| an earlier rect of the same height marked, match group after group -- which +| is what a tall fill meets. A group that does not match becomes the new key. +| +| a0 = min row a1 = max row +| d0 = rows left over d1 = groups left +| d2 = minWord d3 = maxWord +| d4 = key's min long d5 = key's max long +| d6 = band's min long d7 = band's max long + .equ MARK_MIN_ROW, 4 + .equ MARK_MAX_ROW, 8 + .equ MARK_ROWS, 12 + 2 + .equ MARK_MIN_WORD, 16 + 3 + .equ MARK_MAX_WORD, 20 + 3 + .equ MARK_SAVED, 2 * 4 | d2-d3, pushed first +stageBandsMark68k: +_stageBandsMark68k: + movem.l %d2-%d3,-(%sp) + movea.l MARK_SAVED + MARK_MIN_ROW(%sp),%a0 + movea.l MARK_SAVED + MARK_MAX_ROW(%sp),%a1 + move.w MARK_SAVED + MARK_ROWS(%sp),%d0 + move.b MARK_SAVED + MARK_MIN_WORD(%sp),%d2 + move.b MARK_SAVED + MARK_MAX_WORD(%sp),%d3 + cmpi.w #GROUP_MIN_ROWS,%d0 + bcs.w markRows + movem.l %d4-%d7,-(%sp) + move.w %a0,%d1 + btst #0,%d1 + beq.s markEven + WIDEN_ROW | one row to an even address + subq.w #1,%d0 +markEven: + move.w %d0,%d1 + lsr.w #2,%d1 | whole groups, at least two + andi.w #GROUP_ROWS - 1,%d0 | and the rows after them +markMiss: + move.l (%a0),%d4 + move.l (%a1),%d5 + WIDEN_ROW + WIDEN_ROW + WIDEN_ROW + WIDEN_ROW + move.l -GROUP_ROWS(%a0),%d6 + move.l -GROUP_ROWS(%a1),%d7 + subq.w #1,%d1 + beq.s markGroupsDone +markMatch: + cmp.l (%a0),%d4 + bne.s markMiss + cmp.l (%a1),%d5 + bne.s markMiss + move.l %d6,(%a0)+ + move.l %d7,(%a1)+ + subq.w #1,%d1 + bne.s markMatch +markGroupsDone: + movem.l (%sp)+,%d4-%d7 +markRows: + subq.w #1,%d0 + bcs.s markDone +markRow: + WIDEN_ROW + dbra %d0,markRow +markDone: + movem.l (%sp)+,%d2-%d3 + rts diff --git a/src/x68000/hal.c b/src/x68000/hal.c index f1d1d90..98040f2 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -1,20 +1,15 @@ -// Sharp X68000 HAL -- bring-up (Phase B: chunky stage, expand on present). +// Sharp X68000 HAL: lifecycle, present, SCB, clocks and input. // -// The stage stays a chunky 4bpp surface in main RAM and jlpPresent expands it -// into GVRAM. That is deliberately the slow-but-correct path: it makes all of -// src/generic work unmodified, so the port renders correctly before any native -// primitive exists. Nothing here claims a JL_HAS_* override yet. +// The stage is native planar (src/x68000/halPlanar.c) and jlpPresent copies +// its dirty rows into the four 1bpp TEXT planes at $E00000, which are planar +// too (x68kPresentText, src/x68000/x68kPresent.s). The text plane has one +// 16-colour palette, so the SCB's per-line palettes are reloaded as the beam +// scans by a CRTC raster interrupt (x68kRasterIsr.s). The graphics-plane +// present (X68K_PRESENT_TEXT 0) is the alternative that needs no interrupt. // -// GVRAM lives at $C00000. In 16-colour mode a pixel occupies the low nibble of -// its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so -// one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address -// space per displayed pixel -- the reason this path is a stepping stone rather -// than the destination. See the storage-model note in joey/platform.h. -// -// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path -// renders, and UBER's captured hashes are byte-identical to the Apple IIgs -// golden reference. Input, serial and audio are separately proven -- see -// claudeDocs/x68000_port.md for the evidence for each. +// VERIFIED UNDER EMULATION (MAME 0.264 + installer/patches/mame): UBER's +// hashes match the Apple IIgs golden reference 51/51, and every subsystem has +// its own gate -- see claudeDocs/x68000_port.md for the evidence for each. #include #include @@ -337,6 +332,20 @@ static bool gVdispHooked = false; // _VDISPST took x68kVdispIsr static uint32_t gMillisBase = 0ul; // tickMillisNow() as jlpInit finished static uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL; +// Palette words by lookup, built once from x68kColorFromRgb12 by colorLutBuild: +// a $0RGB colour's red and green nibbles index one table and its blue nibble +// the other, and the two entries OR into exactly x68kColorFromRgb12's word. +// A present converts every colour of each palette that changed, and the +// function's shifts cost ~250 cycles a colour (6.4 ms for all 256, measured on +// examples/scb), the lookup ~70. +#define X68K_LUT_RED_GREEN_ENTRIES 256u +#define X68K_LUT_RED_GREEN_SHIFT 4u +#define X68K_LUT_RED_GREEN_MASK 0xFFu +#define X68K_LUT_BLUE_ENTRIES 16u +#define X68K_LUT_BLUE_MASK 0x0Fu +static uint16_t gColorLutRedGreen[X68K_LUT_RED_GREEN_ENTRIES]; +static uint16_t gColorLutBlue[X68K_LUT_BLUE_ENTRIES]; + #if X68K_PRESENT_TEXT // ----- SCB band tables (text-plane present) ----------------------------------- // @@ -346,6 +355,9 @@ static uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL; // reloads from a table built here -- see "SCB per-band palette" below. #define X68K_MAX_BANDS SURFACE_HEIGHT #define X68K_NO_PALETTE 0xFFu // no line's SCB value; starts the first band +#define X68K_BAND_TABLES 2u // the one being walked and the one being built +#define X68K_ALL_PALETTES 0xFFFFu // a bit per palette +typedef char x68kAllPalettesFit[(SURFACE_PALETTE_COUNT == 16u) ? 1 : -1]; // One band as the raster interrupt reads it. x68kRasterIsr.s addresses the // fields by offset; the size checks below hold both sides to one layout. @@ -357,11 +369,17 @@ typedef struct { // Everything one frame of reloads reads. There are two: the interrupt walks // one while the main thread builds the other, and takes the new one up at the -// frame wrap, so rebuilding never disturbs the frame being scanned. +// frame wrap, so rebuilding never disturbs the frame being scanned. A table is +// rebuilt only as far as the stage changed since it was last built: the +// palettes whose bits are clear in currentPalettes, and the bands unless +// bandsCurrent. Both start clear, so a table's first build is a whole one. typedef struct { const X68kBandT *end; // one past the last band X68kBandT bands[X68K_MAX_BANDS]; uint16_t palettes[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; + uint16_t bandCount; + uint16_t currentPalettes; // bit p: palettes[p] is the stage's palette p + bool bandsCurrent; // bands[] follow the stage's SCB } X68kBandTableT; typedef char x68kBandPaletteAt0[(offsetof(X68kBandT, palette) == 0u) ? 1 : -1]; @@ -373,18 +391,35 @@ typedef char x68kTableBandsAt4[(offsetof(X68kBandTableT, bands) == 4u) ? 1 : -1] extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s +// The present's copy, src/x68000/x68kPresent.s. It carries the geometry as +// constants of its own; these hold each one to the C definition it mirrors. +extern void x68kPresentText(uint8_t *const *planes, const uint8_t *minWord, const uint8_t *maxWord, volatile uint8_t *text); +typedef char x68kPresentRows[(SURFACE_HEIGHT == 200u) ? 1 : -1]; +typedef char x68kPresentCleanMin[(STAGE_DIRTY_CLEAN_MIN == 0xFFu) ? 1 : -1]; +typedef char x68kPresentFullMax[(STAGE_DIRTY_FULL_MAX == 79u) ? 1 : -1]; +typedef char x68kPresentRowBytes[(X68K_BYTES_PER_ROW == 40u) ? 1 : -1]; +typedef char x68kPresentPlanes[(X68K_BITPLANES == 4) ? 1 : -1]; +typedef char x68kPresentTextStride[(X68K_TVRAM_STRIDE == 128u) ? 1 : -1]; +typedef char x68kPresentPlaneStep[(X68K_TVRAM_PLANE_STEP == 0x20000ul) ? 1 : -1]; +// It moves longs, so both ends of every band must be at even addresses: the +// plane buffers are malloc'd, and the stage's first byte in the text plane +// is this far into its row. +typedef char x68kPresentOriginEven[((X68K_ORIGIN_BYTE % 2u) == 0u) ? 1 : -1]; + // Shared with x68kRasterIsr.s, hence not static. Written by the main thread // only with every interrupt masked; the interrupt runs to completion. X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap const X68kBandT *volatile gX68kRasterCursor = NULL; // the next band to reload -static X68kBandTableT gBandTables[2]; +static X68kBandTableT gBandTables[X68K_BAND_TABLES]; static bool gRasterArmed = false; static uint32_t gRasterOldVec = 0ul; // $138 before we took it #endif // ----- Prototypes ----- +static inline uint16_t colorLookup(uint16_t rgb12); +static void colorLutBuild(void); static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps); static void installTickIsr(void); static void installVdispIsr(void); @@ -400,7 +435,7 @@ static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src); static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src); static void clearTextPlane(void); static void disarmRaster(void); -static void installScb(const jlSurfaceT *src); +static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChanged); #endif #if !X68K_PRESENT_TEXT static void buildSpreadTables(void); @@ -436,6 +471,29 @@ static inline void x68kInterruptsRestore(uint16_t sr) { } +// ----- Palette words ----------------------------------------------------------- + +static inline uint16_t colorLookup(uint16_t rgb12) { + return (uint16_t)(gColorLutRedGreen[(rgb12 >> X68K_LUT_RED_GREEN_SHIFT) & X68K_LUT_RED_GREEN_MASK] | gColorLutBlue[rgb12 & X68K_LUT_BLUE_MASK]); +} + + +// Each entry is x68kColorFromRgb12 of its channels alone, the others zero. The +// fields do not overlap, and each entry carries the shared intensity bit when +// its own channels are non-zero, so the OR has it exactly when the colour is +// not black -- the same word as converting the whole colour. +static void colorLutBuild(void) { + uint16_t i; + + for (i = 0u; i < X68K_LUT_RED_GREEN_ENTRIES; i++) { + gColorLutRedGreen[i] = x68kColorFromRgb12((uint16_t)(i << X68K_LUT_RED_GREEN_SHIFT)); + } + for (i = 0u; i < X68K_LUT_BLUE_ENTRIES; i++) { + gColorLutBlue[i] = x68kColorFromRgb12(i); + } +} + + // ----- Lifecycle ------------------------------------------------------------ bool jlpInit(const jlConfigT *config) { @@ -446,6 +504,7 @@ bool jlpInit(const jlConfigT *config) { // and stay there. _dos_super(0) returns the old USP, or a negative value // if we were already supervisor -- in which case do not switch back. gPrevSsp = _dos_super(0); + colorLutBuild(); gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing #if X68K_PRESENT_TEXT // 16 colours, because the present writes the four 1bpp TEXT planes directly @@ -766,6 +825,9 @@ static void vdispPoll(void) { // of the previous one is reached by staying in the interrupt, so a run of // per-line changes holds the interrupt, at IPL 6, for the whole run: about // one raster (31.8 us) per transition, and about two for a band it re-arms for. +// Interrupt level 5 -- the SCC: the RS-232C port and the mouse -- waits out +// the whole run, which bounds the runs a serial program can use (see "Serial +// under a dense SCB" in claudeDocs/x68000_port.md). // One band per RUN of equal SCB values, in the raster units the compare wants. @@ -805,16 +867,21 @@ static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src) { } -// Quantise every palette once into the form the interrupt stores. +// Quantise the palettes the table does not have current into the form the +// interrupt stores. static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src) { uint16_t pal; uint16_t col; for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { + if ((table->currentPalettes & (uint16_t)(1u << pal)) != 0u) { + continue; + } for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { - table->palettes[pal][col] = x68kColorFromRgb12(src->palette[pal][col]); + table->palettes[pal][col] = colorLookup(src->palette[pal][col]); } } + table->currentPalettes = X68K_ALL_PALETTES; } @@ -845,11 +912,22 @@ static void disarmRaster(void) { // being scanned finishes with the bands it started with: a fade or an SCB // change can lag the pixels by up to a frame but never tears one. A // single-band screen needs no interrupt: its palette is written here, once. -static void installScb(const jlSurfaceT *src) { +// paletteMask has bit p set for each stage palette changed since the last +// call, scbChanged whether the SCB did; both tables note what they now lack, +// and the one being built makes up only that. A palette fade converts one +// palette and keeps its bands, where rebuilding everything cost ~4.8 ms. +static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChanged) { X68kBandTableT *table; uint16_t sr; uint16_t col; + uint16_t t; + for (t = 0u; t < X68K_BAND_TABLES; t++) { + gBandTables[t].currentPalettes &= (uint16_t)~paletteMask; + if (scbChanged) { + gBandTables[t].bandsCurrent = false; + } + } // Withdraw any queued table first, so the interrupt cannot take up the one // about to be rewritten; after that the table it walks cannot change. sr = x68kInterruptsOff(); @@ -857,7 +935,11 @@ static void installScb(const jlSurfaceT *src) { table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0]; x68kInterruptsRestore(sr); buildScbPalettes(table, src); - if (buildScbBands(table, src) == 1u) { + if (!table->bandsCurrent) { + table->bandCount = buildScbBands(table, src); + table->bandsCurrent = true; + } + if (table->bandCount == 1u) { disarmRaster(); for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { X68K_TEXT_PALETTE[col] = table->bands[0].palette[col]; @@ -900,7 +982,7 @@ static void uploadGfxPalette(const jlSurfaceT *src) { for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { - X68K_GFX_PALETTE[(pal << 4) | col] = x68kColorFromRgb12(src->palette[pal][col]); + X68K_GFX_PALETTE[(pal << 4) | col] = colorLookup(src->palette[pal][col]); } } } @@ -1020,55 +1102,13 @@ static void clearTextPlane(void) { } -// Copy one row of one plane into the text plane. Both ends sit at the same -// offset from a 4-aligned base -- the stage plane's row stride is 40 and the -// text plane's is 128, both multiples of 4, and the centring offset -// X68K_ORIGIN_BYTE is 12 -- so once the leading bytes are stepped off, the rest -// moves a long at a time with no shifting. Inline rather than memcpy because a -// full present is 800 of these calls and the call alone was costing more than -// the bytes. -static inline __attribute__((always_inline)) -void x68kCopyRowToText(volatile uint8_t *dst, const uint8_t *srcRow, uint16_t count) { - while (count != 0u && (((uintptr_t)dst & 3u) != 0u)) { - *dst++ = *srcRow++; - count--; - } - while (count >= 4u) { - *(volatile uint32_t *)dst = *(const uint32_t *)srcRow; - dst += 4; - srcRow += 4; - count -= 4u; - } - while (count != 0u) { - *dst++ = *srcRow++; - count--; - } -} - - // Present to the TEXT plane. The stage and the text plane are both four 1bpp // planes, so this is a copy -- no pixel conversion, no table, 0.5 bytes of bus // traffic per pixel. The SCB is honoured by the raster interrupt above rather -// than by a per-pixel palette index. +// than by a per-pixel palette index. The copy, dirty-band walk included, is +// x68kPresentText (src/x68000/x68kPresent.s), whose header has the numbers. void jlpPresent(const jlSurfaceT *src) { - X68kPlanarT *pd; - const uint8_t *sp; - uint16_t g; - uint16_t y; - uint16_t yEnd; - uint16_t plane; - uint16_t firstByte; - uint16_t rowBytes; - - // Four rows of dirty band read as ONE long. A clean row's minWord is the - // 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so - // four clean rows read exactly 0xFFFFFFFF and the whole group skips. This - // is the trick src/dos/hal.c already used and this port was missing: - // visiting all two hundred rows one at a time cost roughly 0.9 ms of EVERY - // present, dirty or not. A failing group still runs the per-row clean test, - // so the rows written are byte-identical to the plain walk. - typedef uint32_t __attribute__((may_alias)) BandGroupT; - const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord; + X68kPlanarT *pd; if (src == NULL) { return; @@ -1079,38 +1119,15 @@ void jlpPresent(const jlSurfaceT *src) { return; } // gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume - // both here the way src/iigs/hal.c does. Either one rebuilds the bands, - // because every band table carries its own copy of the palettes. + // both here the way src/iigs/hal.c does. Either one updates the band + // tables, which carry their own copies of the palettes. if (gStagePaletteDirty || gStageScbDirty) { - installScb(src); - gStagePaletteDirty = false; - gStageScbDirty = false; - } - for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) { - if (minGroups[g] == 0xFFFFFFFFul) { - continue; - } - yEnd = (uint16_t)(g * 4u + 4u); - for (y = (uint16_t)(g * 4u); y < yEnd; y++) { - if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { - continue; - } - // Bands are 16-bit stage words = 4 pixels = 2 bytes of a plane. - firstByte = (uint16_t)(gStageMinWord[y] >> 1); - rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); - if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) { - rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte); - } - for (plane = 0u; plane < X68K_BITPLANES; plane++) { - sp = pd->planes[plane] + ((uint16_t)y * X68K_BYTES_PER_ROW) + firstByte; - x68kCopyRowToText((volatile uint8_t *)(X68K_TVRAM_PLANE0 - + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP) - + x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y)) - + X68K_ORIGIN_BYTE + firstByte), - sp, rowBytes); - } - } + installScb(src, gStagePaletteDirtyMask, gStageScbDirty); + gStagePaletteDirty = false; + gStagePaletteDirtyMask = 0u; + gStageScbDirty = false; } + x68kPresentText(pd->planes, gStageMinWord, gStageMaxWord, (volatile uint8_t *)(X68K_TVRAM_PLANE0 + x68kTvramOffset(X68K_ORIGIN_X, X68K_ORIGIN_Y))); } #else void jlpPresent(const jlSurfaceT *src) { diff --git a/src/x68000/x68kPlanar.h b/src/x68000/x68kPlanar.h index beca34f..8155a16 100644 --- a/src/x68000/x68kPlanar.h +++ b/src/x68000/x68kPlanar.h @@ -1,11 +1,5 @@ // Sharp X68000 per-surface planar storage. // -// Step 1 of the storage-model conversion (see claudeDocs/x68000_port.md). Nothing -// includes this yet -- the port still ships the chunky-plus-expand path, which -// is hash-verified against the Apple IIgs golden reference. This header lands -// first so the conversion can proceed in verifiable increments instead of one -// unlandable change. -// // WHY THIS IS ALMOST amigaPlanar.h // ------------------------------- // A 320px-wide 4bpp surface is 40 bytes per row per plane on ANY planar @@ -24,13 +18,13 @@ // plane 2 $E40000 plane 3 $E60000 // pixel(x,y) -> plane + y*128 + x/8, bit 7-(x&7), plane 0 = LSB // -// so jlpPresent scatters the compact 40-byte rows into that 128-byte stride. -// That is the ONLY place the 128 appears, and it is where the win comes from: -// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000 -// bytes of GVRAM word writes on the chunky path. Bus traffic is what the -// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see -// claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher -// per-access cost by roughly 2x overall. +// so the present (src/x68000/x68kPresent.s) scatters the compact 40-byte rows +// into that 128-byte stride. That is the ONLY place the 128 matters, and it is +// where the win comes from: 4 planes x 200 rows x 40 bytes = 32,000 bytes per +// frame, against 128,000 bytes of GVRAM word writes on the chunky path. Bus +// traffic is what the measured wait states punish (TVRAM 1.92 cycles/access, +// GVRAM 1.03 -- see claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic +// cut beats TVRAM's higher per-access cost by roughly 2x overall. // // Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc, // because nothing but the CPU ever reads them. @@ -107,15 +101,9 @@ static inline X68kPlanarT *x68kSurfacePlanar(const jlSurfaceT *s) { } -// Byte offset of pixel (x,y) within one DISPLAY plane, and the bit within it. -// Split out so the present loop and any future asm agree on one definition. +// Byte offset of pixel (x,y) within one DISPLAY plane (x a multiple of 8). static inline uint32_t x68kTvramOffset(uint16_t x, uint16_t y) { return ((uint32_t)y * X68K_TVRAM_STRIDE) + (uint32_t)(x >> 3); } - -static inline uint8_t x68kTvramBit(uint16_t x) { - return (uint8_t)(0x80u >> (x & 7u)); -} - #endif diff --git a/src/x68000/x68kPresent.s b/src/x68000/x68kPresent.s new file mode 100644 index 0000000..4c7f9f0 --- /dev/null +++ b/src/x68000/x68kPresent.s @@ -0,0 +1,234 @@ +| Sharp X68000 present: copy the stage's dirty rows into the text plane. +| +| The stage is four planes of 40 bytes a row and the text plane is four planes +| of 128 bytes a row, 128 KB apart, so a present is a copy with a stride +| change and nothing else. This routine is the whole of jlpPresent's copy: it +| walks the dirty bands and copies every dirty row of all four planes, row by +| row, so each row is whole in every plane before the next one starts. +| +| WHY ASM. The C loop it replaces cost 73 cycles a long where the copy itself +| is 24, measured under MAME on a full-stage present (58.7 ms for 32,000 +| bytes): ~256 cycles of pointer and alignment set-up per plane-row and ~560 +| per row, around ten longs of real work. Here a row's four copies are +| straight runs of move.l (An)+,(An)+ entered part-way, so the only per-row +| cost left is the band test and the jumps between the four runs. +| +| BANDS. gStageMinWord / gStageMaxWord hold each row's first and last dirty +| 16-bit stage word (4 pixels); a clean row is min 0xFF, max 0x00. A band is +| copied in whole LONGS of each plane (8 words), so it can come out up to six +| bytes wider than it was marked: those bytes are the surface's own and match +| what is already on screen, and a band in whole longs never needs a byte or +| word move. Its ends are even on both sides (the surface row is 40 bytes, the +| text row 128, the centring offset 12), which is all a 68000 long move asks. +| A max past the last word of the row (STAGE_DIRTY_FULL_MAX) is clamped to +| it, as jlpPresent's C did. +| +| RUNS. Consecutive rows with the same raw band are a run: once the cursors +| are set up for its first row, each copy leaves them at the band's start on +| the next row, so the next row of the run is only a compare and a jsr. A row +| with another band sets them up again from its row number (rowStart); a +| clean row or a skipped group breaks the run. +| +| GROUPS. The min-word bytes of four rows read as one long; four clean rows +| are 0xFFFFFFFF and are passed over with one compare, so a present with +| nothing dirty costs ~30 cycles per four rows. +| +| void x68kPresentText(uint8_t *const *planes, const uint8_t *minWord, +| const uint8_t *maxWord, volatile uint8_t *text); +| +| planes the stage's four plane buffers (X68kPlanarT.planes) +| minWord gStageMinWord, aligned(2) so its groups read as longs +| maxWord gStageMaxWord +| text plane 0 of the text plane at stage row 0, byte 0 +| +| The geometry below is the C's (src/x68000/hal.c holds each value against +| its C definition with a compile-time check). cdecl, elf2x68k: no leading +| underscore, d2-d7/a2-a6 callee-save. +| +| Registers across the walk: +| a0 = text-plane cursor a1-a4 = plane 0-3 cursors +| a5 = the copy's entry a6 = min-word walker +| d2 = plane row -> next row's band d3 = text plane p -> p + 1 +| d4 = text plane 3 -> next row's plane 0 +| d5 = the run's raw band, (min << 16) | max, or -1 for none +| d6 = groups left - 1 d7 = maxWord - minWord + + .equ ROWS, 200 | SURFACE_HEIGHT + .equ GROUP_ROWS, 4 | min-word bytes in a long + .equ GROUPS, ROWS / GROUP_ROWS + .equ CLEAN_GROUP, -1 | four STAGE_DIRTY_CLEAN_MIN bytes + .equ FULL_MAX, 79 | STAGE_DIRTY_FULL_MAX: the row's last word + .equ WORD_TO_LONG, 3 | 8 stage words (32 px) make a plane long + .equ LONG_TO_BYTE, 2 + .equ ROW_BYTES, 40 | X68K_BYTES_PER_ROW + .equ ROW_LONGS, ROW_BYTES / 4 + .equ PLANES, 4 | X68K_BITPLANES + .equ TEXT_STRIDE, 128 | X68K_TVRAM_STRIDE + .equ TEXT_ROW_SHIFT, 7 | log2(TEXT_STRIDE) + .equ PLANE_STEP, 0x20000 | X68K_TVRAM_PLANE_STEP + .equ MOVE_BYTES, 2 | move.l (An)+,(An)+ +| One plane's copy: the moves, adda.w + adda.l (2 bytes each), jmp d16(An). + .equ PLANE_CODE, ROW_LONGS * MOVE_BYTES + 8 +| Arguments, above the eleven saved registers and the return address. + .equ ARG_PLANES, 48 + .equ ARG_MIN, 52 + .equ ARG_MAX, 56 + .equ ARG_TEXT, 60 + .equ BSR_FRAME, 4 | rowStart runs under a bsr + + .ifne TEXT_STRIDE - (1 << TEXT_ROW_SHIFT) + .error "TEXT_ROW_SHIFT must be log2(TEXT_STRIDE)" + .endif + .ifne ROWS - GROUPS * GROUP_ROWS + .error "the rows must be whole groups" + .endif + + .text + .even + .globl x68kPresentText + + +| ---- PRESENT_ROW: one row of a dirty group ---- +| a6 is at the row's min word and steps past it. The row's raw band is +| compared with the run's; a match means the cursors are already on this row's +| band. Otherwise rowStart sets them up, or points a5 at a bare rts for a +| clean row. + .macro PRESENT_ROW + moveq #0,%d0 + move.b (%a6)+,%d0 + swap %d0 + move.b -1(%a6,%d7.l),%d0 | (min << 16) | max + cmp.l %d0,%d5 + beq.s 1f + bsr rowStart +1: jsr (%a5) + .endm + + +x68kPresentText: + movem.l %d2-%d7/%a2-%a6,-(%sp) + movea.l ARG_MIN(%sp),%a6 + move.l ARG_MAX(%sp),%d7 + sub.l %a6,%d7 + moveq #-1,%d5 | no run yet + move.w #GROUPS - 1,%d6 +groupScan: + cmpi.l #CLEAN_GROUP,(%a6)+ + dbne %d6,groupScan + beq.s presentDone | the last group was clean too +groupRows: + subq.l #GROUP_ROWS,%a6 + PRESENT_ROW + PRESENT_ROW + PRESENT_ROW + PRESENT_ROW + dbra %d6,groupNext + bra.s presentDone +groupNext: + cmpi.l #CLEAN_GROUP,(%a6)+ + bne.s groupRows | still in the run + moveq #-1,%d5 | rows skipped: the run is broken + dbra %d6,groupScan +presentDone: + movem.l (%sp)+,%d2-%d7/%a2-%a6 + rts + + +| ---- rowStart: set the cursors up for a row outside the run ---- +| d0 = the row's raw band, (min << 16) | max; a6 is one past its min word. +| A dirty row becomes the run (d5) and gets its cursors, skips and entry; a +| clean one leaves no run and a5 on copyNone. +rowStart: + move.l %d0,%d5 + move.w %d0,%d1 | max + swap %d0 | min + cmpi.w #FULL_MAX,%d1 + bls.s 1f + moveq #FULL_MAX,%d1 | never past the row +1: cmp.w %d0,%d1 + bcs.s rowClean | min > max + lsr.w #WORD_TO_LONG,%d0 | first long + lsr.w #WORD_TO_LONG,%d1 + sub.w %d0,%d1 | longs - 1 + add.w %d1,%d1 + lea copyPlane0 + (ROW_LONGS - 1) * MOVE_BYTES(%pc),%a5 + suba.w %d1,%a5 | enter with that many moves to go + add.w %d1,%d1 + addq.w #4,%d1 + ext.l %d1 | the band's bytes in one plane row + lsl.w #LONG_TO_BYTE,%d0 | its first byte + move.l %a6,%d2 + sub.l ARG_MIN + BSR_FRAME(%sp),%d2 + subq.w #1,%d2 | the row + move.w %d2,%d3 + lsl.w #TEXT_ROW_SHIFT,%d3 + add.w %d0,%d3 + movea.l ARG_TEXT + BSR_FRAME(%sp),%a0 + adda.w %d3,%a0 + mulu.w #ROW_BYTES,%d2 + add.w %d0,%d2 + movea.l ARG_PLANES + BSR_FRAME(%sp),%a4 + movea.l (%a4)+,%a1 + movea.l (%a4)+,%a2 + movea.l (%a4)+,%a3 + movea.l (%a4),%a4 + adda.w %d2,%a1 + adda.w %d2,%a2 + adda.w %d2,%a3 + adda.w %d2,%a4 + moveq #ROW_BYTES,%d2 + sub.w %d1,%d2 + move.l #PLANE_STEP,%d3 + sub.l %d1,%d3 + move.l #TEXT_STRIDE - (PLANES - 1) * PLANE_STEP,%d4 + sub.l %d1,%d4 + rts +rowClean: + moveq #-1,%d5 + lea copyNone(%pc),%a5 + rts + + +| ---- The copy: one row of all four planes ---- +| Entered through a5 somewhere in plane 0's moves, with as many moves left as +| the band has longs; each plane's jump lands at the same point of the next +| plane's moves. Leaves every cursor at the band's start on the next row. +copyPlane0: + .rept ROW_LONGS + move.l (%a1)+,(%a0)+ + .endr + adda.w %d2,%a1 + adda.l %d3,%a0 + jmp PLANE_CODE(%a5) +copyPlane1: + .rept ROW_LONGS + move.l (%a2)+,(%a0)+ + .endr + adda.w %d2,%a2 + adda.l %d3,%a0 + jmp (2 * PLANE_CODE)(%a5) +copyPlane2: + .rept ROW_LONGS + move.l (%a3)+,(%a0)+ + .endr + adda.w %d2,%a3 + adda.l %d3,%a0 + jmp (3 * PLANE_CODE)(%a5) +copyPlane3: + .rept ROW_LONGS + move.l (%a4)+,(%a0)+ + .endr + adda.w %d2,%a4 + adda.l %d4,%a0 +copyNone: + rts + + .ifne (copyPlane1 - copyPlane0) - PLANE_CODE + .error "PLANE_CODE is not the size of one plane's copy" + .endif + .ifne (copyPlane2 - copyPlane1) - PLANE_CODE + .error "the plane copies must be the same size" + .endif + .ifne (copyPlane3 - copyPlane2) - PLANE_CODE + .error "the plane copies must be the same size" + .endif diff --git a/src/x68000/x68kSccRxIsr.s b/src/x68000/x68kSccRxIsr.s index ef5f452..6ca71d6 100644 --- a/src/x68000/x68kSccRxIsr.s +++ b/src/x68000/x68kSccRxIsr.s @@ -13,8 +13,28 @@ | return with RTE. Unlike the tick hook there is NO chaining -- the byte is | consumed here, so running RSDRV's handler afterwards would be wrong. | +| ONE INTERRUPT, EVERY WAITING BYTE. Whatever holds interrupt level 5 off -- +| the SCB raster interrupt staying through a run of per-line palette changes, +| the IOCS Timer C dispatcher -- lets bytes pile up in the chip's FIFO, and the +| handler reads until RR0 says the FIFO is empty rather than one byte per +| interrupt. On the chip that saves an exception per byte after such a hold. +| Under MAME 0.264 it is what keeps the channel working at all: its z80scc +| clears a source's pending bit when the CPU acknowledges it and sets it again +| only when a NEW byte arrives, so a byte left behind by a one-byte handler +| waits for the next one and the channel runs a byte behind, one byte from +| overrun, for good (measured by verify-x68000-serial-mouse.sh's dense-SCB +| variant: after a hold, 1 byte was left in the FIFO after 93% of reads). +| +| RR0 is read without a select: the register pointer is 0 whenever this can +| run (see jlpSerialWrite in serial.c). RR1 needs one, so its select and read +| are made with every interrupt masked -- the Timer C dispatcher, a level 6 +| interrupt that runs the IOCS mouse solicitation's own select-then-write on +| channel B, must not come between them. A byte with an error stops the loop: +| the chip holds it at the head of the FIFO for the special-condition handler +| below, which drops it. +| | Ring protocol (single consumer, single producer): the mainline reads gX68kRxHead, -| this ISR writes gX68kRxTail; the byte is stored BEFORE the tail moves, and the +| this ISR writes gX68kRxTail; each byte is stored BEFORE the tail moves, and the | 68000 is strictly in-order, so no further interlock is needed. A full ring | drops the byte -- the frame CRC upstream turns that into a clean retransmit-less | drop rather than corruption. @@ -24,6 +44,16 @@ .equ SCC_A_CTRL, 0xE98005 | channel A command/status .equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer) .equ RING_MASK, 4095 | X68K_RX_RING_CAP - 1 (power of two) + .equ RR0_RX_AVAILABLE, 0 | RR0 bit: a byte is waiting + .equ SCC_RR1, 1 | the register the select names + .equ RR1_ERRORS, 0x70 | framing, overrun, parity + .equ WR0_RESET_IUS, 0x38 | reset highest interrupt under service + .equ WR0_ERROR_RESET, 0x30 + .equ SR_MASK_ALL, 0x0700 +| Bytes one interrupt reads at most: the chip's three-byte FIFO and one more +| that can complete while it is being read. The bound keeps a stream faster +| than the loop from holding level 5 indefinitely. + .equ DRAIN_MAX, 4 .text .even @@ -32,19 +62,33 @@ | Channel A "receive character available". x68kSccRxIsr: - movem.l %d0-%d2/%a0,-(%sp) - move.b SCC_A_DATA,%d0 | the received byte + movem.l %d0-%d3/%a0,-(%sp) + lea gX68kRxRing,%a0 move.w gX68kRxTail,%d1 + moveq #DRAIN_MAX - 1,%d3 +rxByte: + move.b SCC_A_DATA,%d0 | the received byte move.w %d1,%d2 addq.w #1,%d2 andi.w #RING_MASK,%d2 | next tail cmp.w gX68kRxHead,%d2 beq.s rxFull | ring full: drop, CRC upstream recovers - lea gX68kRxRing,%a0 move.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail - move.w %d2,gX68kRxTail -rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS - movem.l (%sp)+,%d0-%d2/%a0 + move.w %d2,%d1 + move.w %d1,gX68kRxTail +rxFull: + btst #RR0_RX_AVAILABLE,SCC_A_CTRL + beq.s rxDone | the FIFO is empty + move.w %sr,%d0 + ori.w #SR_MASK_ALL,%sr + move.b #SCC_RR1,SCC_A_CTRL + move.b SCC_A_CTRL,%d2 + move.w %d0,%sr + andi.b #RR1_ERRORS,%d2 + dbne %d3,rxByte | the next byte, unless it is in error +rxDone: + move.b #WR0_RESET_IUS,SCC_A_CTRL + movem.l (%sp)+,%d0-%d3/%a0 rte | Channel A "special receive condition" (overrun / framing error). The byte is @@ -53,7 +97,7 @@ rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS x68kSccSpIsr: move.l %d0,-(%sp) move.b SCC_A_DATA,%d0 | unlatch the offending byte - move.b #0x30,SCC_A_CTRL | WR0: error reset - move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS + move.b #WR0_ERROR_RESET,SCC_A_CTRL + move.b #WR0_RESET_IUS,SCC_A_CTRL move.l (%sp)+,%d0 rte diff --git a/tests/host/dirtyMarkHost.c b/tests/host/dirtyMarkHost.c new file mode 100644 index 0000000..8a3f3dd --- /dev/null +++ b/tests/host/dirtyMarkHost.c @@ -0,0 +1,246 @@ +// dirtyMarkHost.c - randomized check of the stage's dirty-band writers +// (src/core/surface.c, src/m68k/stageBands.s) against the plain per-row +// widen they must match byte for byte. +// +// surfaceMarkDirtyRows takes rows four at a time and reuses a group's result +// for the next group that held the same bytes, so its answer depends on what +// the rows held: a mark of fresh rows, of rows an earlier rect marked, and of +// rows every earlier mark left different all take different paths. Each +// scenario seeds the bands one of several ways -- clean, per-row random, runs +// of equal bands, every row full -- then applies a few random marks of every +// height and phase, comparing all 400 band bytes with a reference after each. +// Also checks the whole-stage fills (stageDirtyClearAll, surfaceMarkDirtyAll). +// +// Built for the host by scripts/check-dirtymark.sh (the C marker), and for the +// Atari ST by scripts/verify-atarist-dirtymark.sh, which runs it under Hatari +// against the 68000 asm the Amiga and X68000 share. -DDIRTY_MARK_SCENARIOS +// sets the count and -DDIRTY_MARK_LOG a file to write the result line to. +// Exits nonzero on any mismatch. + +#include +#include +#include +#include + +#include "surfaceInternal.h" + +#ifndef DIRTY_MARK_SCENARIOS +#define DIRTY_MARK_SCENARIOS 200000ul +#endif + +#define MARKS_PER_SCENARIO 8u +#define RNG_SEED 0x2545F491ul +#define SEED_KINDS 4u +#define SEED_CLEAN 0u +#define SEED_RANDOM_ROWS 1u +#define SEED_RUNS 2u +#define SEED_FULL 3u +#define RUN_ROWS_MAX 24u +#define CLEAN_ONE_IN 3u +#define HEIGHT_CLASSES 3u +#define HEIGHT_SMALL_MAX 12u +#define HEIGHT_MEDIUM_MAX 64u +#define FAILURES_SHOWN 5u + +static void bandRandom(uint8_t *minWord, uint8_t *maxWord); +static bool bandsMatch(const char *what); +static uint16_t heightRandom(uint16_t y); +static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); +static uint32_t rngNext(void); +static uint16_t rngBelow(uint16_t n); +static void seedBands(uint16_t kind); +static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord); + +static uint32_t gRng = RNG_SEED; +static uint8_t gRefMin[SURFACE_HEIGHT]; +static uint8_t gRefMax[SURFACE_HEIGHT]; +static uint32_t gChecks = 0ul; +static uint32_t gFailures = 0ul; + + +// A random real band: first and last word anywhere on the row, first <= last. +static void bandRandom(uint8_t *minWord, uint8_t *maxWord) { + uint8_t a; + uint8_t b; + + a = (uint8_t)rngBelow(SURFACE_WORDS_PER_ROW); + b = (uint8_t)rngBelow(SURFACE_WORDS_PER_ROW); + *minWord = (a < b) ? a : b; + *maxWord = (a < b) ? b : a; +} + + +// Compare the stage's bands with the reference; report the first few misses. +static bool bandsMatch(const char *what) { + uint16_t y; + + gChecks++; + if (memcmp(gStageMinWord, gRefMin, SURFACE_HEIGHT) == 0 && memcmp(gStageMaxWord, gRefMax, SURFACE_HEIGHT) == 0) { + return true; + } + gFailures++; + if (gFailures <= FAILURES_SHOWN) { + for (y = 0u; y < SURFACE_HEIGHT; y++) { + if (gStageMinWord[y] != gRefMin[y] || gStageMaxWord[y] != gRefMax[y]) { + printf("dirtyMark: FAIL %s: row %u is [%u,%u], expected [%u,%u]\n", what, (unsigned)y, (unsigned)gStageMinWord[y], (unsigned)gStageMaxWord[y], (unsigned)gRefMin[y], (unsigned)gRefMax[y]); + break; + } + } + } + return false; +} + + +// A mark height for rows from y: short, medium and tall about equally often, +// clipped to the stage. +static uint16_t heightRandom(uint16_t y) { + uint16_t room; + uint16_t h; + + room = (uint16_t)(SURFACE_HEIGHT - y); + switch (rngBelow(HEIGHT_CLASSES)) { + case 0u: + h = (uint16_t)(1u + rngBelow(HEIGHT_SMALL_MAX)); + break; + case 1u: + h = (uint16_t)(1u + rngBelow(HEIGHT_MEDIUM_MAX)); + break; + default: + h = (uint16_t)(1u + rngBelow(SURFACE_HEIGHT)); + break; + } + return (h < room) ? h : room; +} + + +// The widen every marker must match: each row, one at a time. +static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { + for (; y < yEnd; y++) { + if (minWord < gRefMin[y]) { + gRefMin[y] = minWord; + } + if (maxWord > gRefMax[y]) { + gRefMax[y] = maxWord; + } + } +} + + +static uint32_t rngNext(void) { + gRng ^= gRng << 13; + gRng ^= gRng >> 17; + gRng ^= gRng << 5; + return gRng; +} + + +static uint16_t rngBelow(uint16_t n) { + return (uint16_t)(rngNext() % n); +} + + +// Put both the stage's bands and the reference in one of the seed states. +static void seedBands(uint16_t kind) { + uint16_t y; + uint16_t run; + uint8_t minWord; + uint8_t maxWord; + + y = 0u; + while (y < SURFACE_HEIGHT) { + switch (kind) { + case SEED_CLEAN: + run = SURFACE_HEIGHT; + minWord = STAGE_DIRTY_CLEAN_MIN; + maxWord = STAGE_DIRTY_CLEAN_MAX; + break; + case SEED_FULL: + run = SURFACE_HEIGHT; + minWord = 0u; + maxWord = STAGE_DIRTY_FULL_MAX; + break; + case SEED_RUNS: + run = (uint16_t)(1u + rngBelow(RUN_ROWS_MAX)); + if (rngBelow(CLEAN_ONE_IN) == 0u) { + minWord = STAGE_DIRTY_CLEAN_MIN; + maxWord = STAGE_DIRTY_CLEAN_MAX; + } else { + bandRandom(&minWord, &maxWord); + } + break; + default: + run = 1u; + if (rngBelow(CLEAN_ONE_IN) == 0u) { + minWord = STAGE_DIRTY_CLEAN_MIN; + maxWord = STAGE_DIRTY_CLEAN_MAX; + } else { + bandRandom(&minWord, &maxWord); + } + break; + } + for (; run != 0u && y < SURFACE_HEIGHT; run--, y++) { + setRow(y, minWord, maxWord); + } + } +} + + +static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord) { + gStageMinWord[y] = minWord; + gStageMaxWord[y] = maxWord; + gRefMin[y] = minWord; + gRefMax[y] = maxWord; +} + + +int main(void) { + uint32_t scenario; + uint16_t mark; + uint16_t y; + uint16_t h; + uint8_t minWord; + uint8_t maxWord; +#ifdef DIRTY_MARK_LOG + FILE *log; +#endif + + // The whole-stage fills. (gStage is NULL here, which is the stage.) + seedBands(SEED_RANDOM_ROWS); + stageDirtyClearAll(); + memset(gRefMin, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT); + memset(gRefMax, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT); + bandsMatch("stageDirtyClearAll"); + seedBands(SEED_RANDOM_ROWS); + surfaceMarkDirtyAll(gStage); + memset(gRefMin, 0, SURFACE_HEIGHT); + memset(gRefMax, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT); + bandsMatch("surfaceMarkDirtyAll"); + + for (scenario = 0ul; scenario < DIRTY_MARK_SCENARIOS; scenario++) { + seedBands((uint16_t)(scenario % SEED_KINDS)); + for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) { + y = rngBelow(SURFACE_HEIGHT); + h = heightRandom(y); + bandRandom(&minWord, &maxWord); + surfaceMarkDirtyRows(y, (uint16_t)(y + h), minWord, maxWord); + refMark(y, (uint16_t)(y + h), minWord, maxWord); + if (!bandsMatch("surfaceMarkDirtyRows")) { + if (gFailures <= FAILURES_SHOWN) { + printf("dirtyMark: scenario %lu mark %u: rows %u..%u [%u,%u]\n", (unsigned long)scenario, (unsigned)mark, (unsigned)y, (unsigned)(y + h - 1u), (unsigned)minWord, (unsigned)maxWord); + } + memcpy(gStageMinWord, gRefMin, SURFACE_HEIGHT); + memcpy(gStageMaxWord, gRefMax, SURFACE_HEIGHT); + } + } + } + + printf("dirtyMark: %lu passed, %lu failed\n", (unsigned long)(gChecks - gFailures), (unsigned long)gFailures); +#ifdef DIRTY_MARK_LOG + log = fopen(DIRTY_MARK_LOG, "w"); + if (log != NULL) { + fprintf(log, "dirtyMark: %lu passed, %lu failed\n", (unsigned long)(gChecks - gFailures), (unsigned long)gFailures); + fclose(log); + } +#endif + return (gFailures == 0ul) ? 0 : 1; +} diff --git a/tests/host/fileRootHost.c b/tests/host/fileRootHost.c new file mode 100644 index 0000000..6926f73 --- /dev/null +++ b/tests/host/fileRootHost.c @@ -0,0 +1,371 @@ +// fileRootHost.c - Host-cc gate for the run-time file roots +// (src/core/fileRoot.h, JL_HAS_FILE_ROOTS): where jlDataOpen reads DATA/, +// and where the save calls and the log write SAVES/ and joeylog.txt. +// +// Built by scripts/check-fileroots.sh as a whole BLANK-port program (core + +// generics + src/blank/blank.c + this file) twice, each with the POSIX save +// HAL below (-DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE, as check-save.sh +// does): +// roots -DJL_HAS_FILE_ROOTS: this file is the port, its jlpReadRoot and +// jlpWriteRoot the folders argv[1] and argv[2] (each ending in '/'), +// the write root made the first time it is asked for, as a port +// makes it. DATA/ must be read under the read root, SAVES/ and +// joeylog.txt written under the write root and never in the +// current directory (which holds a decoy DATA/); jlLogClose must +// close the log and the next line append to it; reading data must +// not ask for the write root; an empty root must mean the current +// directory, the layout without the flag. +// plain without the flag, as every other port builds the core: DATA/, +// SAVES/ and joeylog.txt relative to the current directory. +// Both check that a path too long for the core's buffers fails cleanly and +// leaves no file behind. Each build runs in an empty current directory of +// its own, and writes its fixtures itself. Prints one line per check and a +// final tally; exits nonzero on any failure. + +#include +#include +#include +#include +#include + +#include "joey/joey.h" +#include "joey/file.h" +#include "port.h" +#include "fileRoot.h" + +// ----- Constants ----- + +#define DATA_DIR "DATA" +#define SAVES_DIR "SAVES" +#define LOG_NAME "joeylog.txt" +#define PATH_SEPARATOR "/" +#define DIR_MODE 0777 + +#define PROBE_NAME "probe.dat" +#define MISSING_NAME "missing.dat" +#define SAVE_NAME "slot.sav" +#define STREAM_NAME "stream.sav" + +// The two copies of PROBE_NAME: the current directory's is a decoy while +// the roots are set. +#define CWD_TEXT "the current directory's DATA" +#define ROOT_TEXT "the read root's DATA" +#define SAVE_TEXT "a save file" +#define STREAM_TEXT "a streamed save file" +#define LOG_FIRST "the first log line" +#define LOG_SECOND "the log line after jlLogClose" +#define LOG_THIRD "the log line after the log moved away" +#define LOG_MOVED_SUFFIX ".moved" +#define LOG_EMPTY_ROOT "the log line with an empty root" + +// Longer than every path buffer in the core (256 bytes). +#define LONG_NAME_CHARS 300 +#define LONG_NAME_CHAR 'x' + +#define PATH_BYTES 1024 +#define FILE_BYTES 256 + +#define ARG_READ_ROOT 1 +#define ARG_WRITE_ROOT 2 +#define ROOTS_ARGC 3 + +// ----- Prototypes ----- + +static void check(const char *name, bool ok); +static bool dataReads(const char *text); +static int dirEntries(const char *dir); +static bool dirMake(const char *dir); +static bool fileHolds(const char *path, const char *text); +static bool fileWrite(const char *path, const char *text); +static bool fixtureWrite(const char *root, const char *text); +static bool pathExists(const char *path); +static void pathJoin(char *out, const char *root, const char *rel, const char *name); +static void testLog(const char *root, bool reopen); +static void testLongName(const char *root); +static void testSaves(const char *root); + +// ----- Module state ----- + +static int gPass = 0; +static int gFail = 0; + +#if defined(JL_HAS_FILE_ROOTS) +static const char *gReadRoot = ""; +static const char *gWriteRoot = ""; +static bool gWriteRootMade = false; +static int gWriteRootAsks = 0; +#endif + +// ----- The port (POSIX save HAL; the roots under JL_HAS_FILE_ROOTS) ----- + +#if defined(JL_HAS_FILE_ROOTS) +const char *jlpReadRoot(void) { + return gReadRoot; +} +#endif + + +bool jlpSaveDelete(const char *path) { + return remove(path) == 0; +} + + +bool jlpSaveDirEnsure(const char *dir) { + return dirMake(dir); +} + + +#if defined(JL_HAS_FILE_ROOTS) +// Made on its first use, as a port makes its settings folder. +const char *jlpWriteRoot(void) { + gWriteRootAsks++; + if (!gWriteRootMade) { + gWriteRootMade = gWriteRoot[0] == '\0' || dirMake(gWriteRoot); + } + return gWriteRoot; +} +#endif + +// ----- Helpers (alphabetical) ----- + +static void check(const char *name, bool ok) { + if (ok) { + gPass++; + printf(" ok %s\n", name); + } else { + gFail++; + printf(" FAIL %s\n", name); + } +} + + +// jlDataOpen finds PROBE_NAME, and it holds text. +static bool dataReads(const char *text) { + char buf[FILE_BYTES]; + size_t got; + FILE *fp; + + fp = jlDataOpen(PROBE_NAME, "rb"); + if (fp == NULL) { + return false; + } + got = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[got] = '\0'; + return strcmp(buf, text) == 0; +} + + +// The entries in dir, . and .. aside; -1 when it cannot be read. +static int dirEntries(const char *dir) { + DIR *d; + struct dirent *e; + int count; + + d = opendir(dir); + if (d == NULL) { + return -1; + } + count = 0; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) { + count++; + } + } + closedir(d); + return count; +} + + +static bool dirMake(const char *dir) { + struct stat st; + + if (mkdir(dir, DIR_MODE) == 0) { + return true; + } + return errno == EEXIST && stat(dir, &st) == 0 && S_ISDIR(st.st_mode); +} + + +// The file at path holds exactly text. +static bool fileHolds(const char *path, const char *text) { + char buf[FILE_BYTES]; + size_t got; + FILE *fp; + + fp = fopen(path, "rb"); + if (fp == NULL) { + return false; + } + got = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + buf[got] = '\0'; + return strcmp(buf, text) == 0; +} + + +static bool fileWrite(const char *path, const char *text) { + FILE *fp; + bool ok; + + fp = fopen(path, "wb"); + if (fp == NULL) { + return false; + } + ok = fputs(text, fp) >= 0; + return fclose(fp) == 0 && ok; +} + + +// root + DATA/PROBE_NAME holding text, the folders made first. +static bool fixtureWrite(const char *root, const char *text) { + char path[PATH_BYTES]; + + if (root[0] != '\0' && !dirMake(root)) { + return false; + } + pathJoin(path, root, DATA_DIR, ""); + if (!dirMake(path)) { + return false; + } + pathJoin(path, root, DATA_DIR PATH_SEPARATOR, PROBE_NAME); + return fileWrite(path, text); +} + + +static bool pathExists(const char *path) { + struct stat st; + + return stat(path, &st) == 0; +} + + +static void pathJoin(char *out, const char *root, const char *rel, const char *name) { + (void)snprintf(out, PATH_BYTES, "%s%s%s", root, rel, name); +} + + +// jlLog writes root + joeylog.txt. With reopen: after jlLogClose the next +// line is appended to it; and jlLogClose really closes it: with the file +// moved away, the next line starts a new one. +static void testLog(const char *root, bool reopen) { + char path[PATH_BYTES]; +#if defined(JL_HAS_FILE_ROOTS) + char moved[PATH_BYTES]; +#endif + + pathJoin(path, root, LOG_NAME, ""); + jlLog(LOG_FIRST); + check("the log line lands in joeylog.txt there", fileHolds(path, LOG_FIRST "\n")); +#if defined(JL_HAS_FILE_ROOTS) + if (reopen) { + jlLogClose(); + jlLogF("%s", LOG_SECOND); + check("after jlLogClose the next line is appended", fileHolds(path, LOG_FIRST "\n" LOG_SECOND "\n")); + jlLogClose(); + pathJoin(moved, root, LOG_NAME, LOG_MOVED_SUFFIX); + check("the log moves away", rename(path, moved) == 0); + jlLog(LOG_THIRD); + check("jlLogClose closed the file: the next line starts a new one", fileHolds(path, LOG_THIRD "\n") && fileHolds(moved, LOG_FIRST "\n" LOG_SECOND "\n")); + } +#else + (void)reopen; +#endif +} + + +// A name too long for the core's path buffers: every call fails, and no +// file appears (a cut-short path would make one). +static void testLongName(const char *root) { + char name[LONG_NAME_CHARS + 1]; + char saves[PATH_BYTES]; + int before; + + memset(name, LONG_NAME_CHAR, LONG_NAME_CHARS); + name[LONG_NAME_CHARS] = '\0'; + pathJoin(saves, root, SAVES_DIR, ""); + before = dirEntries(saves); + check("too long: jlDataOpen gives NULL", jlDataOpen(name, "rb") == NULL); + check("too long: jlSaveWrite fails", !jlSaveWrite(name, SAVE_TEXT, (uint32_t)strlen(SAVE_TEXT))); + check("too long: jlSaveOpen gives NULL", jlSaveOpen(name, "wb") == NULL); + check("too long: jlSaveExists is false", !jlSaveExists(name)); + check("too long: no file appeared in SAVES/", dirEntries(saves) == before); +} + + +// The save calls write, read, stream and delete in root + SAVES/. +static void testSaves(const char *root) { + char path[PATH_BYTES]; + char buf[FILE_BYTES]; + uint32_t got; + FILE *fp; + + pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, SAVE_NAME); + check("jlSaveWrite", jlSaveWrite(SAVE_NAME, SAVE_TEXT, (uint32_t)strlen(SAVE_TEXT))); + check("the save file is in SAVES/ there", fileHolds(path, SAVE_TEXT)); + check("jlSaveExists finds it", jlSaveExists(SAVE_NAME)); + memset(buf, 0, sizeof(buf)); + got = jlSaveRead(SAVE_NAME, buf, sizeof(buf) - 1); + check("jlSaveRead reads it back", got == strlen(SAVE_TEXT) && strcmp(buf, SAVE_TEXT) == 0); + + fp = jlSaveOpen(STREAM_NAME, "wb"); + check("jlSaveOpen", fp != NULL); + if (fp != NULL) { + fputs(STREAM_TEXT, fp); + fclose(fp); + } + pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, STREAM_NAME); + check("the streamed file is in SAVES/ there", fileHolds(path, STREAM_TEXT)); + + pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, SAVE_NAME); + check("jlSaveDelete", jlSaveDelete(SAVE_NAME)); + check("the save file is gone from SAVES/ there", !pathExists(path)); +} + + +int main(int argc, char **argv) { +#if defined(JL_HAS_FILE_ROOTS) + if (argc != ROOTS_ARGC) { + fprintf(stderr, "usage: %s READROOT/ WRITEROOT/ (run in an empty directory)\n", argv[0]); + return 2; + } + gReadRoot = argv[ARG_READ_ROOT]; + gWriteRoot = argv[ARG_WRITE_ROOT]; + printf("fileroot-host: roots (JL_HAS_FILE_ROOTS): read %s, write %s\n", gReadRoot, gWriteRoot); + check("fixtures: DATA/ in the current directory and the read root", fixtureWrite("", CWD_TEXT) && fixtureWrite(gReadRoot, ROOT_TEXT)); + check("the write root does not exist yet", !pathExists(gWriteRoot)); + + check("jlDataOpen reads the read root's DATA/, not the current directory's", dataReads(ROOT_TEXT)); + check("jlDataOpen of a missing file gives NULL", jlDataOpen(MISSING_NAME, "rb") == NULL); + check("reading data never asked for the write root", gWriteRootAsks == 0 && !pathExists(gWriteRoot)); + + testSaves(gWriteRoot); + testLog(gWriteRoot, true); + testLongName(gWriteRoot); + check("nothing written in the current directory", !pathExists(SAVES_DIR) && !pathExists(LOG_NAME)); + + // An empty root is the current directory: the layout without the flag. + printf("fileroot-host: roots, both empty\n"); + jlLogClose(); + gReadRoot = ""; + gWriteRoot = ""; + check("jlDataOpen reads the current directory's DATA/", dataReads(CWD_TEXT)); + testSaves(""); + jlLog(LOG_EMPTY_ROOT); + check("the log line lands in the current directory's joeylog.txt", fileHolds(LOG_NAME, LOG_EMPTY_ROOT "\n")); + jlLogClose(); +#else + (void)argc; + (void)argv; + printf("fileroot-host: plain (no JL_HAS_FILE_ROOTS)\n"); + check("fixture: DATA/ in the current directory", fixtureWrite("", CWD_TEXT)); + check("jlDataOpen reads the current directory's DATA/", dataReads(CWD_TEXT)); + check("jlDataOpen of a missing file gives NULL", jlDataOpen(MISSING_NAME, "rb") == NULL); + testSaves(""); + testLog("", false); + testLongName(""); +#endif + + printf("fileroot-host: %d passed, %d failed\n", gPass, gFail); + return gFail == 0 ? 0 : 1; +}