More bug fixing and optimizations.

This commit is contained in:
Scott Duensing 2026-10-05 16:47:00 -05:00
parent 10232e73bb
commit e0898cc576
64 changed files with 3542 additions and 1011 deletions

View file

@ -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-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-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-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 These X68000 gates run under the patched MAME and the X68000 firmware that
`./install.sh install emulator:x68000` sets up. `./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 ## Repository layout

View file

@ -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 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` 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 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 program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's

View file

@ -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 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 overlap, with a 32-bit byte count on every port. It is the port's libc
instead of a libc byte loop, split correctly at bank boundaries. `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"] [cols="1,1,4", options="header"]
|=== |===

View file

@ -68,7 +68,8 @@ at 9600 baud, so:
At 19200 baud each lasts half as long, and so on. 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 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 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 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 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 stay loss-free at 9600 through full-screen presents, clears, joystick reads
and sound effects. 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. * *The program's own code*, wherever it masks interrupts.
Ways to live with them: Ways to live with them:

View file

@ -42,9 +42,9 @@ jlFree(buffer);
`jlAlloc` does *not* zero the memory it returns. `jlAlloc` does *not* zero the memory it returns.
For bulk copies, prefer `jlMemCopy` over `memcpy`. It has the same contract For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is
(no overlap) but the IIgs implements it as `MVN` block moves at 7 cycles per `memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
byte instead of a libc byte loop, split correctly at bank boundaries. cycles per byte, split correctly at bank boundaries.
==== Large Local Variables ==== 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 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 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 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 and mouse interrupts wait (see the serial chapter for how long a run can be
the next frame rather than mid-frame, so a fade can lag its pixels by one before received bytes are lost). Palette and SCB changes take effect at the
frame but never tears. 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 === 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 time, at the next `jlAudioFrameTick`, because DVX's sound library may not
be called from an interrupt. be called from an interrupt.
* *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes. * *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 * *Serial ports* belong to DVX's serial library, one program each. As on
every port, closing one leaves DTR as it is. every port, closing one leaves DTR as it is.
* A program's `main` is compiled as `jlGameMain`, which the port calls with * A program's `main` is compiled as `jlGameMain`, which the port calls with

View file

@ -23,7 +23,9 @@
#define FRAME_SIZE 80 #define FRAME_SIZE 80
#define FRAME_INSET 2 #define FRAME_INSET 2
#define DOT_SIZE 8 #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_W 24
#define BUTTON_H 16 #define BUTTON_H 16
@ -32,21 +34,15 @@
#define STATUS_W 80 #define STATUS_W 80
#define STATUS_H 4 #define STATUS_H 4
#define ROW_GAP 6
#define STICK_TOP 16 #define STICK_TOP 16
#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + 6) #define BUTTON_TOP (STICK_TOP + FRAME_SIZE + ROW_GAP)
#define STATUS_TOP (BUTTON_TOP + BUTTON_H + 6) #define STATUS_TOP (BUTTON_TOP + BUTTON_H + ROW_GAP)
#define STICK0_LEFT 24 #define STICK0_LEFT 24
#define STICK1_LEFT 216 #define STICK1_LEFT 216
static void buildPalette(jlSurfaceT *screen); #define CODEGEN_BYTES (8u * 1024u)
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);
typedef struct { typedef struct {
int16_t dotX; int16_t dotX;
@ -56,6 +52,15 @@ typedef struct {
bool valid; bool valid;
} StickViewT; } 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]; 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) { static int16_t buttonXFor(int16_t left, int16_t idx) {
/* jlFillRect marks the rect dirty; jlStagePresent flushes only that int16_t centerX;
* dirty band. */
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
jlStagePresent();
}
centerX = (int16_t)(left + FRAME_SIZE / 2);
// Draw the static stick frame (just an outlined square -- four edges if (idx == 0) {
// with the inset filled background). return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2);
static void drawFrame(jlSurfaceT *screen, int16_t left) { }
int16_t top; return (int16_t)(centerX + BUTTON_GAP / 2);
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);
} }
@ -123,23 +115,30 @@ static int16_t dotYFor(int8_t ay) {
} }
static int16_t buttonXFor(int16_t left, int idx) { // jlFillRect marks the rect dirty; jlStagePresent flushes only that band.
int16_t centerX; 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) { // Draw the static stick frame (just an outlined square -- four edges
return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2); // with the inset filled background).
} static void drawFrame(jlSurfaceT *screen, int16_t left) {
return (int16_t)(centerX + BUTTON_GAP / 2); 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) { static void initialPaint(jlSurfaceT *screen) {
int16_t i; int16_t i;
int16_t left;
jlSurfaceClear(screen, COLOR_BACKGROUND); jlSurfaceClear(screen, COLOR_BACKGROUND);
for (i = 0; i < JOYSTICK_COUNT; i++) { for (i = 0; i < JOYSTICK_COUNT; i++) {
int16_t left;
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT; left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
drawFrame(screen, left); drawFrame(screen, left);
gView[i].valid = false; gView[i].valid = false;
@ -174,27 +173,20 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
} }
if (!v->valid || v->connected != connected) { if (!v->valid || v->connected != connected) {
drawAndPresent(screen, drawAndPresent(screen, (int16_t)(left + (FRAME_SIZE - STATUS_W) / 2), STATUS_TOP, STATUS_W, STATUS_H, connected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
(int16_t)(left + (FRAME_SIZE - STATUS_W) / 2),
STATUS_TOP,
STATUS_W, STATUS_H,
connected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
v->connected = connected; v->connected = connected;
} }
// Move the position dot: erase the old square (paint background), // 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 || v->dotX != newDotX || v->dotY != newDotY) {
if (v->valid) { if (v->valid) {
drawAndPresent(screen, v->dotX, v->dotY, DOT_SIZE, DOT_SIZE, COLOR_BACKGROUND); drawAndPresent(screen, v->dotX, v->dotY, DOT_SIZE, DOT_SIZE, COLOR_BACKGROUND);
} }
drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, (ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF);
(ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF);
v->dotX = newDotX; v->dotX = newDotX;
v->dotY = newDotY; 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. // Button squares.
@ -204,8 +196,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
continue; continue;
} }
bx = buttonXFor(left, i); bx = buttonXFor(left, i);
drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF);
newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF);
v->btn[i] = newBtn[i]; v->btn[i] = newBtn[i];
} }
@ -214,10 +205,10 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
int main(void) { int main(void) {
jlConfigT config; jlConfigT config;
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = 8 * 1024; config.codegenBytes = CODEGEN_BYTES;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());

View file

@ -7,7 +7,8 @@
// MAME with a host peer on the line and Lua moving the mouse, and checks that // MAME with a host peer on the line and Lua moving the mouse, and checks that
// neither side disturbs the other. // 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), // - Transmits an endless stream, byte k = streamByte(k, TX_STREAM_MASK),
// STREAM_CHUNK bytes per loop. // STREAM_CHUNK bytes per loop.
// - Checks the peer's endless stream in order, byte k = streamByte(k, // - Checks the peer's endless stream in order, byte k = streamByte(k,
@ -22,6 +23,13 @@
// //
// Mouse: a small pointer follows it. // 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, // The bottom two stage rows are verification scanlines: one nibble per pixel,
// high nibble first, behind a sentinel, for a harness that reads the // high nibble first, behind a sentinel, for a harness that reads the
// framebuffer back: // framebuffer back:
@ -37,7 +45,12 @@
#include <joey/joey.h> #include <joey/joey.h>
#include <joey/serial.h> #include <joey/serial.h>
#ifndef LINE_BAUD
#define LINE_BAUD 9600u #define LINE_BAUD 9600u
#endif
#ifndef DENSE_SCB_LINES
#define DENSE_SCB_LINES 0u
#endif
#define LINE_DATA_BITS 8u #define LINE_DATA_BITS 8u
#define LINE_STOP_BITS 1u #define LINE_STOP_BITS 1u
@ -86,6 +99,9 @@
#define COLOR_POINTER 15 #define COLOR_POINTER 15
#define PALETTE_RED_SHIFT 8 #define PALETTE_RED_SHIFT 8
#define PALETTE_GREEN_SHIFT 4 #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 { typedef struct StreamStatsT {
uint32_t received; // peer bytes read so far = index of the next one expected uint32_t received; // peer bytes read so far = index of the next one expected
@ -108,6 +124,7 @@ typedef struct PointerT {
} PointerT; } PointerT;
static void buildPalette(jlSurfaceT *screen); 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 void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color);
static uint16_t mouseFlags(void); static uint16_t mouseFlags(void);
static void movePointer(jlSurfaceT *screen, PointerT *pointer); 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++) { for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | 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(); screen = jlStageGet();
buildPalette(screen); buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); buildScb(screen);
jlSurfaceClear(screen, COLOR_BACKGROUND); jlSurfaceClear(screen, COLOR_BACKGROUND);
line.baud = LINE_BAUD; line.baud = LINE_BAUD;

View file

@ -4,8 +4,9 @@
# exact sprite-to-background collision bits), the standard sprite # exact sprite-to-background collision bits), the standard sprite
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving # bitmaps (cab, passenger, flame, warp, death, intro star, leaving
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the # cels), the demo-mode RNG table ($446A), the SID SFX programs (the
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0) # 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0).
# and the level-intro star velocity tables ($450C/$4513). # (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 # Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE
# "bank ram" dump, 2-byte load-address header) so the port has one # "bank ram" dump, 2-byte load-address header) so the port has one

View file

@ -27,6 +27,15 @@
#define ST_TICK_HZ 30u #define ST_TICK_HZ 30u
#define ST_MAX_CATCHUP_TICKS 4u #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 // 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 // $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. // levels/level01.dat .. level25.dat, so Y is index 24.
@ -85,6 +94,8 @@ typedef struct {
bool fireArmed; // $445D: FIRE must be released first bool fireArmed; // $445D: FIRE must be released first
uint8_t lastFrame; uint8_t lastFrame;
uint16_t paceAcc; 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 StStateE afterJingle; // where ST_STATE_JINGLE returns to
bool gameOverTune; // $5C24: song 5 started after the wait bool gameOverTune; // $5C24: song 5 started after the wait
uint8_t hsSlot; // $4D51 table slot being named uint8_t hsSlot; // $4D51 table slot being named
@ -189,11 +200,11 @@ static bool joyFire(void);
static uint8_t joyMask(void); static uint8_t joyMask(void);
static bool loadLevel(uint8_t index); static bool loadLevel(uint8_t index);
static uint8_t nextLevelForShift(void); static uint8_t nextLevelForShift(void);
static uint8_t paceTicks(void); static uint8_t paceTicks(uint8_t *c64Frames);
static bool serviceJingle(void); static bool serviceJingle(void);
static void runCabbiesMenu(uint8_t ticks); static void runCabbiesMenu(uint8_t ticks);
static void runDemoEnd(void); static void runDemoEnd(void);
static void runIntro(uint8_t ticks); static void runIntro(uint8_t c64Frames);
static void runNameEntry(uint8_t ticks); static void runNameEntry(uint8_t ticks);
static bool nameEntryStart(void); static bool nameEntryStart(void);
static void runPlaying(uint8_t ticks); 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: // 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 // 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 // the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
// length just clamps to the catch-up cap. // length just clamps to the catch-up cap. *c64Frames gets the C64 frames
static uint8_t paceTicks(void) { // due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at
uint16_t hz = jlFrameHz(); // 50/60 Hz, so the level intro shows every frame the C64 does -- the
uint8_t now = (uint8_t)jlFrameCount(); // cab's up-and-down judder ($470D) is a one-frame pattern that stepping
uint8_t frames = (uint8_t)(now - gGame.lastFrame); // two frames at a time cancels out.
uint8_t ticks = 0u; 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; gGame.lastFrame = now;
if (frames > 16u) { if (frames > 16u) {
frames = 16u; frames = 16u;
} }
if (hz == 50u || hz == 60u) { if (hz == 50u || hz == 60u) {
gGame.paceAcc += (uint16_t)(frames * 35u); perTick = 35u;
hz = 70u; hz = 70u;
} else { }
gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ); 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 // 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. // 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. // $4666 loop: once per C64 frame; joystick ends a demo. A port that shows
static void runIntro(uint8_t ticks) { // 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; uint8_t k;
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
runDemoEnd(); runDemoEnd();
return; 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)) { if (stIntroStep(&gSim)) {
jlMusicStop(); // $4709 jlMusicStop(); // $4709
stSimEnterLevel(&gSim, &gLevel); stSimEnterLevel(&gSim, &gLevel);
@ -930,7 +981,8 @@ static void startIntro(void) {
// $459C: the intro's tune plays under the fly-in, not blocking; the // $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. // C64 leaves it to the IRQ player and stops it at $4709.
stAudioMusic(ST_SONG_INTRO, true); 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 (;;) { for (;;) {
uint8_t ticks; uint8_t ticks;
uint8_t c64Frames;
if (gGame.fatal) { if (gGame.fatal) {
fatalHold(); fatalHold();
@ -1045,6 +1098,7 @@ int main(void) {
gGame.paused = false; gGame.paused = false;
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
gGame.paceAcc = 0u; gGame.paceAcc = 0u;
gGame.frameAcc = 0u;
if (gGame.state == ST_STATE_INTRO) { if (gGame.state == ST_STATE_INTRO) {
stAudioMusic(ST_SONG_INTRO, true); // $46B5 stAudioMusic(ST_SONG_INTRO, true); // $46B5
} }
@ -1058,7 +1112,7 @@ int main(void) {
enterTitle(); enterTitle();
} }
} }
ticks = paceTicks(); ticks = paceTicks(&c64Frames);
switch (gGame.state) { switch (gGame.state) {
case ST_STATE_TITLE: case ST_STATE_TITLE:
runTitle(ticks); runTitle(ticks);
@ -1090,7 +1144,7 @@ int main(void) {
} }
break; break;
case ST_STATE_INTRO: case ST_STATE_INTRO:
runIntro(ticks); runIntro(c64Frames);
break; break;
case ST_STATE_PLAYING: case ST_STATE_PLAYING:
runPlaying(ticks); runPlaying(ticks);

View file

@ -323,8 +323,8 @@ static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) {
} }
n = (gSpeechStageCount < want) ? gSpeechStageCount : want; n = (gSpeechStageCount < want) ? gSpeechStageCount : want;
if (n != 0u) { if (n != 0u) {
// jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there), // jlMemCopy (MVN on the IIgs), in two runs when the unread bytes
// in two runs when the unread bytes wrap the ring's end. // wrap the ring's end.
uint8_t *to = (uint8_t *)dst; uint8_t *to = (uint8_t *)dst;
uint16_t left = n; uint16_t left = n;

View file

@ -2,8 +2,9 @@
// See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the // See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the
// cab set (flying, wreck and the intro decorations) and passenger set in // cab set (flying, wreck and the intro decorations) and passenger set in
// multicolour at the constant level colours (cab/passenger 6, shared // multicolour at the constant level colours (cab/passenger 6, shared
// $D025/$D026 = 2/7), the flame set in multicolour colour 7, then the // $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No
// hires intro star in colour 7. // intro star: the level intro paints its starfield into the character
// screen (stTitle.c), so the star sprite is never drawn.
#include "stCels.h" #include "stCels.h"
@ -12,7 +13,7 @@
#define MC0 0x02u #define MC0 0x02u
#define MC1 0x07u #define MC1 0x07u
#define CAB 0x06u // cab and passenger per-sprite colour #define CAB 0x06u // cab and passenger per-sprite colour
#define FLM 0x07u // flame / star colour #define FLM 0x07u // flame colour
const StCelDefT kStCels[] = { const StCelDefT kStCels[] = {
// Cab: flying (gear up/down x left/right), wreck cels, intro cabs. // 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 }, { 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 },
{ 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 }, { 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 },
{ 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, 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])); const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0]));

View file

@ -37,10 +37,10 @@
// The IIgs reaches its globals DBR-relative, so all of BSS must fit the // 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 // entry bank below the I/O window; the caches are sized down there. The
// sprite cache must still hold every cel stRenderPrewarm builds (the // sprite cache must still hold every cel stRenderPrewarm builds (the
// cab, exhaust and passenger sets plus the intro star) or a cel change // cab, exhaust and passenger sets) or a cel change mid-play rebuilds
// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped // and recompiles a sprite. Glyphs are direct-mapped by character, so
// by character, so the count is a power of two. // the count is a power of two.
// Room for the baked cab/passenger/exhaust set (37) plus the largest // Room for the baked cab/passenger/exhaust set (36) plus the largest
// level's hook cels (I: 30) at once. // level's hook cels (I: 30) at once.
#define ST_SPRITE_CACHE 80u #define ST_SPRITE_CACHE 80u
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #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 mc0;
uint8_t mc1; uint8_t mc1;
uint8_t level; // level index the bitmap came from (level sprites) 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 used;
bool pinned; // prewarmed moving cel: never evicted (its bool pinned; // prewarmed moving cel: never evicted (its
// compiled code is costly to re-emit on the 65816) // 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). // What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
typedef struct { typedef struct {
bool drawn; bool drawn;
bool trusted; // backed up by the compiled, unclipped save
uint16_t x; uint16_t x;
uint8_t y; uint8_t y;
uint8_t ptr; uint8_t ptr;
@ -92,8 +97,7 @@ typedef struct {
// tile, animated by rewriting logoSlotMask[]'s palette slots. // tile, animated by rewriting logoSlotMask[]'s palette slots.
bool logoMode; bool logoMode;
bool starMode; // level intro: cycling the starfield bool starMode; // level intro: cycling the starfield
uint8_t starPhase; // which ring the bright band is on uint8_t starPhase; // which step of every flight is lit
uint8_t starTick; // frames until the band steps outward
bool logoTileOk; // the live glyph is one of the frames bool logoTileOk; // the live glyph is one of the frames
uint8_t logoSlotCount; uint8_t logoSlotCount;
uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in 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. // 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 }; 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 // Level-intro warp: how a star step looks at each distance BEHIND the
// bright band, as C64 colour indices (white, light grey, grey, dark // lit step, as C64 colour indices; further behind is unlit. One yellow
// grey, then black). A ring this far behind the band or further is // star per ray, as the C64's star sprites (colour 7, $4523) fly with no
// unlit, so a lit head with a short fading tail sweeps outward and the // trail.
// rest of the field stays dark. static const uint8_t kStarRamp[] = { 7u };
static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u };
#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) #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 StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx);
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
static void cacheCompile(StSpriteCacheT *e); static void cacheCompile(StSpriteCacheT *e);
static StSpriteCacheT *cacheFreeSlot(void); 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 void levelCelsEvict(void);
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); 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 bool spriteOnScreen(int16_t px, int16_t py);
static void starPaletteApply(void); static void starPaletteApply(void);
static void tileFromChunky(jlTileT *out, const uint8_t *chunky); static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
static bool tileHasPixels(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);
}
}
}
// Compile a cached cel if it is not already, backing off after a failure // 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, // The cached cel for hardware sprite `idx` of the current frame, built
// built on first use from its VIC bitmap and colours. // on first use from its VIC bitmap and colours; NULL when there is
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { // nothing to draw.
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) {
uint8_t ptr = sim->frame.ptr[idx]; uint8_t ptr = sim->frame.ptr[idx];
uint8_t color = drawColor(sim, idx); uint8_t color = drawColor(sim, idx);
uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); 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. // 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) { 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; e->lastUse = gRender.cacheStamp;
return e->sprite; return e;
} }
for (k = 0u; k < ST_SPRITE_CACHE; k++) { for (k = 0u; k < ST_SPRITE_CACHE; k++) {
e = &gRender.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) { 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; e->lastUse = gRender.cacheStamp;
gRender.lastHit[idx] = e; gRender.lastHit[idx] = e;
return e->sprite; return e;
} }
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the // Pinned entries (prewarmed cab/exhaust/passenger cels) are the
// costly-to-recompile ones and are never evicted; the least- // 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->used = false;
slot->compiled = false; slot->compiled = false;
} }
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) {
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) {
return 0; return 0;
} }
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a // 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->used = true;
slot->pinned = gPrewarming; slot->pinned = gPrewarming;
gRender.lastHit[idx] = slot; 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, // Build a VIC bitmap's cel into a cache entry from the shared cel
// because the two-shift form of the multiply came back as a __mulhi3 // expansion (stCels.c, the same one the offline baker uses), cropped to
// library call on the 65816. // 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, // 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 // so the warning below stays about real misses. Only the title screen's
// decorations qualify: logoColorCycle recolours them every cycle step, // 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 uint8_t cellCol(uint16_t cell, uint8_t row) {
static const uint16_t kRowBase[ST_SCREEN_ROWS] = { static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) {
} }
return; return;
} }
// A plain loop, not memset: 25 entries do not pay for the 65816 // One loop over both 25-entry arrays.
// libc's far-called byte loop.
for (row = 0u; row < ST_SCREEN_ROWS; row++) { for (row = 0u; row < ST_SCREEN_ROWS; row++) {
gRender.dirtyColMin[row] = 0xFFu; gRender.dirtyColMin[row] = 0xFFu;
gRender.dirtyColMax[row] = 0u; 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 // Public
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
} }
} }
if (gRender.starMode) { if (gRender.starMode) {
// The warp is the only thing animating here, and it animates in // The warp animates in the palette: no cells change and no sprite
// the palette: no cells change, no sprite moves for it, so the // moves for it. The lit step follows the intro's own frame count,
// present that ends this frame copies no pixels at all. // so the stars keep the C64's speed whatever the frame rate.
gRender.starTick++; uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
if (gRender.starTick >= ST_STAR_STEP_FRAMES) {
gRender.starTick = 0u; if (phase != gRender.starPhase) {
gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); gRender.starPhase = phase;
starPaletteApply(); starPaletteApply();
} }
} }
@ -752,17 +799,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = ST_HW_SPRITES; k > first; k--) { for (k = ST_HW_SPRITES; k > first; k--) {
StDrawnT *d = &gRender.slot[k - 1u]; StDrawnT *d = &gRender.slot[k - 1u];
if (d->drawn) { 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; d->drawn = false;
} }
} }
paintCells(stage, sim); paintCells(stage, sim);
for (k = first; k < ST_HW_SPRITES; k++) { for (k = first; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k]; StDrawnT *d = &gRender.slot[k];
jlSpriteT *sp; StSpriteCacheT *e;
int16_t px; int16_t px;
int16_t py; int16_t py;
if ((sim->frame.enableMask & kStBit[idx]) == 0u) { if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
continue; continue;
} }
@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
if (!spriteOnScreen(px, py)) { if (!spriteOnScreen(px, py)) {
continue; continue;
} }
sp = cachedSprite(sim, idx); e = cachedSprite(sim, idx);
if (sp == 0) { if (e == 0) {
continue; 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->drawn = true;
d->x = sim->frame.x[idx]; d->x = sim->frame.x[idx];
d->y = sim->frame.y[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. // The first unused cache slot, or NULL when the cache is full.
static StSpriteCacheT *cacheFreeSlot(void) { static StSpriteCacheT *cacheFreeSlot(void) {
uint8_t k; uint8_t k;
@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
} }
if (got == n) { if (got == n) {
slot->sprite = cels[k]; 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 { } else {
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr); const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
if (bm == 0) { if (bm == 0) {
continue; continue;
} }
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1); if (!celCreate(slot, 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) {
continue; continue;
} }
slot->compiled = false; 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 // 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 // count (a stale bake), so the caller falls back to the runtime JIT
// prewarm below. Every port stages its own bake (make/<port>.mk). // prewarm below. Every port stages its own bake (make/<port>.mk).
@ -1002,6 +1061,10 @@ static bool loadPrecompiledCels(void) {
e->mc0 = kStCels[i].mc0; e->mc0 = kStCels[i].mc0;
e->mc1 = kStCels[i].mc1; e->mc1 = kStCels[i].mc1;
e->level = 0xFFu; e->level = 0xFFu;
e->offX = 0u;
e->offY = 0u;
e->w = ST_SPRITE_PX;
e->h = ST_SPRITE_PX;
e->used = true; e->used = true;
e->pinned = true; e->pinned = true;
e->lastUse = 0u; e->lastUse = 0u;
@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) {
jlStagePresent(); 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) { 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 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 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 }; 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; uint8_t done = 0u;
bool ok; bool ok;
uint8_t k; uint8_t k;
@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
done++; done++;
loadingBar(stage, done, total); 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; sim->frame = saved;
gPrewarming = false; gPrewarming = false;
ok = prewarmReport(total); ok = prewarmReport(total);
@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *sim) {
// Turn on the level intro's palette-cycled starfield. Call it after // Turn on the level intro's palette-cycled starfield. Call it after
// stRenderSceneChanged (which turns it back off) on entry to the level // stRenderSceneChanged (which turns it back off) on entry to the level
// intro. The cells were painted by stIntroEnter; from here the warp is // 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) { void stRenderIntroStars(void) {
gRender.starMode = true; gRender.starMode = true;
gRender.starPhase = 0u; gRender.starPhase = 0u;
gRender.starTick = 0u;
starPaletteApply(); starPaletteApply();
} }

View file

@ -100,8 +100,9 @@
// pixel rows. // pixel rows.
// //
// The contract between the two halves is just this colour range: // 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 // stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its
// the centre), and the renderer cycles that range. These are C64 colour // 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 // 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. // mode, 4 and 5; the cab sprite adds 2, 6 and 7.
#define ST_STAR_RING_FIRST 8u #define ST_STAR_RING_FIRST 8u
@ -112,12 +113,14 @@
// something else must fail to compile here instead of quietly making // something else must fail to compile here instead of quietly making
// the warp skip rings. // the warp skip rings.
typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; 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 // A C64 star flies $20 frames ($4500) and the painted field marks every
// stars are the far ones and get the small glyph. The big glyph belongs // ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on
// to the outer rings, the stars about to rush past the viewer. // that many intro frames (introStepCount): the warp keeps the C64's speed at
#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings // any frame rate.
#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings #define ST_INTRO_STAR_FLIGHT 0x20u
#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant #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 // VIC sprite block pointers the game selects by name. The cab's bit 0 is
// the landing gear, so each facing is a pair. // 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 uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
} StLevelT; } 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 { typedef struct {
uint8_t col; // $7175,X X low byte uint8_t col; // $7175,X X low byte
uint8_t msb; // $7185,X X high bit (any non-zero value = set) 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 enable; // $718E,X
uint8_t ptr; // $7197,X block pointer uint8_t ptr; // $7197,X block pointer
uint8_t color; // $719F,X uint8_t color; // $719F,X
uint8_t strideToPow2[2];
} StSpriteT; } StSpriteT;
typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1];
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the // The frame the VIC last displayed ($4253 marshal + $4293 flush): the
// renderer draws this and the collision test evaluates it. // renderer draws this and the collision test evaluates it.
@ -542,6 +551,7 @@ typedef struct {
uint8_t logoCycleAux; // $48A5 uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6 uint8_t logoDiv; // $48A6
uint8_t introLoopCount; // $4508 uint8_t introLoopCount; // $4508
uint8_t introStepCount; // port: intro frames run, for the painted warp
uint8_t introCabDx; // $450A uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B uint8_t introCabDxTimer; // $450B
uint8_t spriteMc0; // $D025 uint8_t spriteMc0; // $D025

View file

@ -14,8 +14,10 @@
// //
// The seven star sprites are replaced by a static starfield whose // 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 // 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 // how. The stars sit on the seven rays the C64's stars fly, where those
// sequence keeps its length and its cue; only the warp changes. // 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 <string.h> #include <string.h>
@ -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_SPARKLE_TICKS 0x5Au
#define ST_TITLE_HOVER_COL 0x28u #define ST_TITLE_HOVER_COL 0x28u
#define ST_INTRO_STAR_PTR 0xDAu #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_END_ROW 0x94u
#define ST_INTRO_CAB_DX_RELOAD 0x28u #define ST_INTRO_CAB_DX_RELOAD 0x28u
// The palette-cycled starfield. Seven stars per ring so the bright band // The palette-cycled starfield. The C64 flies one star sprite along each
// stays the same weight all the way out, placed at fixed pseudo-random // of seven rays at these velocities ($450C / $4513, pixels a frame) for
// angles and clear of the three lines the intro screen writes (the level // $20 frames ($4500), then launches it again from the start. The painted
// name on row 12, and the demo hints on rows 3 and 24). // field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP
typedef struct { // frames; the renderer lights one step of every ray at a time, so a star
uint8_t col; // hops out along its ray. A star is drawn where the sprite's image ($DA:
uint8_t row; // a 5x3 speck centred on pixel 11, 9 of the sprite) would be.
uint8_t ring; #define ST_INTRO_STARS 7u
} StStarT; #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[] = { static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 };
{ 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 };
{ 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 void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta);
static void logoColorCycle(StSimT *sim); static void logoColorCycle(StSimT *sim);
static void logoFlip(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 // $4113
@ -124,21 +119,60 @@ static void logoFlip(StSimT *sim) {
} }
// Paint the level-intro starfield into screen RAM, once. A star's cell // Paint the level-intro starfield into screen RAM, once. A star's cell is
// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- // coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by
// the renderer animates the warp by cycling those colours, which costs // its ray's launch delay so the rays fire out of step as on the C64, and is
// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. // never touched again -- the renderer animates the warp by cycling those
static void paintStarfield(StSimT *sim) { // colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h).
const StStarT *star; // Step 0 is the launch point, which every ray shares, and steps off the
uint8_t one[2]; // screen are left out, as are rows that carry the intro's text: on the C64
uint8_t k; // 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 // Before any star goes down, or the first star on a row would hide the
for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { // rest of that row's.
star = &kIntroStar[k]; for (row = 0u; row < ST_SCREEN_ROWS; row++) {
one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; textRow[row] = rowHasText(sim, row);
stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring));
} }
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. // $4523..$4665 -- set up the level-name screen.
void stIntroEnter(StSimT *sim, const StLevelT *level) { void stIntroEnter(StSimT *sim, const StLevelT *level) {
uint8_t delay[ST_INTRO_STARS];
uint8_t k; uint8_t k;
sim->level = level; sim->level = level;
@ -161,20 +196,19 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->spr[7].row = 0xE4u; sim->spr[7].row = 0xE4u;
sim->introCabDxTimer = 0x14u; sim->introCabDxTimer = 0x14u;
sim->introCabDx = 0x01u; sim->introCabDx = 0x01u;
for (k = 0u; k < 7u; k++) { for (k = 0u; k < ST_INTRO_STARS; k++) {
uint8_t idx = (uint8_t)(6u - k); uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k);
sim->spr[idx].row = 0x8Cu; sim->spr[idx].row = ST_INTRO_STAR_ROW;
sim->spr[idx].msb = 0u; 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].color = 0x07u;
sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].ptr = ST_INTRO_STAR_PTR;
sim->spr[idx].enable = 0u; sim->spr[idx].enable = 0u;
// $44F8/$4500 held each star's spawn timer and reload. Nothing // $44F8: each star's launch delay. The painted field uses it to
// reads them now that the warp is painted rather than flown, but // put the rays out of step, and the seven RNG draws MUST stay in
// the seven RNG draws MUST stay: they advance the shared RNG and // any case: they advance the shared RNG and the level that
// the level that follows draws from it, so dropping them would // follows draws from it.
// change the game that comes after the intro. delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS);
(void)stSimRng(sim, 0x20u);
} }
sim->borderColor = 0u; sim->borderColor = 0u;
sim->bgColor = 0u; sim->bgColor = 0u;
@ -188,12 +222,13 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->charDirtyAll = true; sim->charDirtyAll = true;
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
stSimDrawText(sim, 10u, 12u, level->name, 3u); stSimDrawText(sim, 10u, 12u, level->name, 3u);
paintStarfield(sim);
if (sim->demoMode != 0u && sim->postMortem == 0u) { if (sim->demoMode != 0u && sim->postMortem == 0u) {
stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u);
stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u);
} }
paintStarfield(sim, delay);
sim->introLoopCount = 0u; sim->introLoopCount = 0u;
sim->introStepCount = 0u;
} }
@ -203,6 +238,7 @@ bool stIntroStep(StSimT *sim) {
uint8_t k; uint8_t k;
stSimMarshal(sim); stSimMarshal(sim);
sim->introStepCount++;
sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u);
addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]);
if ((sim->introLoopCount & 1u) == 0u) { if ((sim->introLoopCount & 1u) == 0u) {

View file

@ -170,6 +170,7 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
uint16_t now; uint16_t now;
uint32_t startMillis; uint32_t startMillis;
uint32_t endMillis; uint32_t endMillis;
uint16_t afterFirst;
bool slowOp; bool slowOp;
count = 0UL; count = 0UL;
@ -178,13 +179,20 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
startFrame = jlFrameCount(); startFrame = jlFrameCount();
startMillis = jlMillisElapsed(); startMillis = jlMillisElapsed();
// Calibrate: jlWaitVBL aligned us to a tick edge, so a sub-frame op // Calibrate: an op slower than one frame advances the counter on
// cannot see the counter advance during a single call. If the first // every call -- poll the clock every call then, so a batch cannot
// call DOES advance it, the op is slower than one frame -- poll the // overrun the window. One advance proves nothing: jlWaitVBL syncs to
// clock every call so a batch cannot overrun the window. // 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(); op();
count++; count++;
slowOp = ((uint16_t)(jlFrameCount() - startFrame) != 0u); afterFirst = jlFrameCount();
op();
count++;
slowOp = (afterFirst != startFrame) && (jlFrameCount() != afterFirst);
for (;;) { for (;;) {
now = jlFrameCount(); now = jlFrameCount();
@ -1032,6 +1040,84 @@ static void PROBE_ATTR op_probeVgaCheck(void) {
#endif #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) #if defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_IIGS)
// P7-4 (perf-hunt-2 rank 4): gameFrameClean component split. The // P7-4 (perf-hunt-2 rank 4): gameFrameClean component split. The
// golden row = 3 copyRect window erases + 3 sprite draws + present; // golden row = 3 copyRect window erases + 3 sprite draws + present;
@ -1928,6 +2014,9 @@ static void __attribute__((noinline)) runAllTests(void) {
#ifdef JOEYLIB_PLATFORM_DOS #ifdef JOEYLIB_PLATFORM_DOS
op_probeVgaCheck(); op_probeVgaCheck();
#endif #endif
#ifdef JOEYLIB_PLATFORM_DVX
op_probeDvxDisplayCheck();
#endif
#endif #endif
jlLogF("UBER: ----- end -----"); jlLogF("UBER: ----- end -----");

View file

@ -46,9 +46,9 @@ void *jlAlloc(uint32_t bytes);
// Release a buffer returned by jlAlloc. NULL is accepted and ignored. // Release a buffer returned by jlAlloc. NULL is accepted and ignored.
void jlFree(void *p); void jlFree(void *p);
// memcpy for big buffers. Same contract as memcpy (no overlap), but the // memcpy with a 32-bit count. Same contract as memcpy (no overlap); every
// IIgs runs it as MVN block moves (7 cycles a byte) instead of the libc // port runs its libc memcpy (on the IIgs MVN block moves at 7 cycles a
// byte loop, split at bank boundaries by the port. // byte, split at bank boundaries).
void jlMemCopy(void *dst, const void *src, uint32_t bytes); void jlMemCopy(void *dst, const void *src, uint32_t bytes);
// Block the calling thread until the next display vertical blank. // Block the calling thread until the next display vertical blank.

View file

@ -78,20 +78,11 @@
#define JOEYLIB_NATIVE_CHUNKY #define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "MS-DOS" #define JOEYLIB_PLATFORM_NAME "MS-DOS"
#elif defined(JOEYLIB_PLATFORM_X68000) #elif defined(JOEYLIB_PLATFORM_X68000)
// BRING-UP STATE: chunky stage in main RAM, expanded to GVRAM by jlpPresent. // The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c,
// This is deliberately the slow-but-correct path -- it makes every one of the // the Amiga's layout), and a present copies its dirty rows into the text
// 772 lines of src/generic work unmodified, so the port renders correctly // plane at $E00000, which is four 1bpp planes too. The text plane has one
// before a single native primitive exists. // 16-colour palette, so per-line SCB palettes are reloaded as the beam scans
// // by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s).
// 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.
#define JOEYLIB_CPU_68000 #define JOEYLIB_CPU_68000
#define JOEYLIB_ENDIAN_BIG #define JOEYLIB_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR #define JOEYLIB_NATIVE_PLANAR
@ -136,9 +127,9 @@
#define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else #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_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else
#define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro #define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro
#define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: inline word copy for #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: iigsCopyRectRows
// short spans (llvm-mos memcpy is a slow far // [dp],y row copies (short rows lose to
// byte-loop, finding #79), function // memcpy's per-call setup), function
#define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro #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_SAVE // noop macro (codegen path saves)
#define JL_HAS_SPRITE_RESTORE // noop macro #define JL_HAS_SPRITE_RESTORE // noop macro

View file

@ -279,10 +279,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(SPRITEBAKE_BIN) $< $@ $(SPRITEBAKE_BIN) $< $@
$(SPRITEBAKE_BIN): $(SPRITEBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-amiga $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-amiga
$(STAXIBAKE_BIN): $(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
# The cel bank is generated from the game's cel table (stCels.c). # The cel bank is generated from the game's cel table (stCels.c).

View file

@ -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). # other targets' bakes it must refuse, and the expected pixels (common.mk).
TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk 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)) 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 $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@ -224,6 +234,15 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) $(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) $(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) $(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 $@) @mkdir -p $(dir $@)
$(SPRITEBAKE_BIN) $< $@ $(SPRITEBAKE_BIN) $< $@
$(SPRITEBAKE_BIN): $(SPRITEBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-atarist $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-atarist
$(STAXIBAKE_BIN): $(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
# The cel bank is generated from the game's cel table (stCels.c). # The cel bank is generated from the game's cel table (stCels.c).

View file

@ -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); # 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. # 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) 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)

View file

@ -300,10 +300,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(SPRITEBAKE_BIN) $< $@ $(SPRITEBAKE_BIN) $< $@
$(SPRITEBAKE_BIN): $(SPRITEBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-dos $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-dos
$(STAXIBAKE_BIN): $(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
# The cel bank is generated from the game's cel table (stCels.c). # 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 $@) @mkdir -p $(dir $@)
cp $< $@ cp $< $@
$(MKSTLEVEL_BIN): $(MKSTLEVEL_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk mkstlevel $(MAKE) -f $(REPO_DIR)/make/tools.mk mkstlevel
$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod $(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod

View file

@ -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 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 # 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 # 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 # 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 TILEBANK_OWN := $(tilebank_DATA)/tilebank/own.tbk
DOS_STAXI_DATA := $(REPO_DIR)/build/dos/bin/STAXI/DATA 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-lib: $(LIB)
dvx-apps: $(addprefix app-,$(APPS)) dvx-apps: $(addprefix app-,$(APPS))
dvx-probe: $(addprefix app-,$(PROBE_APPS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ $(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/. # 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 # sdk.mk rebuilds only what changed; the project files are rewritten only
# when their contents change, so an unchanged program is not relinked. # 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 define appRule
app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
@mkdir -p $(APPSDIR)/$(1) @mkdir -p $(APPSDIR)/$(1)
@ -194,7 +210,7 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
'VENDOR = $(VENDOR)' \ 'VENDOR = $(VENDOR)' \
'LIBS = $(LIB)' \ 'LIBS = $(LIB)' \
'DATA = $$($(1)_DATA)' \ '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 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/Makefile
@printf '%s\n' \ @printf '%s\n' \
'.topic $(1).overview' \ '.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)"' \ 'description text "JoeyLib example: $$($(1)_TITLE)"' \
| $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res
$$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package $$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package
@mkdir -p $(BINDIR) @mkdir -p $$(or $$($(1)_BIN),$(BINDIR))
rm -rf $(BINDIR)/apps/$(VENDOR)/$(1) rm -rf $$(or $$($(1)_BIN),$(BINDIR))/apps/$(VENDOR)/$(1)
cp -R $(APPSDIR)/$(1)/pkg/apps $(BINDIR)/ cp -R $(APPSDIR)/$(1)/pkg/apps $$(or $$($(1)_BIN),$(BINDIR))/
endef endef
$(foreach a,$(APPS),$(eval $(call appRule,$(a)))) $(foreach a,$(APPS) $(PROBE_APPS),$(eval $(call appRule,$(a))))
.PHONY: audio-data tilebank-data staxi-data .PHONY: audio-data tilebank-data staxi-data
audio-data: $(AUDIO_DATA_FILES) audio-data: $(AUDIO_DATA_FILES)

View file

@ -146,10 +146,10 @@ $(STAXI_IIGS_LEVELS) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs
STAXIBAKE := $(REPO_DIR)/build/tools/staxibake STAXIBAKE := $(REPO_DIR)/build/tools/staxibake
$(SPRITEBAKE_IIGS): $(SPRITEBAKE_IIGS): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-iigs $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-iigs
$(STAXIBAKE): $(STAXIBAKE): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
$(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c $(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c

View file

@ -39,8 +39,9 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
LDFLAGS := -lm LDFLAGS := -lm
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c) PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
# Raw interrupt shims (timer tick, SCB raster, SCC receive): each saves every # Raw interrupt shims (timer tick, frame count, SCB raster, SCC receive): each
# register and returns with RTE, which a C function cannot. # 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) PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
# Sprite codegen is the shared 68k PLANAR emitter, unchanged. Its # 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)) \ $(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(BUILD)/obj/68k/planarFill.o \ $(BUILD)/obj/68k/planarFill.o \
$(BUILD)/obj/68k/planarPlot.o \ $(BUILD)/obj/68k/planarPlot.o \
$(BUILD)/obj/68k/stageBands.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteCompile.o \
$(BUILD)/obj/codegen/spriteStage.o $(BUILD)/obj/codegen/spriteStage.o
@ -133,7 +135,7 @@ STAXI_DIR := $(BINDIR)/STAXI
STAXI_BIN := $(STAXI_DIR)/STAXI.X 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 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 all x68000: x68000-lib x68000-examples
@ -259,10 +261,10 @@ $(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(STAXI_GEN_DIR)/levels @mkdir -p $(STAXI_GEN_DIR)/levels
python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(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 $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000
$(STAXIBAKE_BIN): $(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c $(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 x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X
$(REPO_DIR)/scripts/verify-x68000-serial-mouse.sh $(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 # 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). # ports, held through MAME's pads and read back from JOY.X's display (~10 s).
x68000-verify-joy: $(BINDIR)/JOY.X x68000-verify-joy: $(BINDIR)/JOY.X

View file

@ -18,8 +18,15 @@
# 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was # 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was
# set: the same hashes, the full-screen present's timings. # 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 # default 600 s timeout
# scripts/bench-dvx.sh --full-screen # on the whole screen # 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 # TIMEOUT=900 scripts/bench-dvx.sh # override
set -euo pipefail set -euo pipefail
@ -31,19 +38,35 @@ joeyNeed DOSBOX dosbox
joeyNeed DVX_ROOT dvx joeyNeed DVX_ROOT dvx
fullScreen=0 fullScreen=0
if [ "${1:-}" = "--full-screen" ]; then program=uber
fullScreen=1 appDir=$(dvxAppDir uber)
elif [ $# -gt 0 ]; then out=$DVX_BIN/$DVX_LOG_NAME
echo "usage: scripts/bench-dvx.sh [--full-screen]" >&2 for arg in "$@"; do
exit 2 case "$arg" in
fi --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} timeoutSeconds=${TIMEOUT:-600}
# The emulator's hard cap, a little past the wait, whatever this script does. # The emulator's hard cap, a little past the wait, whatever this script does.
wallSeconds=$((timeoutSeconds + 30)) wallSeconds=$((timeoutSeconds + 30))
out=$DVX_BIN/$DVX_LOG_NAME dvxLogOut=${out%/*}/dvx.log
doneLine="press any key to exit" 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) work=$(mktemp -d -t joeylib-bench-dvx.XXXXXX)
guard="" guard=""
@ -56,20 +79,20 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir uber)" dvxStage "$work/c" "$appDir"
if [ "$fullScreen" = 1 ]; then if [ "$fullScreen" = 1 ]; then
dvxFullScreen "$work/c" uber dvxFullScreen "$work/c" "$program"
fi fi
dvxDosboxConf "$work/dosbox.conf" "$work/c" dvxDosboxConf "$work/dosbox.conf" "$work/c"
mkdir -p "$work/xdg" mkdir -p "$work/xdg"
rm -f "$out" rm -f "$out" "$dvxLogOut"
display=":$((RANDOM % 400 + 1700))" display=":$((RANDOM % 400 + 1700))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
xvfb=$! xvfb=$!
sleep 1 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 \ XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wallSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ timeout -s KILL "$wallSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 &
@ -77,13 +100,13 @@ guard=$!
dosbox=$(joeyChild "$guard" || true) dosbox=$(joeyChild "$guard" || true)
start=$SECONDS start=$SECONDS
log=$(dvxAwaitLog "$work/c" uber "$doneLine" "$timeoutSeconds" "$guard" || true) log=$(dvxAwaitLog "$work/c" "$program" "$doneLine" "$timeoutSeconds" "$guard" || true)
waited=$((SECONDS - start)) waited=$((SECONDS - start))
if [ -z "$log" ]; then if [ -z "$log" ]; then
log=$(dvxProgramLog "$work/c" uber) log=$(dvxProgramLog "$work/c" "$program")
fi fi
if [ -z "$log" ]; then 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 dvxDvxLog "$work/c" >&2
exit 1 exit 1
fi fi
@ -91,6 +114,7 @@ tr -d '\r' < "$log" > "$out"
joeyStop "$dosbox" "$guard" 2> /dev/null joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox="" dosbox=""
guard="" guard=""
dvxDvxLogCopy "$work/c" "$dvxLogOut"
if [ "$fullScreen" = 1 ] && ! dvxDvxLog "$work/c" 200 | grep -q "$DVX_FULL_SCREEN_LOG"; then 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 echo "bench-dvx: FAIL -- --full-screen, but DVX.LOG has no \"$DVX_FULL_SCREEN_LOG\" (DVX.LOG follows)" >&2

27
scripts/check-dirtymark.sh Executable file
View file

@ -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"

44
scripts/check-fileroots.sh Executable file
View file

@ -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)"

View file

@ -8,6 +8,8 @@
DVX_GATE="$(basename "$0" .sh)" DVX_GATE="$(basename "$0" .sh)"
DVX_BIN="$JOEY_REPO/build/dvx/bin" 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. # The folder a program installs in under apps/, and the name of its log.
DVX_VENDOR=joeylib DVX_VENDOR=joeylib
DVX_LOG_NAME=joeylog.txt 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 # 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). # the stage's size at 8 bits a pixel).
DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp" 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). # The runtime parts of toolchains/opt/dvx (all but the SDK).
DVX_RUNTIME="dvx.exe config system libs widgets apps" DVX_RUNTIME="dvx.exe config system libs widgets apps"
# The desktop the gates run: 640x480 in 16-bit colour, DVX's own DOS test # 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 # 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 # exits, or DVX.LOG says a program failed to load) for PROGRAM's log to hold
# it does. # a line matching REGEX; print its path if it does.
dvxAwaitLog() { dvxAwaitLog() {
local log local log
local waited local waited
@ -175,6 +180,7 @@ dvxAwaitLog() {
return 0 return 0
fi fi
joeyAlive "$5" || return 1 joeyAlive "$5" || return 1
dvxDvxLog "$1" 400 | grep -q "$DVX_LOAD_FAILED_LOG" && return 1
sleep 1 sleep 1
done done
return 1 return 1
@ -201,3 +207,13 @@ dvxDvxLog() {
[ -n "$log" ] && tr -d '\r' < "$log" | tail -"${2:-15}" [ -n "$log" ] && tr -d '\r' < "$log" | tail -"${2:-15}"
return 0 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
}

191
scripts/lib/joyScreen.py Normal file
View file

@ -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=<state> B=<state> scale=<S> dots=<A's x,y offset> <B's>"; 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))

View file

@ -4,14 +4,18 @@
KEYS (examples/keys) draws every character it received on its row 198 as 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 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 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 screenshot (on a desktop the window is wherever DVX put it, at the window's
holds exactly TARGET. 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 --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 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 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[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF
PAL12[15] = 0xF00 PAL12[15] = 0xF00
PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12] 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 # 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 MAX_DIST = 3 * 9 ** 2
NO_NIBBLE = -1
SENTINEL = (0xA, 0x5, 0xA, 0x5) 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): def main(argv):
@ -38,12 +87,15 @@ def main(argv):
png, target = argv[1], argv[2] png, target = argv[1], argv[2]
size = None size = None
at = None at = None
scales = SCALES
args = argv[3:] args = argv[3:]
while args: while args:
if args[0] == "--size" and len(args) > 1: if args[0] == "--size" and len(args) > 1:
size = tuple(int(n) for n in args[1].split("x")) size = tuple(int(n) for n in args[1].split("x"))
elif args[0] == "--at" and len(args) > 1: elif args[0] == "--at" and len(args) > 1:
at = tuple(int(n) for n in args[1].split(",")) at = tuple(int(n) for n in args[1].split(","))
elif args[0] == "--scale" and len(args) > 1:
scales = (int(args[1]),)
else: else:
sys.stderr.write(__doc__) sys.stderr.write(__doc__)
return 2 return 2
@ -51,43 +103,30 @@ def main(argv):
img = Image.open(png).convert("RGB") img = Image.open(png).convert("RGB")
w, h = img.size w, h = img.size
px = img.load()
if size is not None and img.size != size: 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])) print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1]))
return 1 return 1
nearest = {} pixels = list(img.getdata())
nibs = nibbleMap(img)
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]
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii") expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
found = None found = None
for y in range(h): for scale in scales:
for x in range(w - 2 * len(expect)): found = findRow(nibs, pixels, w, h, scale, 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
if found: if found:
break break
if not found: 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 return 1
x, y, row = found 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: if row != expect:
print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper())) print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
return 1 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: 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])) print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1]))
return 1 return 1

View file

@ -5,14 +5,20 @@
# #
# Kinds, in the order `all` runs them: # Kinds, in the order `all` runs them:
# host the shared layer on the host, no emulator: the new-port # 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 # golden UBER on each port, its 51 per-op surface hashes against
# tests/goldens/uber (x68000 against the IIgs's, its own gate; # tests/goldens/uber (x68000 against the IIgs's, its own gate;
# dvx against the DOS port's, whose stage it shows) # 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 # visual iigs: every launchable example on joey.2mg boots and draws
# dvx: full screen -- UBER's hashes through a suspend and a # dvx: full screen -- UBER's hashes through a suspend and a
# resume, and KEYS's row read off the 320x200 VGA screen, then # resume, and KEYS's row read off the 320x200 VGA screen, then
# in its window, then on the whole screen again # 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 # x68000: the SCB -- every text-palette reload of a 147-change
# screen lands in the blanking before its band, timed on the beam # screen lands in the blanking before its band, timed on the beam
# tilebank each port: examples/tilebank loads the port's own .tbk bake # 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 # gate's surface hash, and refuses every other target's bake (the
# host half is check-tilebank.sh under `host`) # host half is check-tilebank.sh under `host`)
# input each port: a typed string reaches jlInputGetChar byte-exact # input each port: a typed string reaches jlInputGetChar byte-exact
# (dvx: and the present shows it, decoded from the desktop) # (dvx: and the present shows it, decoded from the desktop, also
# x68000: also both joysticks -- every direction, both triggers # at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program)
# and every release, read back from examples/joy's display # 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: # audio six gates, two kinds:
# iigs DOC wavetable + sound IRQ vector (register tap) # iigs DOC wavetable + sound IRQ vector (register tap)
# x68000 YM2151 noise + PPI port C + ADPCM rate (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 # save iigs: a SAVES/ blob written and read back through GS/OS
# loss iigs: 4096 bytes through full-screen presents, lossless on the # loss iigs: 4096 bytes through full-screen presents, lossless on the
# modem port at 9600 and a Super Serial Card at 2400 # 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 # disk iigs: the one-disk STAXI (ROM 3 and ROM 01) and AGI images boot
# and draw # and draw
# #
@ -91,6 +100,8 @@ AMIGA_UBER_SECONDS=400
# kind port name command...: every gate, in run order within its kind. # kind port name command...: every gate, in run order within its kind.
GATES=$(cat <<EOF GATES=$(cat <<EOF
host host blank scripts/check-blank.sh host host blank scripts/check-blank.sh
host host roots scripts/check-fileroots.sh
host host marks scripts/check-dirtymark.sh
host host input scripts/check-input.sh host host input scripts/check-input.sh
host host millis scripts/check-millis.sh host host millis scripts/check-millis.sh
host host save scripts/check-save.sh host host save scripts/check-save.sh
@ -100,11 +111,13 @@ host host tilebank scripts/check-tilebank.sh
golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt
golden x68000 uber scripts/verify-x68000-golden.sh golden x68000 uber scripts/verify-x68000-golden.sh
golden atarist uber scripts/bench-atarist.sh && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/atarist/bin/joeylog.txt golden atarist uber scripts/bench-atarist.sh && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/atarist/bin/joeylog.txt
golden atarist marks scripts/verify-atarist-dirtymark.sh
golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && tools/diff-uber-hashes tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && tools/diff-uber-hashes tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT
golden dvx uber scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt golden dvx uber scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt
visual iigs examples scripts/verify-iigs-all.sh visual iigs examples scripts/verify-iigs-all.sh
visual dvx full scripts/verify-dvx-fullscreen.sh visual dvx full scripts/verify-dvx-fullscreen.sh
visual dvx display scripts/verify-dvx-display.sh
visual x68000 scb scripts/verify-x68000-scb.sh visual x68000 scb scripts/verify-x68000-scb.sh
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1 tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
@ -119,6 +132,8 @@ input atarist input scripts/verify-atarist-input.sh
input amiga input scripts/verify-amiga-input.sh input amiga input scripts/verify-amiga-input.sh
input dos input scripts/verify-dos-input.sh input dos input scripts/verify-dos-input.sh
input dvx input scripts/verify-dvx-input.sh input dvx input scripts/verify-dvx-input.sh
input dvx depths scripts/verify-dvx-input.sh 640x480x8:1 800x600x15:2 1024x768x32:3
input dvx joy scripts/verify-dvx-joy.sh
audio iigs audio scripts/verify-iigs-audio.sh staxi audio iigs audio scripts/verify-iigs-audio.sh staxi
audio x68000 audio scripts/verify-x68000-audio.sh audio x68000 audio scripts/verify-x68000-audio.sh
audio atarist audio scripts/verify-atarist-audio.sh audio atarist audio scripts/verify-atarist-audio.sh
@ -136,6 +151,7 @@ mouse x68000 mouse scripts/verify-x68000-mouse.sh
mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh
save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1 save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1
loss iigs loss make -s -f make/iigs.mk iigs-verify-serial-loss || exit 1 loss iigs loss make -s -f make/iigs.mk iigs-verify-serial-loss || exit 1
loss x68000 scb scripts/verify-x68000-serial-scb.sh
disk iigs staxi make -s -f make/iigs.mk iigs-verify-game-disk GAME=STAXI || exit 1 disk iigs staxi make -s -f make/iigs.mk iigs-verify-game-disk GAME=STAXI || exit 1
disk iigs staxi01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/STAXI.2mg disk iigs staxi01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/STAXI.2mg
disk iigs agi make -s -f make/iigs.mk iigs-game-disk-agi || exit 1; scripts/verify-iigs-game-disk.sh build/iigs/bin/AGI.2mg 2 8M disk iigs agi make -s -f make/iigs.mk iigs-game-disk-agi || exit 1; scripts/verify-iigs-game-disk.sh build/iigs/bin/AGI.2mg 2 8M

View file

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# verify-atarist-dirtymark.sh - the stage's dirty-band marker on a real 68000:
# tests/host/dirtyMarkHost.c built for the Atari ST (make -f make/atarist.mk
# atarist-dirtymark -> 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

123
scripts/verify-dvx-display.sh Executable file
View file

@ -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)"

View file

@ -3,18 +3,26 @@
# (examples/keys) as the desktop of a scratch DVX in DOSBox Staging under # (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 # 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 # key events: ':' needs Shift, which DVX's keyboard layout applies before the
# window's onKey hands the character to jlInputGetChar). Two checks: # window's onKey hands the character to jlInputGetChar). Three checks:
# 1. the present: a raw screenshot of the desktop must show KEYS's # 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 # verification scanline (row 198: sentinel, count, every received
# character as two pixel nibbles) somewhere -- the window's content, # character as two pixel nibbles) somewhere -- the window's content,
# expanded from the stage through the port's palette tables into DVX's # expanded from the stage through the port's palette tables into the
# 16-bit desktop -- decoding to exactly the typed bytes # 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); # (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 # as hex to its joeylog.txt (in its settings folder), which must equal
# the typed ASCII. # 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; # Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
# the keys program (make dvx). # the keys program (make dvx).
set -uo pipefail set -uo pipefail
@ -27,10 +35,16 @@ joeyNeed DVX_ROOT dvx
program=keys program=keys
target="192.168.1.10:6510" target="192.168.1.10:6510"
stageWidth=320
stageHeight=200
wall=180 # hard cap on DOSBox, whatever happens to this script wall=180 # hard cap on DOSBox, whatever happens to this script
readySeconds=60 # DVX boot until its shell reports ready readySeconds=60 # DVX boot until its shell reports ready
settleSeconds=3 # then KEYS opening its window and taking the focus 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 logSeconds=30 # Escape until KEYS's typed line is in its log
specs=("$@")
[ ${#specs[@]} -gt 0 ] || specs=(640x480x16:1)
dvxNeedBuild "$program" dvxNeedBuild "$program"
@ -38,6 +52,8 @@ work=$(mktemp -d -t joeylib-dvx-input.XXXXXX)
guard="" guard=""
dosbox="" dosbox=""
xvfb="" xvfb=""
win=""
fails=""
# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". # 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed".
cleanup() { cleanup() {
joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null
@ -45,72 +61,135 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir "$program")"
dvxDosboxConf "$work/dosbox.conf" "$work/c" # fail SPEC WHAT -- note a failed check; the gate goes on to report the rest.
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$work/shots" >> "$work/dosbox.conf" fail() {
mkdir -p "$work/xdg" "$work/shots" 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))" display=":$((RANDOM % 400 + 2500))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
xvfb=$! xvfb=$!
sleep 1 sleep 1
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ for spec in "${specs[@]}"; do
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ echo "verify-dvx-input: $spec"
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & run "$spec"
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
done 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 if [ -n "$fails" ]; then
echo "verify-dvx-input: FAIL - $fails" echo "verify-dvx-input: FAIL - $fails"
exit 1 exit 1
fi 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)"

261
scripts/verify-dvx-joy.sh Executable file
View file

@ -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)"

View file

@ -77,12 +77,14 @@ local TVRAM, PLANE_STEP, TVRAM_STRIDE, PLANES = 0xE00000, 0x20000, 128, 4
local STICK_LEFT = { 24, 216 } local STICK_LEFT = { 24, 216 }
local STICK_TOP = 16 local STICK_TOP = 16
local FRAME_SIZE = 80 local FRAME_SIZE = 80
local FRAME_INSET = 2
local DOT_SIZE = 8 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_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_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_BACKGROUND, COLOR_DOT_OFF, COLOR_DOT_ON = 0, 2, 3
local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5 local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5
local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7 local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7

View file

@ -64,6 +64,18 @@
# PASS: every Lua check right, the peer's stream PASS, at least MIN_BYTES each # 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. # 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 # Requires: ./install.sh install x68000 -- the patched MAME
# (installer/patches/mame: 0002 for the FIFO depth, 0004 for the # (installer/patches/mame: 0002 for the FIFO depth, 0004 for the
# RS-232C slot and the mouse) and the Human68k system disk. # 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}" WALL="${X68K_SERMOUSE_WALL:-600}"
PASSES="${X68K_SERMOUSE_PASSES:-3}" PASSES="${X68K_SERMOUSE_PASSES:-3}"
MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}" MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}"
BAUD="${X68K_SERMOUSE_BAUD:-9600}"
DENSE="${X68K_SERMOUSE_DENSE:-0}"
PROG="$repo/build/x68000/bin/SERMOUSE.X" PROG="$repo/build/x68000/bin/SERMOUSE.X"
FIFO_DEPTH=3 FIFO_DEPTH=3
DEFAULT_BAUD=9600
# The peer outlives MAME by this much, so it always sees the link close, and # 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. # is given this long to report once MAME is gone.
PEER_MARGIN=30 PEER_MARGIN=30
@ -101,12 +116,27 @@ cleanup() {
} }
trap cleanup EXIT 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 x68kBootDisk "$work/boot.xdf" 'A:\SERMOUSE.X' || exit 1
x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1 x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1
{ {
printf 'local PASSES = %d\n' "$PASSES" printf 'local PASSES = %d\n' "$PASSES"
printf 'local MIN_BYTES = %d\n' "$MIN_BYTES" printf 'local MIN_BYTES = %d\n' "$MIN_BYTES"
printf 'local BAUD = %d\n' "$BAUD"
printf 'local DENSE = %d\n' "$DENSE"
cat <<'LUA' cat <<'LUA'
local cpu = manager.machine.devices[":maincpu"] local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"] local mem = cpu.spaces["program"]
@ -293,8 +323,8 @@ end)
local function finish(verdict) local function finish(verdict)
done = true done = true
local s = prev or {} 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", 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, checks, #fails, s.rx or 0, s.tx or 0, 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, quiet and string.format("%d:%d", quiet.rx, quiet.tx) or "none", iackFixed,
#fails > 0 and (" detail=" .. table.concat(fails, ",")) or "")) #fails > 0 and (" detail=" .. table.concat(fails, ",")) or ""))
io.flush() io.flush()
@ -490,6 +520,11 @@ done
# MAME itself, which cleanup stops first (stopping only the timeout process # MAME itself, which cleanup stops first (stopping only the timeout process
# could leave MAME running). # could leave MAME running).
cd "$work" || exit 1 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 \ JOEY_Z80SCC_RX_FIFO=$FIFO_DEPTH timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \ -rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \
@ -538,4 +573,4 @@ fi
if [ "$fail" -ne 0 ]; then if [ "$fail" -ne 0 ]; then
exit 1 exit 1
fi 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)"

View file

@ -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)"

View file

@ -86,7 +86,7 @@ void *jlAlloc(uint32_t bytes) {
void jlMemCopy(void *dst, const void *src, uint32_t bytes) { void jlMemCopy(void *dst, const void *src, uint32_t bytes) {
jlpMemCopy(dst, src, bytes); memcpy(dst, src, (size_t)bytes);
} }

View file

@ -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) // avoids a multiply helper. The caller contract (joey/palette.h)
// guarantees $0RGB input, so copy the row wholesale and force // guarantees $0RGB input, so copy the row wholesale and force
// color 0 to black -- the only documented silent rewrite. The copy // color 0 to black -- the only documented silent rewrite. The copy
// is an unrolled sequence of word assignments, NOT memcpy: the // is an unrolled sequence of word assignments, NOT memcpy: 15 direct
// clang IIgs libc memcpy is a ~30 cyc/byte byte loop (finding #79) // word stores beat the old masked loop on every port, and on the
// and measurably REGRESSED this op when tried (1443 -> 603 // IIgs beat memcpy too (when libc's was a byte loop it cut this op
// ops/sec); 15 direct word stores beat both that and the old // from 1443 to 603 ops/sec; its short-copy loop still costs ~10
// masked loop on every port. // cycles a byte at this size).
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5)); row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
row[0] = 0x0000; row[0] = 0x0000;
row[1] = colors16[1]; row[1] = colors16[1];

View file

@ -288,9 +288,6 @@ extern jlSurfaceT *gStage;
extern uint16_t iigsGetTickWord(void); extern uint16_t iigsGetTickWord(void);
#define jlpFrameCount() iigsGetTickWord() #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). // The one Memory Manager owner ID every IIgs allocation uses (hal.c).
uint16_t iigsMemUserId(void); uint16_t iigsMemUserId(void);
// Interrupt-latency bound of the long interrupts-off paths (hal.c): a // Interrupt-latency bound of the long interrupts-off paths (hal.c): a
@ -777,9 +774,6 @@ uint16_t jlpGenericFrameHz(void);
#endif #endif
uint16_t jlpGenericFrameCount(void); uint16_t jlpGenericFrameCount(void);
#if !defined(jlpMemCopy)
#define jlpMemCopy(_d, _s, _n) memcpy((_d), (_s), (size_t)(_n))
#endif
#if !defined(jlpFrameCount) #if !defined(jlpFrameCount)
#if defined(JL_HAS_FRAME_COUNT) #if defined(JL_HAS_FRAME_COUNT)
uint16_t jlpFrameCount(void); uint16_t jlpFrameCount(void);

View file

@ -55,24 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
return; 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, // 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 // sizeof(uint8_t)==1, so the call form below is exact); every port's
// libc memset is already a vectorized fill. // 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)); memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1));
#endif
if (s == gStage) { if (s == gStage) {
gStageScbDirty = true; gStageScbDirty = true;
} }

View file

@ -473,8 +473,9 @@ static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
return; return;
} }
// Clipped interpreter marks and unusual sprite heights: the shared // Clipped interpreter marks and unusual sprite heights: the shared
// multi-row widen (per-port loop shape in surface.c) -- one source // multi-row widen (the 68000 ports' src/m68k/stageBands.s, the C
// of truth, no second rolled loop here. h == 1 also lands here (a // 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 // sprite clipped to one row); the extra call vs the old macro's
// inline single-row arm is confined to that edge case. // inline single-row arm is confined to that edge case.
surfaceMarkDirtyRows((uint16_t)y, (uint16_t)(y + h), minWord, maxWord); surfaceMarkDirtyRows((uint16_t)y, (uint16_t)(y + h), minWord, maxWord);

View file

@ -13,10 +13,25 @@
#define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t)) #define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t))
#define SURFACE_FILE_BYTES (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES) #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 ----- // ----- Prototypes -----
// (public API declared in joey/surface.h) // (public API declared in joey/surface.h)
// (internal prototypes declared in surfaceInternal.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 ----- // ----- Module state -----
@ -60,9 +75,85 @@ uint16_t gStagePaletteDirtyMask = 0xFFFFu;
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
// (surfaceMarkDirtyRows inlines the per-row widen with walking pointers #if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000)
// -- see below. The single-row widen is inlined by the surfaceMarkDirtyRect // Widen the STAGE_DIRTY_GROUP4_ROWS rows at minPtr / maxPtr. (Spelling the
// macro, and the IIgs multi-row path is the asm marker.) // 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) ----- // ----- Public API (alphabetical) -----
@ -282,104 +373,45 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) {
if (s != gStage) { if (s != gStage) {
return; return;
} }
// Whole-stage mark is a constant fill of both band arrays. On the // Whole-stage mark is a constant fill of both band arrays. The 68000
// IIgs, memset lowers to `jsl memset` -- a far-call byte loop (a C // ports share stageDirtyClearAll's one-pass fill; the x86 and IIgs
// word-fill fared no better: llvm-mos re-derives the far pointer // memsets are already tight (the IIgs's matched the asm word-store
// every store), so the fill is one JSL into the asm word-store // loop it replaced, 2026-10-04).
// loop; the other ports' libc memsets are already tight fills. #if defined(JOEYLIB_CPU_68000)
#if defined(JOEYLIB_PLATFORM_IIGS) stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(0u), STAGE_BAND_FILL(STAGE_DIRTY_FULL_MAX));
iigsStageBandsMarkAll();
#else #else
memset(gStageMinWord, 0, SURFACE_HEIGHT); memset(gStageMinWord, 0, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
#endif #endif
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
{ memset(gStageDirtyGroup, 1, STAGE_DIRTY_GROUPS);
uint8_t grp;
for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) {
gStageDirtyGroup[grp] = 1u;
}
}
#endif #endif
} }
// Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h): // Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h):
// widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never // widen rows y..yEnd-1 to cover [minWord, maxWord]. The IIgs and the 68000
// compile this -- the macro routes them straight to the asm marker // ports never compile this -- the macro routes them to their asm markers --
// iigsMarkDirtyRowsInner instead. // so it is the x86 ports' and the host's. Tall marks go four rows at a time
#ifndef JOEYLIB_PLATFORM_IIGS // (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) { void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
uint8_t *minPtr; uint8_t *minPtr;
uint8_t *maxPtr; uint8_t *maxPtr;
uint8_t *minEnd;
// Straight-line inlined widen (no per-row helper call) with walking if ((uint16_t)(yEnd - y) >= STAGE_MARK_GROUP_MIN_ROWS) {
// pointers, so a multi-row stage op -- sprite draw, fillRect -- markGroups(y, yEnd, minWord, maxWord);
// pays one tight loop instead of yEnd-y function calls. This is return;
// 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);
minPtr = &gStageMinWord[y]; minPtr = &gStageMinWord[y];
maxPtr = &gStageMaxWord[y]; maxPtr = &gStageMaxWord[y];
#if defined(JOEYLIB_PLATFORM_AMIGA) minEnd = &gStageMinWord[yEnd];
{ while (minPtr < minEnd) {
uint16_t rows; markRow(minPtr++, maxPtr++, minWord, maxWord);
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;
}
}
} }
#else
{
uint8_t *minEnd;
minEnd = &gStageMinWord[yEnd];
while (minPtr < minEnd) {
if (minWord < *minPtr) {
*minPtr = minWord;
}
if (maxWord > *maxPtr) {
*maxPtr = maxWord;
}
minPtr++;
maxPtr++;
}
}
#endif
} }
#endif #endif
@ -439,12 +471,16 @@ bool stageAlloc(void) {
void stageDirtyClearAll(void) { void stageDirtyClearAll(void) {
// Constant fill of both band arrays, same form as its sibling // Constant fill of both band arrays, same form as its sibling
// surfaceMarkDirtyAll. This runs after EVERY jlStagePresent, so on // surfaceMarkDirtyAll. This runs after EVERY jlStagePresent. The IIgs
// the IIgs the two memset far-call byte loops here were ~10 ms of // clears only the 8-row groups its group map says were marked
// every present's fixed base; the asm word-store loop fixes that. // (joeyDraw.s), which no fill can match on an idle present. The
// The other ports keep memset (their libc fills are already tight). // 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) #if defined(JOEYLIB_PLATFORM_IIGS)
iigsStageBandsClearAll(); iigsStageBandsClearAll();
#elif defined(JOEYLIB_CPU_68000)
stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MIN), STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MAX));
#else #else
memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT); memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);

View file

@ -78,6 +78,20 @@ typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 :
extern uint8_t gStageMinWord[SURFACE_HEIGHT]; extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[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). // Present-path telemetry (NATIVE-PERF Phase 6 W0, IIgs added W2).
// The presents accumulate how many bytes they moved toward the // The presents accumulate how many bytes they moved toward the
// display and how many band rows they scanned; the UBER_PROBE build // 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 // Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1
// (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes // (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes
// straight to the asm marker so a multi-row mark stays a single JSL; // straight to the asm marker so a multi-row mark stays a single JSL; the
// elsewhere it is the C loop in surface.c. // 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 #ifdef JOEYLIB_PLATFORM_IIGS
void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); 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)) #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 // Reset the bands of every 8-row group the group map says was marked to
// memset to a far-call byte loop -- ~10 ms per present; joeyDraw.s). // the CLEAN sentinels, and clear the map (joeyDraw.s): what
// ClearAll = CLEAN sentinels (post-present); MarkAll = every row fully // stageDirtyClearAll does after each present, skipping clean groups.
// dirty. iigsByteFill is the same fix for arbitrary far-pointer fills
// (SCB range): value replicated in both bytes of `fill`, count >= 1.
void iigsStageBandsClearAll(void); void iigsStageBandsClearAll(void);
void iigsStageBandsMarkAll(void); #elif defined(JOEYLIB_CPU_68000)
void iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count); // 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 #else
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
#endif #endif

View file

@ -39,6 +39,7 @@
#include "port.h" #include "port.h"
#include "audioSfxMixInternal.h" #include "audioSfxMixInternal.h"
#include "dosTimer.h"
#include "joey/audio.h" #include "joey/audio.h"
// ----- SB I/O port offsets from base ----- // ----- SB I/O port offsets from base -----
@ -79,8 +80,6 @@
// ----- PIC ----- // ----- PIC -----
#define PIC1_CMD 0x20
#define PIC1_IMR 0x21
#define PIC2_CMD 0xA0 #define PIC2_CMD 0xA0
#define PIC2_IMR 0xA1 #define PIC2_IMR 0xA1
#define PIC_EOI 0x20 #define PIC_EOI 0x20
@ -881,35 +880,28 @@ static void adlibNoiseInit(void) {
// constraints have to hold afterward: // constraints have to hold afterward:
// //
// 1. BIOS tick counter at $0040:$006C must keep advancing at the // 1. BIOS tick counter at $0040:$006C must keep advancing at the
// canonical 18.2 Hz so anything that reads it (DOS time() / // canonical 18.2 Hz so anything that reads it (DOS time(),
// uclock() / RTC-less time-of-day tracking) stays right. We // RTC-less time-of-day tracking) stays right. We
// use an accumulator: each IRQ adds the PIT divisor to it, // use an accumulator: each IRQ adds the PIT divisor to it,
// and every time it crosses 65536 we bump the BIOS counter // and every time it crosses 65536 we bump the BIOS counter
// and subtract -- the math comes out to exactly 18.2065 Hz // and subtract -- the math comes out to exactly 18.2065 Hz
// regardless of our own IRQ rate. // regardless of our own IRQ rate.
// //
// 2. jlpMillisElapsed must report wall-clock ms even though // 2. jlpMillisElapsed must report wall-clock ms even though the
// uclock()'s sub-tick PIT read is now wrong (PIT cycles // PIT's sub-tick count now spans gPitDivisor, not a BIOS tick.
// 0..gPitDivisor, not 0..65535 as uclock assumes). We expose // We expose an own-ISR-counter-driven ms via the gAudioIsr*
// an own-ISR-counter-driven ms via the gAudioIsr* globals // globals below; src/dos/hal.c::jlpMillisElapsed prefers it
// below; src/dos/hal.c::jlpMillisElapsed prefers it // whenever gAudioIsrActive is true, continuing its timeline.
// whenever gAudioIsrActive is true.
// //
// Literal "chain to old INT $08" doesn't work cleanly under DPMI // Literal "chain to old INT $08" doesn't work cleanly under DPMI
// because the old PM handler ends in IRET, which corrupts the // because the old PM handler ends in IRET, which corrupts the
// stack frame of any CALL into it. INT $1C (user timer hook) and // stack frame of any CALL into it. INT $1C (user timer hook) and
// floppy motor timeout are dropped; nothing AGI cares about uses // floppy motor timeout are dropped; nothing AGI cares about uses
// either. // 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 #define IRQ0_INT_VECTOR 0x08
// Exposed (extern in hal.c) so jlpMillisElapsed can read wall-clock // 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; volatile uint32_t gAudioIsrFires = 0;
uint16_t gAudioIsrHz = 0; uint16_t gAudioIsrHz = 0;
volatile bool gAudioIsrActive = false; 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) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
uint32_t divisor; uint32_t divisor;
@ -957,7 +940,7 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
__asm__ __volatile__("cli" ::: "memory"); __asm__ __volatile__("cli" ::: "memory");
gAudioTickFn = NULL; gAudioTickFn = NULL;
gAudioIsrActive = false; 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); _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec);
__asm__ __volatile__("sti" ::: "memory"); __asm__ __volatile__("sti" ::: "memory");
_go32_dpmi_free_iret_wrapper(&gNewTimerVec); _go32_dpmi_free_iret_wrapper(&gNewTimerVec);
@ -995,7 +978,7 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
gAudioIsrHz = hz; gAudioIsrHz = hz;
gAudioIsrActive = true; gAudioIsrActive = true;
_go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gNewTimerVec); _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"); __asm__ __volatile__("sti" ::: "memory");
return true; return true;
} }

36
src/dos/dosTimer.h Normal file
View file

@ -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 <stdint.h>
#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

View file

@ -15,15 +15,16 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> // uclock / UCLOCKS_PER_SEC
#include <dos.h> #include <dos.h>
#include <dpmi.h> #include <dpmi.h>
#include <go32.h> #include <go32.h>
#include <pc.h> #include <pc.h>
#include <sys/exceptn.h> #include <sys/exceptn.h>
#include <sys/farptr.h>
#include <sys/nearptr.h> #include <sys/nearptr.h>
#include "dosTimer.h"
#include "port.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "x86Expand.h" #include "x86Expand.h"
@ -39,10 +40,13 @@
// ----- Prototypes ----- // ----- Prototypes -----
static void crashHandler(int sig); static void crashHandler(int sig);
static void installCrashLog(void); void dosPitProgram(uint8_t ctrl, uint16_t divisor);
static void uploadPalette(const jlSurfaceT *src); static void installCrashLog(void);
static void uploadPaletteIfNeeded(const jlSurfaceT *src); static uint64_t pitClocksNow(void);
static void pitClockStart(void);
static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src);
// ----- Module state ----- // ----- Module state -----
@ -50,6 +54,15 @@ static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false; static bool gNearEnabled = false;
static FILE *gCrashLog = NULL; 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 old $3DA edge-poll frame-counter state lived here; removed with
// the poll-rate-dependent counter itself -- see jlpFrameCount.) // 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 // 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 // signal handlers that route fault reports there. This runs before the
// VGA mode change so its file handle is established while the screen // 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 // 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 // consumed here: mode 13h has one DAC; the per-line SCB choice is
// applied in the pixel expansion, which reads src->scb directly. // 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) { static void uploadPaletteIfNeeded(const jlSurfaceT *src) {
if (!gStagePaletteDirty) { if (!gStagePaletteDirty) {
return; return;
@ -184,6 +261,7 @@ bool jlpInit(const jlConfigT *config) {
(void)config; (void)config;
installCrashLog(); installCrashLog();
pitClockStart();
memset(&regs, 0, sizeof(regs)); memset(&regs, 0, sizeof(regs));
regs.x.ax = 0x0013; regs.x.ax = 0x0013;
@ -224,25 +302,21 @@ void jlpPresent(const jlSurfaceT *src) {
// bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4 // bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
// gives the byte range x86ExpandRow needs. // gives the byte range x86ExpandRow needs.
// Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed // 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 // ST/Amiga idle-scan trick; STAGE_DIRTY_GROUP4_CLEAN). A failing
// CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX // group keeps the per-row CLEAN test, so a clean row INSIDE a dirty
// (79), so 4 clean rows read exactly 0xFFFFFFFF and the whole // group is skipped exactly as before -- written bytes and counters are
// group skips. A failing group keeps the per-row CLEAN test, so a // byte-identical to the row loop.
// 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; uint8_t g;
const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
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; int16_t yEnd;
if (minGroups[g] == 0xFFFFFFFFul) { if (STAGE_DIRTY_GROUP4_CLEAN(g)) {
continue; continue;
} }
yEnd = (int16_t)((uint16_t)g * 4u + 4u); yEnd = (int16_t)(((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT) + STAGE_DIRTY_GROUP4_ROWS);
for (y = (int16_t)((uint16_t)g * 4u); y < yEnd; y++) { for (y = (int16_t)((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT); y < yEnd; y++) {
// Canonical clean-row test (finding #47): min > max is the // Canonical clean-row test (finding #47): min > max is the
// CLEAN sentinel; see STAGE_DIRTY_ROW_CLEAN. // CLEAN sentinel; see STAGE_DIRTY_ROW_CLEAN.
if (STAGE_DIRTY_ROW_CLEAN(y)) { if (STAGE_DIRTY_ROW_CLEAN(y)) {
@ -319,8 +393,8 @@ void jlpWaitVBL(void) {
} }
// Frame count derived from the millisecond clock (PIT ISR fires or // Frame count derived from the millisecond clock (PIT clocks or audio
// uclock -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The // ISR fires -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The
// old implementation edge-polled $3DA bit 3 and only advanced when a // old implementation edge-polled $3DA bit 3 and only advanced when a
// call happened to land DURING the ~1ms retrace pulse, so any caller // call happened to land DURING the ~1ms retrace pulse, so any caller
// doing more than ~1ms of work between polls silently lost frames // 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 // 1. pitClocksNow (above), from jlpInit's zero.
// 60 Hz sound dispatcher, uclock()'s sub-tick read is wrong // 2. While the audio HAL drives counter 0 at its own rate the count no
// (uclock assumes 0..65535 cycles; ours is 0..gPitDivisor). // longer spans a BIOS tick, so the audio ISR's fire counter is used
// We bypass uclock and derive ms from the ISR fire counter, // instead: each fire is 1000/hz ms, hardware-IRQ-locked.
// which advances exactly once per 1/gAudioIsrHz second wall.
// //
// 2. Otherwise (audio ISR not installed), uclock works correctly // When the source changes -- audio starts, stops, re-registers at another
// because PIT 0 is at the BIOS default and DJGPP's sub-tick // rate or restarts its counter -- the new source is rebased onto the last
// math holds. // value returned, so the clock never jumps back.
// //
// gAudioIsr* are owned by src/dos/audio.c. // gAudioIsr* are owned by src/dos/audio.c.
extern volatile uint32_t gAudioIsrFires; extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz; extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive; extern volatile bool gAudioIsrActive;
static uclock_t gMillisBase = 0;
static bool gMillisBaseSet = false;
uint32_t jlpMillisElapsed(void) { uint32_t jlpMillisElapsed(void) {
uclock_t delta; uint32_t ms;
uint32_t fires;
uint16_t hz;
if (gAudioIsrActive && gAudioIsrHz > 0u) { hz = (gAudioIsrActive && gAudioIsrHz > 0u) ? gAudioIsrHz : 0u;
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so if (hz != 0u) {
// wall-clock-correct and monotonic. uint64 promotion in // uint64 promotion keeps 60 Hz * 4M fires (~18 hours) in range.
// the multiply keeps 60 Hz * 4M ticks (~18 hours) from fires = gAudioIsrFires;
// overflowing. ms = (uint32_t)(((uint64_t)fires * 1000uL) / (uint32_t)hz);
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL) } else {
/ (uint32_t)gAudioIsrHz); // 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 if (hz != gMillisLastHz || fires < gMillisLastIsr) {
// legitimate uclock() value of 0 on the first call cannot re-arm gMillisRebase = gMillisLast - ms;
// the base every call. gMillisLastHz = hz;
if (!gMillisBaseSet) {
gMillisBase = uclock();
gMillisBaseSet = true;
return 0u;
} }
// Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of gMillisLastIsr = fires;
// dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not gMillisLast = ms + gMillisRebase;
// 1193.18), which would run the clock ~0.015% fast. The uint64 return gMillisLast;
// intermediate keeps delta*1000 from overflowing.
delta = uclock() - gMillisBase;
return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
} }
void jlpShutdown(void) { void jlpShutdown(void) {
__dpmi_regs regs; __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) { if (gNearEnabled) {
__djgpp_nearptr_disable(); __djgpp_nearptr_disable();

View file

@ -42,7 +42,6 @@
// ----- Constants ----- // ----- Constants -----
#define NO_VOICE 0 // audio.lib voice ids are > 0 #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. // JoeyLib SFX are signed 8-bit; DVX's 8-bit PCM is unsigned.
#define PCM8_SIGN_FLIP 0x80u #define PCM8_SIGN_FLIP 0x80u
@ -69,10 +68,6 @@
#define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ #define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ
#define NOISE_MIN_HZ (NOISE_CURVE_TOP_HZ - NOISE_PITCH_MAX * NOISE_CURVE_STEP_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 ----- // ----- Types -----
// One SFX slot: a one-shot sample or a stream (or neither), its voice and // 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 // jlAudioTickRegister: the callback, its DVX timer and the ticks the timer
// has counted that are not delivered yet. // has counted that are not delivered yet.
static jlAudioTickFnT gTickFn = NULL; 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 volatile uint32_t gTicksDue = 0;
static bool gTickPinned = false;
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
@ -275,17 +269,12 @@ static void tickIsr(void *userData) {
static void tickStop(void) { static void tickStop(void) {
if (gTickTimer != NO_TIMER) { if (gTickTimer != DVX_PORT_NO_TIMER) {
platformTimerRemove(gTickTimer); dvxPortTimerStop(gTickTimer, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue));
gTickTimer = NO_TIMER; gTickTimer = DVX_PORT_NO_TIMER;
} }
gTickFn = NULL; gTickFn = NULL;
gTicksDue = 0; 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) { if (fn == NULL) {
return true; return true;
} }
if (platformLockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true) != 0) { gTickFn = fn;
return false; gTickTimer = dvxPortTimerStart(hz, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue));
} if (gTickTimer == DVX_PORT_NO_TIMER) {
if (platformLockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false) != 0) { gTickFn = NULL;
(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();
return false; return false;
} }
return true; return true;

View file

@ -13,6 +13,8 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "joey/surface.h"
// The program's task stack. DVX's default (32 KB) is far below what a DOS // 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. // program gets, and JoeyLib programs keep sizable buffers on the stack.
#define DVX_PORT_STACK_BYTES (256 * 1024) #define DVX_PORT_STACK_BYTES (256 * 1024)
@ -40,6 +42,15 @@ void dvxPortExit(int status) __attribute__((noreturn));
// main's status. // main's status.
int32_t dvxPortRun(DxeAppContextT *ctx); 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 ----- // ----- hal.c -----
extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown 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. // Alt+Enter); the choice is kept for the program's next run.
void dvxPortFullScreenToggle(void); 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 // 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 // input poll, the present): yield to the desktop if the program has not
// this virtual frame, then dvxPortEndIfClosed. // this virtual frame, then dvxPortEndIfClosed.

View file

@ -75,6 +75,7 @@
// $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF). // $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF).
#define RGB12_RED_SHIFT 8 #define RGB12_RED_SHIFT 8
#define RGB12_GREEN_SHIFT 4 #define RGB12_GREEN_SHIFT 4
#define RGB12_BLUE_SHIFT 0
#define RGB12_CHANNEL_MASK 0x0Fu #define RGB12_CHANNEL_MASK 0x0Fu
#define RGB12_TO_RGB24 17u #define RGB12_TO_RGB24 17u
@ -88,6 +89,9 @@
#define PALETTES_NONE 0x0000u #define PALETTES_NONE 0x0000u
#define PALETTES_ALL 0xFFFFu #define PALETTES_ALL 0xFFFFu
// dvxPortProbeDisplayMismatch with nowhere the stage is shown.
#define PROBE_NO_TARGET 0xFFFFFFFFu
// Game menu commands. // Game menu commands.
#define CMD_GAME_FULL_SCREEN 100 #define CMD_GAME_FULL_SCREEN 100
#define CMD_GAME_HELP 101 #define CMD_GAME_HELP 101
@ -126,6 +130,7 @@ typedef struct {
static uint64_t clockFrame(void); static uint64_t clockFrame(void);
static uint64_t clockMicros(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 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 fullSet(bool on);
static void lutBuild(const jlSurfaceT *src, uint16_t palettes); static void lutBuild(const jlSurfaceT *src, uint16_t palettes);
static LutKindE lutKind(const DisplayT *d); 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 onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty); static void onPaint(WindowT *win, RectT *dirty);
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show); 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 void redrawAll(void);
static uint8_t rgb12Channel(uint16_t rgb, uint8_t shift);
static bool settingFullScreenLoad(void); static bool settingFullScreenLoad(void);
static void settingFullScreenSave(bool on); static void settingFullScreenSave(bool on);
static const DisplayT *targetDisplay(void); static const DisplayT *targetDisplay(void);
static bool targetGet(PresentTargetT *t); static bool targetGet(PresentTargetT *t);
static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h); static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h);
static bool windowCreate(AppContextT *ac); static bool windowCreate(AppContextT *ac);
static void yieldMark(uint64_t frame);
// ----- Module state ----- // ----- Module state -----
WindowT *gDvxPortWindow = NULL; WindowT *gDvxPortWindow = NULL;
static int32_t gScale = 1; // window pixels per stage pixel, each way static int32_t gScale = 1; // window pixels per stage pixel, each way
static GameFramePacerT gPacer; static GameFramePacerT gPacer;
static bool gClockStarted = false; static bool gClockStarted = false;
static bool gCloseRequested = false; static bool gCloseRequested = false;
static uint64_t gYieldFrame = 0; // the frame the program last yielded in 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, // 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 // 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 // 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 // the mode and tells onFullChange (Active) before this returns. Off: the
// desktop comes back, and the window is redrawn. // 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++) { for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
rgb = src->palette[p][c]; rgb = src->palette[p][c];
r = (uint8_t)(((rgb >> RGB12_RED_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); r = rgb12Channel(rgb, RGB12_RED_SHIFT);
g = (uint8_t)(((rgb >> RGB12_GREEN_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); g = rgb12Channel(rgb, RGB12_GREEN_SHIFT);
b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); b = rgb12Channel(rgb, RGB12_BLUE_SHIFT);
index = (uint8_t)((p << NIBBLE_BITS) | c); index = (uint8_t)((p << NIBBLE_BITS) | c);
switch (gLutKind) { switch (gLutKind) {
case LUT_INDEXED: case LUT_INDEXED:
@ -446,6 +465,7 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
int32_t runY0; int32_t runY0;
int32_t runX0; int32_t runX0;
int32_t runX1; int32_t runX1;
int32_t clean;
uint8_t maxWord; uint8_t maxWord;
if (!targetGet(&t) || t.pixels == NULL) { if (!targetGet(&t) || t.pixels == NULL) {
@ -455,15 +475,25 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
runX0 = 0; runX0 = 0;
runX1 = 0; runX1 = 0;
for (y = 0; y < t.rows; y++) { for (y = 0; y < t.rows; y++) {
if ((palettes & (1u << src->scb[y])) != 0) { // Clean rows end the run: four at a time where no palette change
x0 = 0; // forces a redraw (STAGE_DIRTY_GROUP4_CLEAN), else one.
x1 = SURFACE_WIDTH; clean = 0;
} else if (STAGE_DIRTY_ROW_CLEAN(y)) { 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) { if (runY0 >= 0 && show) {
targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0); targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
} }
runY0 = -1; runY0 = -1;
y += clean - 1;
continue; continue;
}
if ((palettes & (1u << src->scb[y])) != 0) {
x0 = 0;
x1 = SURFACE_WIDTH;
} else { } else {
// Clamp a band past the row end, as the DOS present does. // 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); 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 // 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. // stage goes now. Before jlInit has the stage the first present does it.
static void redrawAll(void) { 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 // The saved choice: whether the program was on the whole screen when the
// user last switched. // user last switched.
static bool settingFullScreenLoad(void) { 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) ----- // ----- Port API (alphabetical) -----
void dvxPortEndIfClosed(void) { void dvxPortEndIfClosed(void) {
@ -679,13 +764,88 @@ void dvxPortFullScreenToggle(void) {
} }
void dvxPortService(void) { // UBER_PROBE's display parity row (examples/uber): the pixels of where the
uint64_t frame; // 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(); *compared = 0u;
if (frame != gYieldFrame) { if (src == NULL || !targetGet(&t) || t.pixels == NULL) {
gYieldFrame = frame; return PROBE_NO_TARGET;
tsYield(); }
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(); dvxPortEndIfClosed();
} }
@ -725,6 +885,8 @@ bool jlpInit(const jlConfigT *config) {
if (!windowCreate(gDvxPortProgram->shellCtx)) { if (!windowCreate(gDvxPortProgram->shellCtx)) {
return false; 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()) { if (settingFullScreenLoad()) {
fullSet(true); fullSet(true);
} }
@ -767,6 +929,10 @@ void jlpPresent(const jlSurfaceT *src) {
// Destroying the window gives the screen back too when the program has it // Destroying the window gives the screen back too when the program has it
// (DVX tells onFullChange nothing then). // (DVX tells onFullChange nothing then).
void jlpShutdown(void) { 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) { if (gDvxPortWindow != NULL) {
dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow); dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow);
gDvxPortWindow = NULL; gDvxPortWindow = NULL;
@ -785,11 +951,11 @@ void jlpWaitVBL(void) {
if (gFullActive) { if (gFullActive) {
tsYield(); tsYield();
dvxExclusiveWaitRetrace(); dvxExclusiveWaitRetrace();
gYieldFrame = clockFrame(); yieldMark(clockFrame());
} else { } else {
gPacer.frame = clockFrame(); gPacer.frame = clockFrame();
(void)gameFrameWait(&gPacer); (void)gameFrameWait(&gPacer);
gYieldFrame = gPacer.frame; yieldMark(gPacer.frame);
} }
dvxPortEndIfClosed(); dvxPortEndIfClosed();
} }

View file

@ -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) ----- // ----- HAL API (alphabetical) -----
const char *jlpReadRoot(void) { const char *jlpReadRoot(void) {

View file

@ -20,8 +20,7 @@
// purpose: the previous byte-at-a-time form let gcc assume d/s alias, which // 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 // 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 -- // 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 // IIgs uses asm macros and Amiga/ST override every tile op.
// byte-loop-libc memcpy caveat (PERF-AUDIT #79) does not apply here.
#define GENERIC_TILE_COPY_ROWS(_d, _dStride, _s, _sStride) do { \ #define GENERIC_TILE_COPY_ROWS(_d, _dStride, _s, _sStride) do { \
uint8_t copyRow_; \ uint8_t copyRow_; \
for (copyRow_ = 0; copyRow_ < TILE_PIXELS_PER_SIDE; copyRow_++) { \ for (copyRow_ = 0; copyRow_ < TILE_PIXELS_PER_SIDE; copyRow_++) { \

View file

@ -66,13 +66,6 @@ typedef void *Handle;
extern uint16_t iigsGetTickWord(void); extern uint16_t iigsGetTickWord(void);
extern uint16_t iigsVblInstall(void); extern uint16_t iigsVblInstall(void);
extern uint16_t iigsVblRemove(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. */ /* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */
extern uint16_t iigsReadHzParam(void); extern uint16_t iigsReadHzParam(void);
static uint16_t gFrameHz = 60u; 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 // start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
// get cleared here after upload. // get cleared here after upload.
// Word stores, NOT memcpy: the clang IIgs libc memcpy is a ~30 cyc/byte // Word stores, NOT memcpy: 16-bit stores land at ~8 cycles a word, ~2.8k
// byte loop (finding #79 in palette.c), so the 712 bytes here cost ~21k // cycles for the pair, where libc memcpy's MVN moves a byte at a time at
// cycles -- 7.6 ms, a third of a frame -- every time both flags fire. // 7 cycles a byte (~5k).
// 16-bit stores land at ~8 cyc/word, ~2.8k cycles for the pair.
static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) { static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) {
uint16_t i; 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 // The Memory Manager owner ID for every JoeyLib allocation on the IIgs
// (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates // (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates
// a fresh application ID per call, so it is called exactly once here // 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 // jlpSurfaceCopyRect override (NATIVE-PERF Phase 7; asm since hunt-3
// rank 1). History of this function IS the llvm-mos lesson arc: the // 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 // 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 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 // the SHIPPED build at ~5.3 ms per 16x16 erase, and the current -S

View file

@ -44,7 +44,6 @@
#include "port.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "joey/surface.h" #include "joey/surface.h"
#include "surfaceInternal.h" // iigsByteFill (hunt-3 rank 5)
// ----- Hardware registers ----- // ----- Hardware registers -----
@ -617,10 +616,9 @@ void jlpInputPoll(void) {
// bit-7 read will clear normally. // bit-7 read will clear normally.
kbdStrb = *IIGS_KBDSTRB; kbdStrb = *IIGS_KBDSTRB;
if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) { if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) {
// iigsByteFill, not memset: llvm-mos lowers memset to a // Runs on nearly every real game frame (idle keyboard), so it is
// far-call byte loop (finding #79), and this branch runs on // libc's 16-bit store fill, not a loop. Hunt-3 rank 5.
// nearly EVERY real game frame (idle keyboard). Hunt-3 rank 5. memset(gKeyState, 0, sizeof(gKeyState));
iigsByteFill(gKeyState, 0u, (uint16_t)sizeof(gKeyState));
} }
readModifierKeys(); readModifierKeys();

View file

@ -5424,26 +5424,18 @@ mdrExit:
; ==================================================================== ; ====================================================================
; iigsStageBandsClearAll(void) / iigsStageBandsMarkAll(void) ; iigsStageBandsClearAll(void)
; ;
; Constant-fill BOTH dirty-band arrays (gStageMinWord/gStageMaxWord, ; Reset the dirty bands (gStageMinWord/gStageMaxWord, 200 bytes each) to
; 200 bytes each) without libc memset. llvm-mos lowers memset to ; the CLEAN sentinels after a present -- min = $FF (STAGE_DIRTY_CLEAN_MIN),
; `jsl memset` -- libc's `while (n--) *d++ = c` byte loop behind a far ; max = $00 (CLEAN_MAX) -- for only the 8-row groups gStageDirtyGroup says
; call (finding #79 class) -- and stageDirtyClearAll runs after EVERY ; were marked, clearing those group bytes too. (It began as a blind fill
; present, so the two memsets were ~10 ms of every present's fixed ; when libc memset was a far-call byte loop; libc's memset is a tight
; base. A C word-fill rewrite still emitted ~35 instructions per store ; 16-bit fill now, but no fill can skip the clean groups.)
; (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.
; ;
; ClearAll: min = $FF (STAGE_DIRTY_CLEAN_MIN), max = $00 (CLEAN_MAX) ; ABI: no args, no return. Same DBR-inherited abs,x/abs,y write path as
; MarkAll: min = $00, max = $4F (STAGE_DIRTY_FULL_MAX = 79)
;
; ABI: no args, no return. Same DBR-inherited abs,x write path as
; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT ; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT
; phk/plb). php/plp preserves caller widths; A/X are cdecl scratch. ; phk/plb). php/plp preserves caller widths; A/X/Y are cdecl scratch.
; SURFACE_HEIGHT = 200 -> last word offset 198; `bpl` exits after the
; offset-0 store wraps X to $FFFE (N set).
; ==================================================================== ; ====================================================================
.section .text.iigsStageBands,"ax" .section .text.iigsStageBands,"ax"
.globl iigsStageBandsClearAll .globl iigsStageBandsClearAll
@ -5521,97 +5513,6 @@ isbcDone:
plp plp
rtl 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, ; iigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank,

View file

@ -121,57 +121,3 @@ iigsReadHzParam:
jsl 0xe10000 jsl 0xe10000
pla ; A = result (return value) pla ; A = result (return value)
rtl 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

152
src/m68k/stageBands.s Normal file
View file

@ -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

View file

@ -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 // The stage is native planar (src/x68000/halPlanar.c) and jlpPresent copies
// into GVRAM. That is deliberately the slow-but-correct path: it makes all of // its dirty rows into the four 1bpp TEXT planes at $E00000, which are planar
// src/generic work unmodified, so the port renders correctly before any native // too (x68kPresentText, src/x68000/x68kPresent.s). The text plane has one
// primitive exists. Nothing here claims a JL_HAS_* override yet. // 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 // VERIFIED UNDER EMULATION (MAME 0.264 + installer/patches/mame): UBER's
// its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so // hashes match the Apple IIgs golden reference 51/51, and every subsystem has
// one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address // its own gate -- see claudeDocs/x68000_port.md for the evidence for each.
// 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.
#include <x68k/dos.h> #include <x68k/dos.h>
#include <x68k/iocs.h> #include <x68k/iocs.h>
@ -337,6 +332,20 @@ static bool gVdispHooked = false; // _VDISPST took x68kVdispIsr
static uint32_t gMillisBase = 0ul; // tickMillisNow() as jlpInit finished static uint32_t gMillisBase = 0ul; // tickMillisNow() as jlpInit finished
static uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL; 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 #if X68K_PRESENT_TEXT
// ----- SCB band tables (text-plane present) ----------------------------------- // ----- 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. // reloads from a table built here -- see "SCB per-band palette" below.
#define X68K_MAX_BANDS SURFACE_HEIGHT #define X68K_MAX_BANDS SURFACE_HEIGHT
#define X68K_NO_PALETTE 0xFFu // no line's SCB value; starts the first band #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 // 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. // 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 // 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 // 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 { typedef struct {
const X68kBandT *end; // one past the last band const X68kBandT *end; // one past the last band
X68kBandT bands[X68K_MAX_BANDS]; X68kBandT bands[X68K_MAX_BANDS];
uint16_t palettes[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; 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; } X68kBandTableT;
typedef char x68kBandPaletteAt0[(offsetof(X68kBandT, palette) == 0u) ? 1 : -1]; 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 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 // Shared with x68kRasterIsr.s, hence not static. Written by the main thread
// only with every interrupt masked; the interrupt runs to completion. // only with every interrupt masked; the interrupt runs to completion.
X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked
X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap
const X68kBandT *volatile gX68kRasterCursor = NULL; // the next band to reload 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 bool gRasterArmed = false;
static uint32_t gRasterOldVec = 0ul; // $138 before we took it static uint32_t gRasterOldVec = 0ul; // $138 before we took it
#endif #endif
// ----- Prototypes ----- // ----- Prototypes -----
static inline uint16_t colorLookup(uint16_t rgb12);
static void colorLutBuild(void);
static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps); static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps);
static void installTickIsr(void); static void installTickIsr(void);
static void installVdispIsr(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 buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src);
static void clearTextPlane(void); static void clearTextPlane(void);
static void disarmRaster(void); static void disarmRaster(void);
static void installScb(const jlSurfaceT *src); static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChanged);
#endif #endif
#if !X68K_PRESENT_TEXT #if !X68K_PRESENT_TEXT
static void buildSpreadTables(void); 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 ------------------------------------------------------------ // ----- Lifecycle ------------------------------------------------------------
bool jlpInit(const jlConfigT *config) { 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 // 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. // if we were already supervisor -- in which case do not switch back.
gPrevSsp = _dos_super(0); gPrevSsp = _dos_super(0);
colorLutBuild();
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
#if X68K_PRESENT_TEXT #if X68K_PRESENT_TEXT
// 16 colours, because the present writes the four 1bpp TEXT planes directly // 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 // 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 // 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. // 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. // 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) { static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src) {
uint16_t pal; uint16_t pal;
uint16_t col; uint16_t col;
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { 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++) { 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 // 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 // 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. // 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; X68kBandTableT *table;
uint16_t sr; uint16_t sr;
uint16_t col; 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 // 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. // about to be rewritten; after that the table it walks cannot change.
sr = x68kInterruptsOff(); sr = x68kInterruptsOff();
@ -857,7 +935,11 @@ static void installScb(const jlSurfaceT *src) {
table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0]; table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0];
x68kInterruptsRestore(sr); x68kInterruptsRestore(sr);
buildScbPalettes(table, src); buildScbPalettes(table, src);
if (buildScbBands(table, src) == 1u) { if (!table->bandsCurrent) {
table->bandCount = buildScbBands(table, src);
table->bandsCurrent = true;
}
if (table->bandCount == 1u) {
disarmRaster(); disarmRaster();
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
X68K_TEXT_PALETTE[col] = table->bands[0].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 (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { 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 // 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 // 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 // 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) { void jlpPresent(const jlSurfaceT *src) {
X68kPlanarT *pd; 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;
if (src == NULL) { if (src == NULL) {
return; return;
@ -1079,38 +1119,15 @@ void jlpPresent(const jlSurfaceT *src) {
return; return;
} }
// gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume // gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume
// both here the way src/iigs/hal.c does. Either one rebuilds the bands, // both here the way src/iigs/hal.c does. Either one updates the band
// because every band table carries its own copy of the palettes. // tables, which carry their own copies of the palettes.
if (gStagePaletteDirty || gStageScbDirty) { if (gStagePaletteDirty || gStageScbDirty) {
installScb(src); installScb(src, gStagePaletteDirtyMask, gStageScbDirty);
gStagePaletteDirty = false; gStagePaletteDirty = false;
gStageScbDirty = false; gStagePaletteDirtyMask = 0u;
} 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);
}
}
} }
x68kPresentText(pd->planes, gStageMinWord, gStageMaxWord, (volatile uint8_t *)(X68K_TVRAM_PLANE0 + x68kTvramOffset(X68K_ORIGIN_X, X68K_ORIGIN_Y)));
} }
#else #else
void jlpPresent(const jlSurfaceT *src) { void jlpPresent(const jlSurfaceT *src) {

View file

@ -1,11 +1,5 @@
// Sharp X68000 per-surface planar storage. // 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 // WHY THIS IS ALMOST amigaPlanar.h
// ------------------------------- // -------------------------------
// A 320px-wide 4bpp surface is 40 bytes per row per plane on ANY planar // 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 // plane 2 $E40000 plane 3 $E60000
// pixel(x,y) -> plane + y*128 + x/8, bit 7-(x&7), plane 0 = LSB // 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. // so the present (src/x68000/x68kPresent.s) scatters the compact 40-byte rows
// That is the ONLY place the 128 appears, and it is where the win comes from: // into that 128-byte stride. That is the ONLY place the 128 matters, and it is
// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000 // where the win comes from: 4 planes x 200 rows x 40 bytes = 32,000 bytes per
// bytes of GVRAM word writes on the chunky path. Bus traffic is what the // frame, against 128,000 bytes of GVRAM word writes on the chunky path. Bus
// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see // traffic is what the measured wait states punish (TVRAM 1.92 cycles/access,
// claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher // GVRAM 1.03 -- see claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic
// per-access cost by roughly 2x overall. // 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, // Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc,
// because nothing but the CPU ever reads them. // 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. // Byte offset of pixel (x,y) within one DISPLAY plane (x a multiple of 8).
// Split out so the present loop and any future asm agree on one definition.
static inline uint32_t x68kTvramOffset(uint16_t x, uint16_t y) { static inline uint32_t x68kTvramOffset(uint16_t x, uint16_t y) {
return ((uint32_t)y * X68K_TVRAM_STRIDE) + (uint32_t)(x >> 3); 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 #endif

234
src/x68000/x68kPresent.s Normal file
View file

@ -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

View file

@ -13,8 +13,28 @@
| return with RTE. Unlike the tick hook there is NO chaining -- the byte is | 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. | 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, | 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 | 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 | drops the byte -- the frame CRC upstream turns that into a clean retransmit-less
| drop rather than corruption. | drop rather than corruption.
@ -24,6 +44,16 @@
.equ SCC_A_CTRL, 0xE98005 | channel A command/status .equ SCC_A_CTRL, 0xE98005 | channel A command/status
.equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer) .equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer)
.equ RING_MASK, 4095 | X68K_RX_RING_CAP - 1 (power of two) .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 .text
.even .even
@ -32,19 +62,33 @@
| Channel A "receive character available". | Channel A "receive character available".
x68kSccRxIsr: x68kSccRxIsr:
movem.l %d0-%d2/%a0,-(%sp) movem.l %d0-%d3/%a0,-(%sp)
move.b SCC_A_DATA,%d0 | the received byte lea gX68kRxRing,%a0
move.w gX68kRxTail,%d1 move.w gX68kRxTail,%d1
moveq #DRAIN_MAX - 1,%d3
rxByte:
move.b SCC_A_DATA,%d0 | the received byte
move.w %d1,%d2 move.w %d1,%d2
addq.w #1,%d2 addq.w #1,%d2
andi.w #RING_MASK,%d2 | next tail andi.w #RING_MASK,%d2 | next tail
cmp.w gX68kRxHead,%d2 cmp.w gX68kRxHead,%d2
beq.s rxFull | ring full: drop, CRC upstream recovers 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.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail
move.w %d2,gX68kRxTail move.w %d2,%d1
rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS move.w %d1,gX68kRxTail
movem.l (%sp)+,%d0-%d2/%a0 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 rte
| Channel A "special receive condition" (overrun / framing error). The byte is | 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: x68kSccSpIsr:
move.l %d0,-(%sp) move.l %d0,-(%sp)
move.b SCC_A_DATA,%d0 | unlatch the offending byte move.b SCC_A_DATA,%d0 | unlatch the offending byte
move.b #0x30,SCC_A_CTRL | WR0: error reset move.b #WR0_ERROR_RESET,SCC_A_CTRL
move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS move.b #WR0_RESET_IUS,SCC_A_CTRL
move.l (%sp)+,%d0 move.l (%sp)+,%d0
rte rte

246
tests/host/dirtyMarkHost.c Normal file
View file

@ -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 <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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;
}

371
tests/host/fileRootHost.c Normal file
View file

@ -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 <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#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;
}