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-serial # both-directions RS-232C, ~1 min
make -f make/x68000.mk x68000-verify-joy # both joysticks, every line, ~10 s
make -f make/x68000.mk x68000-verify-serial-scb # RS-232C under per-line palettes, ~3 min
```
These X68000 gates run under the patched MAME and the X68000 firmware that
`./install.sh install emulator:x68000` sets up.
DVX's display is gated as well: `scripts/verify-dvx-display.sh` runs UBER's
probe build (`make dvx` puts it in `build/dvx/probe`) on nine desktop depths
and scales and on the full screen, and checks every displayed pixel against
the stage.
## Repository layout

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
counter advancing at the canonical 18.2 Hz through an accumulator so `time`
stays correct.
stays correct. `jlMillisElapsed` carries on from where it was when the tick
starts or stops; it never jumps back.
On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's

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
overlap, but the IIgs implements it as `MVN` block moves at 7 cycles per byte
instead of a libc byte loop, split correctly at bank boundaries.
overlap, with a 32-bit byte count on every port. It is the port's libc
`memcpy`; on the IIgs that is `MVN` block moves at 7 cycles per byte, split
correctly at bank boundaries.
[cols="1,1,4", options="header"]
|===

View file

@ -68,7 +68,8 @@ at 9600 baud, so:
At 19200 baud each lasts half as long, and so on.
JoeyLib's own work is safe while a port is open. On the IIgs, where the
JoeyLib's own work is safe while a port is open, with one exception on the
X68000 (below). On the IIgs, where the
display and sound code masks interrupts for long stretches, opening a port
makes JoeyLib cut them into pieces of at most ~1.7 ms: the present and the
full-surface clear, the joystick read (~2.8 ms otherwise, with the stick at
@ -98,6 +99,12 @@ These can still lose bytes, and are the program's to schedule around:
that relies on the protocol to recover. The built-in modem and printer ports
stay loss-free at 9600 through full-screen presents, clears, joystick reads
and sound effects.
* *Per-scanline palettes on the X68000.* The port reloads its one hardware
palette as the beam scans, and a run of palette changes on consecutive lines
holds serial receive off for the whole run, about 32 microseconds a line.
Receive stays loss-free while such runs are under about 90 lines at 9600
baud, 40 at 19200 and 15 at 38400; palette changes three or more lines apart
release it between bands.
* *The program's own code*, wherever it masks interrupts.
Ways to live with them:

View file

@ -42,9 +42,9 @@ jlFree(buffer);
`jlAlloc` does *not* zero the memory it returns.
For bulk copies, prefer `jlMemCopy` over `memcpy`. It has the same contract
(no overlap) but the IIgs implements it as `MVN` block moves at 7 cycles per
byte instead of a libc byte loop, split correctly at bank boundaries.
For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is
`memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
cycles per byte, split correctly at bank boundaries.
==== Large Local Variables
@ -285,9 +285,10 @@ interrupt. Each change of palette between one line and the next costs about
one line of CPU time (32 microseconds); a band more than two lines from the
one before costs about two. A screen that changes palette on every line
therefore spends a third of each frame in that interrupt, during which serial
and mouse interrupts wait. Palette and SCB changes take effect at the start of
the next frame rather than mid-frame, so a fade can lag its pixels by one
frame but never tears.
and mouse interrupts wait (see the serial chapter for how long a run can be
before received bytes are lost). Palette and SCB changes take effect at the
start of the next frame rather than mid-frame, so a fade can lag its pixels by
one frame but never tears.
=== A Portability Checklist
@ -370,7 +371,11 @@ hashes are DOS's -- and the rest behaves like this:
time, at the next `jlAudioFrameTick`, because DVX's sound library may not
be called from an interrupt.
* *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes.
DVX's keyboard joystick is not reported as one.
Two sticks on one game port (a Y cable, or a four-axis stick in DVX's
default two-stick mode) are joysticks 0 and 1. An axis that was never
calibrated sets its range as it moves, so until it is calibrated a full
push can read a little short of 127. DVX's keyboard joystick is not
reported as one.
* *Serial ports* belong to DVX's serial library, one program each. As on
every port, closing one leaves DTR as it is.
* A program's `main` is compiled as `jlGameMain`, which the port calls with

View file

@ -23,7 +23,9 @@
#define FRAME_SIZE 80
#define FRAME_INSET 2
#define DOT_SIZE 8
#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2)
// A full push puts the dot against the frame's inside edge, never on the
// frame, which erasing it would notch.
#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2 - FRAME_INSET)
#define BUTTON_W 24
#define BUTTON_H 16
@ -32,21 +34,15 @@
#define STATUS_W 80
#define STATUS_H 4
#define ROW_GAP 6
#define STICK_TOP 16
#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + 6)
#define STATUS_TOP (BUTTON_TOP + BUTTON_H + 6)
#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + ROW_GAP)
#define STATUS_TOP (BUTTON_TOP + BUTTON_H + ROW_GAP)
#define STICK0_LEFT 24
#define STICK1_LEFT 216
static void buildPalette(jlSurfaceT *screen);
static void drawFrame(jlSurfaceT *screen, int16_t left);
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
static int16_t dotXFor(int16_t left, int8_t ax);
static int16_t dotYFor(int8_t ay);
static int16_t buttonXFor(int16_t left, int idx);
static void initialPaint(jlSurfaceT *screen);
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
#define CODEGEN_BYTES (8u * 1024u)
typedef struct {
int16_t dotX;
@ -56,6 +52,15 @@ typedef struct {
bool valid;
} StickViewT;
static void buildPalette(jlSurfaceT *screen);
static int16_t buttonXFor(int16_t left, int16_t idx);
static int16_t dotXFor(int16_t left, int8_t ax);
static int16_t dotYFor(int8_t ay);
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
static void drawFrame(jlSurfaceT *screen, int16_t left);
static void initialPaint(jlSurfaceT *screen);
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
static StickViewT gView[JOYSTICK_COUNT];
@ -79,27 +84,14 @@ static void buildPalette(jlSurfaceT *screen) {
}
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
/* jlFillRect marks the rect dirty; jlStagePresent flushes only that
* dirty band. */
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
jlStagePresent();
}
static int16_t buttonXFor(int16_t left, int16_t idx) {
int16_t centerX;
// Draw the static stick frame (just an outlined square -- four edges
// with the inset filled background).
static void drawFrame(jlSurfaceT *screen, int16_t left) {
int16_t top;
top = STICK_TOP;
jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
jlFillRect(screen,
(int16_t)(left + FRAME_INSET),
(int16_t)(top + FRAME_INSET),
(uint16_t)(FRAME_SIZE - 2 * FRAME_INSET),
(uint16_t)(FRAME_SIZE - 2 * FRAME_INSET),
COLOR_BACKGROUND);
centerX = (int16_t)(left + FRAME_SIZE / 2);
if (idx == 0) {
return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2);
}
return (int16_t)(centerX + BUTTON_GAP / 2);
}
@ -123,23 +115,30 @@ static int16_t dotYFor(int8_t ay) {
}
static int16_t buttonXFor(int16_t left, int idx) {
int16_t centerX;
// jlFillRect marks the rect dirty; jlStagePresent flushes only that band.
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
jlStagePresent();
}
centerX = (int16_t)(left + FRAME_SIZE / 2);
if (idx == 0) {
return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2);
}
return (int16_t)(centerX + BUTTON_GAP / 2);
// Draw the static stick frame (just an outlined square -- four edges
// with the inset filled background).
static void drawFrame(jlSurfaceT *screen, int16_t left) {
int16_t top;
top = STICK_TOP;
jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
jlFillRect(screen, (int16_t)(left + FRAME_INSET), (int16_t)(top + FRAME_INSET), (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), COLOR_BACKGROUND);
}
static void initialPaint(jlSurfaceT *screen) {
int16_t i;
int16_t left;
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (i = 0; i < JOYSTICK_COUNT; i++) {
int16_t left;
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
drawFrame(screen, left);
gView[i].valid = false;
@ -174,27 +173,20 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
}
if (!v->valid || v->connected != connected) {
drawAndPresent(screen,
(int16_t)(left + (FRAME_SIZE - STATUS_W) / 2),
STATUS_TOP,
STATUS_W, STATUS_H,
connected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
drawAndPresent(screen, (int16_t)(left + (FRAME_SIZE - STATUS_W) / 2), STATUS_TOP, STATUS_W, STATUS_H, connected ? COLOR_CONNECTED : COLOR_DISCONNECTED);
v->connected = connected;
}
// Move the position dot: erase the old square (paint background),
// then stamp the new one. If position didn't change, skip.
// then stamp the new one. The first pass only stamps. If the position
// didn't change, skip.
if (!v->valid || v->dotX != newDotX || v->dotY != newDotY) {
if (v->valid) {
drawAndPresent(screen, v->dotX, v->dotY, DOT_SIZE, DOT_SIZE, COLOR_BACKGROUND);
}
drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE,
(ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF);
drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, (ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF);
v->dotX = newDotX;
v->dotY = newDotY;
} else if (!v->valid) {
// First paint of a centered dot: still need to draw it once.
drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, COLOR_DOT_OFF);
}
// Button squares.
@ -204,8 +196,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
continue;
}
bx = buttonXFor(left, i);
drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H,
newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF);
drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF);
v->btn[i] = newBtn[i];
}
@ -217,7 +208,7 @@ int main(void) {
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.codegenBytes = CODEGEN_BYTES;
if (!jlInit(&config)) {
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
// neither side disturbs the other.
//
// Serial: the default port at 9600 8N1, both directions at once.
// Serial: the default port at LINE_BAUD (9600 unless built otherwise) 8N1,
// both directions at once.
// - Transmits an endless stream, byte k = streamByte(k, TX_STREAM_MASK),
// STREAM_CHUNK bytes per loop.
// - Checks the peer's endless stream in order, byte k = streamByte(k,
@ -22,6 +23,13 @@
//
// Mouse: a small pointer follows it.
//
// Built with -DDENSE_SCB_LINES=n, stage lines 0 .. n-1 alternate between two
// palettes, a palette change on every one of them. A port that honours the SCB
// by reloading its one hardware palette as the beam scans (the X68000's raster
// interrupt) then holds that interrupt for the whole run of lines every frame,
// and the serial checks show whether receive survives it. The default, 0, is
// one palette and no changes.
//
// The bottom two stage rows are verification scanlines: one nibble per pixel,
// high nibble first, behind a sentinel, for a harness that reads the
// framebuffer back:
@ -37,7 +45,12 @@
#include <joey/joey.h>
#include <joey/serial.h>
#ifndef LINE_BAUD
#define LINE_BAUD 9600u
#endif
#ifndef DENSE_SCB_LINES
#define DENSE_SCB_LINES 0u
#endif
#define LINE_DATA_BITS 8u
#define LINE_STOP_BITS 1u
@ -86,6 +99,9 @@
#define COLOR_POINTER 15
#define PALETTE_RED_SHIFT 8
#define PALETTE_GREEN_SHIFT 4
#define GREY_PALETTE 0u
#define BLUE_PALETTE 1u // the dense SCB's other palette
#define BLUE_FLOOR 0x00Fu // added to every colour but black
typedef struct StreamStatsT {
uint32_t received; // peer bytes read so far = index of the next one expected
@ -108,6 +124,7 @@ typedef struct PointerT {
} PointerT;
static void buildPalette(jlSurfaceT *screen);
static void buildScb(jlSurfaceT *screen);
static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color);
static uint16_t mouseFlags(void);
static void movePointer(jlSurfaceT *screen, PointerT *pointer);
@ -127,7 +144,30 @@ static void buildPalette(jlSurfaceT *screen) {
for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i);
}
jlPaletteSet(screen, 0, colors);
jlPaletteSet(screen, GREY_PALETTE, colors);
}
// The grey palette everywhere, or DENSE_SCB_LINES lines alternating between
// it and the same ramp in blue.
static void buildScb(jlSurfaceT *screen) {
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, GREY_PALETTE);
#if DENSE_SCB_LINES > 0
{
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t i;
uint16_t line;
jlPaletteGet(screen, GREY_PALETTE, colors);
for (i = 1u; i < SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR);
}
jlPaletteSet(screen, BLUE_PALETTE, colors);
for (line = 1u; line < DENSE_SCB_LINES; line += 2u) {
jlScbSet(screen, line, BLUE_PALETTE);
}
}
#endif
}
@ -295,7 +335,7 @@ int main(void) {
}
screen = jlStageGet();
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
buildScb(screen);
jlSurfaceClear(screen, COLOR_BACKGROUND);
line.baud = LINE_BAUD;

View file

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

View file

@ -27,6 +27,15 @@
#define ST_TICK_HZ 30u
#define ST_MAX_CATCHUP_TICKS 4u
// The C64 runs the game every other frame but the level intro ($4666)
// every frame.
#define ST_C64_FRAMES_PER_TICK 2u
// The intro's loop count ($4508) runs 0..3 and the cab's judder ($470D)
// steps -2 into count 0; batches that cannot show every frame end on the
// counts either side of that jump (runIntro).
#define ST_INTRO_LOOP_MASK 3u
#define ST_INTRO_SHOW_BEFORE 3u
#define ST_INTRO_SHOW_AFTER 0u
// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which
// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are
// levels/level01.dat .. level25.dat, so Y is index 24.
@ -85,6 +94,8 @@ typedef struct {
bool fireArmed; // $445D: FIRE must be released first
uint8_t lastFrame;
uint16_t paceAcc;
uint16_t frameAcc; // C64 frames owed, in paceAcc's units
int8_t introOwed; // intro frames owed to (+) or run ahead of (-) the next batch
StStateE afterJingle; // where ST_STATE_JINGLE returns to
bool gameOverTune; // $5C24: song 5 started after the wait
uint8_t hsSlot; // $4D51 table slot being named
@ -189,11 +200,11 @@ static bool joyFire(void);
static uint8_t joyMask(void);
static bool loadLevel(uint8_t index);
static uint8_t nextLevelForShift(void);
static uint8_t paceTicks(void);
static uint8_t paceTicks(uint8_t *c64Frames);
static bool serviceJingle(void);
static void runCabbiesMenu(uint8_t ticks);
static void runDemoEnd(void);
static void runIntro(uint8_t ticks);
static void runIntro(uint8_t c64Frames);
static void runNameEntry(uint8_t ticks);
static bool nameEntryStart(void);
static void runPlaying(uint8_t ticks);
@ -604,22 +615,35 @@ static uint8_t nextLevelForShift(void) {
// Game ticks due this rendered frame, from the vertical blank count:
// one tick per two frames on 50/60 Hz displays, three per seven on
// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
// length just clamps to the catch-up cap.
static uint8_t paceTicks(void) {
// length just clamps to the catch-up cap. *c64Frames gets the C64 frames
// due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at
// 50/60 Hz, so the level intro shows every frame the C64 does -- the
// cab's up-and-down judder ($470D) is a one-frame pattern that stepping
// two frames at a time cancels out.
static uint8_t paceTicks(uint8_t *c64Frames) {
uint16_t hz = jlFrameHz();
uint8_t now = (uint8_t)jlFrameCount();
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
uint8_t ticks = 0u;
uint16_t perTick = ST_TICK_HZ;
gGame.lastFrame = now;
if (frames > 16u) {
frames = 16u;
}
if (hz == 50u || hz == 60u) {
gGame.paceAcc += (uint16_t)(frames * 35u);
perTick = 35u;
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
// the next ones); a stall longer than the cap's worth is forgiven.
@ -674,15 +698,42 @@ static void runDemoEnd(void) {
}
// $4666 loop: two frames per game tick; joystick ends a demo.
static void runIntro(uint8_t ticks) {
// $4666 loop: once per C64 frame; joystick ends a demo. A port that shows
// every frame shows the cab's one-frame judder ($470D) as the C64 does. One
// that shows only some would sample it at a fixed phase -- every second frame
// shows -1, 0, -1, 0, a smooth climb -- so when two or more frames are due
// the batch is cut or stretched by one (evened out on the next batch) to end
// on the loop counts either side of the -2 jump: the frames shown then cross
// it and the cab judders, at the same average speed.
static void runIntro(uint8_t c64Frames) {
static const int8_t kTry[] = { 0, -1, 1 };
int16_t due;
uint8_t steps;
uint8_t end;
uint8_t t;
uint8_t k;
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
runDemoEnd();
return;
}
for (k = 0u; k < (uint8_t)(ticks * 2u); k++) {
due = (int16_t)(c64Frames + gGame.introOwed);
gGame.introOwed = 0;
steps = (uint8_t)((due > 0) ? due : 0);
if (c64Frames >= ST_C64_FRAMES_PER_TICK) {
for (t = 0u; t < (uint8_t)sizeof(kTry); t++) {
if (due + kTry[t] < 1) {
continue;
}
end = (uint8_t)((gSim.introLoopCount + due + kTry[t]) & ST_INTRO_LOOP_MASK);
if (end == ST_INTRO_SHOW_BEFORE || end == ST_INTRO_SHOW_AFTER) {
steps = (uint8_t)(due + kTry[t]);
gGame.introOwed = (int8_t)(-kTry[t]);
break;
}
}
}
for (k = 0u; k < steps; k++) {
if (stIntroStep(&gSim)) {
jlMusicStop(); // $4709
stSimEnterLevel(&gSim, &gLevel);
@ -930,6 +981,7 @@ static void startIntro(void) {
// $459C: the intro's tune plays under the fly-in, not blocking; the
// C64 leaves it to the IRQ player and stops it at $4709.
stAudioMusic(ST_SONG_INTRO, true);
gGame.introOwed = 0;
gGame.state = ST_STATE_INTRO;
}
@ -1018,6 +1070,7 @@ int main(void) {
for (;;) {
uint8_t ticks;
uint8_t c64Frames;
if (gGame.fatal) {
fatalHold();
@ -1045,6 +1098,7 @@ int main(void) {
gGame.paused = false;
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
gGame.paceAcc = 0u;
gGame.frameAcc = 0u;
if (gGame.state == ST_STATE_INTRO) {
stAudioMusic(ST_SONG_INTRO, true); // $46B5
}
@ -1058,7 +1112,7 @@ int main(void) {
enterTitle();
}
}
ticks = paceTicks();
ticks = paceTicks(&c64Frames);
switch (gGame.state) {
case ST_STATE_TITLE:
runTitle(ticks);
@ -1090,7 +1144,7 @@ int main(void) {
}
break;
case ST_STATE_INTRO:
runIntro(ticks);
runIntro(c64Frames);
break;
case ST_STATE_PLAYING:
runPlaying(ticks);

View file

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

View file

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

View file

@ -37,10 +37,10 @@
// The IIgs reaches its globals DBR-relative, so all of BSS must fit the
// entry bank below the I/O window; the caches are sized down there. The
// sprite cache must still hold every cel stRenderPrewarm builds (the
// cab, exhaust and passenger sets plus the intro star) or a cel change
// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped
// by character, so the count is a power of two.
// Room for the baked cab/passenger/exhaust set (37) plus the largest
// cab, exhaust and passenger sets) or a cel change mid-play rebuilds
// and recompiles a sprite. Glyphs are direct-mapped by character, so
// the count is a power of two.
// Room for the baked cab/passenger/exhaust set (36) plus the largest
// level's hook cels (I: 30) at once.
#define ST_SPRITE_CACHE 80u
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES)
@ -66,6 +66,10 @@ typedef struct {
uint8_t mc0;
uint8_t mc1;
uint8_t level; // level index the bitmap came from (level sprites)
uint8_t offX; // where the (cropped) cel sits in the 24x24
uint8_t offY; // VIC frame, in pixels -- see celCreate
uint8_t w; // the cel's size in pixels
uint8_t h;
bool used;
bool pinned; // prewarmed moving cel: never evicted (its
// compiled code is costly to re-emit on the 65816)
@ -78,6 +82,7 @@ typedef struct {
// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
typedef struct {
bool drawn;
bool trusted; // backed up by the compiled, unclipped save
uint16_t x;
uint8_t y;
uint8_t ptr;
@ -92,8 +97,7 @@ typedef struct {
// tile, animated by rewriting logoSlotMask[]'s palette slots.
bool logoMode;
bool starMode; // level intro: cycling the starfield
uint8_t starPhase; // which ring the bright band is on
uint8_t starTick; // frames until the band steps outward
uint8_t starPhase; // which step of every flight is lit
bool logoTileOk; // the live glyph is one of the frames
uint8_t logoSlotCount;
uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in
@ -136,22 +140,18 @@ static const uint16_t kC64Palette[16] = {
// four of these are used; the rest are headroom if the frames change.
static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u };
// Level-intro warp: how a star ring looks at each distance BEHIND the
// bright band, as C64 colour indices (white, light grey, grey, dark
// grey, then black). A ring this far behind the band or further is
// unlit, so a lit head with a short fading tail sweeps outward and the
// rest of the field stays dark.
static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u };
// Level-intro warp: how a star step looks at each distance BEHIND the
// lit step, as C64 colour indices; further behind is unlit. One yellow
// star per ray, as the C64's star sprites (colour 7, $4523) fly with no
// trail.
static const uint8_t kStarRamp[] = { 7u };
#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0]))
// Frames per ring step. The intro runs at the game's 30 Hz tick, so a
// step every other frame sweeps the eight rings in about half a second.
#define ST_STAR_STEP_FRAMES 2u
static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx);
static void cacheCompile(StSpriteCacheT *e);
static StSpriteCacheT *cacheFreeSlot(void);
static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
static void levelCelsEvict(void);
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
@ -173,26 +173,7 @@ static bool prewarmReport(uint16_t expect);
static bool spriteOnScreen(int16_t px, int16_t py);
static void starPaletteApply(void);
static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
// Paint a VIC sprite bitmap onto the scratch surface's top-left 3x3
// tiles from the shared cel expansion (stCels.c, the same one the
// offline baker uses), so a cel built here for the interpreter fallback
// matches the precompiled .spc byte for byte.
static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) {
uint8_t blob[ST_CEL_BYTES];
jlTileT tile;
uint8_t tx;
uint8_t ty;
stCelBlob(bm, multi, color, mc0, mc1, blob);
for (ty = 0u; ty < ST_SPRITE_TILES; ty++) {
for (tx = 0u; tx < ST_SPRITE_TILES; tx++) {
tileFromChunky(&tile, &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]);
jlTilePaste(gRender.scratch, tx, ty, &tile);
}
}
}
static bool tileHasPixels(const uint8_t *chunky);
// Compile a cached cel if it is not already, backing off after a failure
@ -210,9 +191,10 @@ static void cacheCompile(StSpriteCacheT *e) {
}
// The JoeyLib sprite for hardware sprite `idx` of the current frame,
// built on first use from its VIC bitmap and colours.
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
// The cached cel for hardware sprite `idx` of the current frame, built
// on first use from its VIC bitmap and colours; NULL when there is
// nothing to draw.
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) {
uint8_t ptr = sim->frame.ptr[idx];
uint8_t color = drawColor(sim, idx);
uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
@ -227,14 +209,14 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
// Most frames a hardware sprite shows the cel it showed last time.
if (e != 0 && e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) {
e->lastUse = gRender.cacheStamp;
return e->sprite;
return e;
}
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
e = &gRender.cache[k];
if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) {
e->lastUse = gRender.cacheStamp;
gRender.lastHit[idx] = e;
return e->sprite;
return e;
}
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the
// costly-to-recompile ones and are never evicted; the least-
@ -264,9 +246,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
slot->used = false;
slot->compiled = false;
}
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1);
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) {
if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) {
return 0;
}
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a
@ -293,7 +273,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
slot->used = true;
slot->pinned = gPrewarming;
gRender.lastHit[idx] = slot;
return slot->sprite;
return slot;
}
@ -312,9 +292,61 @@ static const uint8_t kCellRow[125] = {
};
// Column of a cell whose row is known: cell - row * 40 through a table,
// because the two-shift form of the multiply came back as a __mulhi3
// library call on the 65816.
// Build a VIC bitmap's cel into a cache entry from the shared cel
// expansion (stCels.c, the same one the offline baker uses), cropped to
// the tiles that hold pixels. Most VIC sprites leave whole tiles of their
// 24x21 frame empty, and every one of those would otherwise be saved,
// drawn, restored and presented each time the sprite moves. A bitmap
// with no pixels keeps the whole frame.
//
// The tiles go onto the scratch surface last to first: on the planar
// ports tileFromChunky converts through the scratch's top-left tile, so
// that tile has to be the one written last.
static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) {
uint8_t blob[ST_CEL_BYTES];
jlTileT tile;
uint8_t txMin = ST_SPRITE_TILES;
uint8_t txMax = 0u;
uint8_t tyMin = ST_SPRITE_TILES;
uint8_t tyMax = 0u;
uint8_t tx;
uint8_t ty;
stCelBlob(bm, multi, color, mc0, mc1, blob);
ty = ST_SPRITE_TILES;
while (ty > 0u) {
ty--;
tx = ST_SPRITE_TILES;
while (tx > 0u) {
const uint8_t *chunk;
tx--;
chunk = &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES];
tileFromChunky(&tile, chunk);
jlTilePaste(gRender.scratch, tx, ty, &tile);
if (tileHasPixels(chunk)) {
txMin = (tx < txMin) ? tx : txMin;
txMax = (tx > txMax) ? tx : txMax;
tyMin = (ty < tyMin) ? ty : tyMin;
tyMax = (ty > tyMax) ? ty : tyMax;
}
}
}
if (txMin > txMax) {
txMin = 0u;
tyMin = 0u;
txMax = ST_SPRITE_TILES - 1u;
tyMax = ST_SPRITE_TILES - 1u;
}
e->offX = (uint8_t)(txMin * TILE_PIXELS_PER_SIDE);
e->offY = (uint8_t)(tyMin * TILE_PIXELS_PER_SIDE);
e->w = (uint8_t)((txMax - txMin + 1u) * TILE_PIXELS_PER_SIDE);
e->h = (uint8_t)((tyMax - tyMin + 1u) * TILE_PIXELS_PER_SIDE);
e->sprite = jlSpriteCreateFromSurface(gRender.scratch, (int16_t)e->offX, (int16_t)e->offY, (uint8_t)(e->w / TILE_PIXELS_PER_SIDE), (uint8_t)(e->h / TILE_PIXELS_PER_SIDE));
return e->sprite != 0;
}
// True for a cel that is MEANT to be built here rather than pre-listed,
// so the warning below stays about real misses. Only the title screen's
// decorations qualify: logoColorCycle recolours them every cycle step,
@ -335,6 +367,9 @@ static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) {
}
// Column of a cell whose row is known: cell - row * 40 through a table,
// because the two-shift form of the multiply came back as a __mulhi3
// library call on the 65816.
static uint8_t cellCol(uint16_t cell, uint8_t row) {
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) {
}
return;
}
// A plain loop, not memset: 25 entries do not pay for the 65816
// libc's far-called byte loop.
// One loop over both 25-entry arrays.
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
gRender.dirtyColMin[row] = 0xFFu;
gRender.dirtyColMax[row] = 0u;
@ -688,6 +722,19 @@ static void tileFromChunky(jlTileT *out, const uint8_t *chunky) {
}
// True when a chunky 8x8 tile has any non-transparent (non-zero) pixel.
static bool tileHasPixels(const uint8_t *chunky) {
uint8_t k;
for (k = 0u; k < TILE_BYTES; k++) {
if (chunky[k] != 0u) {
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
}
}
if (gRender.starMode) {
// The warp is the only thing animating here, and it animates in
// the palette: no cells change, no sprite moves for it, so the
// present that ends this frame copies no pixels at all.
gRender.starTick++;
if (gRender.starTick >= ST_STAR_STEP_FRAMES) {
gRender.starTick = 0u;
gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u));
// The warp animates in the palette: no cells change and no sprite
// moves for it. The lit step follows the intro's own frame count,
// so the stars keep the C64's speed whatever the frame rate.
uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
if (phase != gRender.starPhase) {
gRender.starPhase = phase;
starPaletteApply();
}
}
@ -752,7 +799,11 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = ST_HW_SPRITES; k > first; k--) {
StDrawnT *d = &gRender.slot[k - 1u];
if (d->drawn) {
if (d->trusted) {
jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]);
} else {
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
}
d->drawn = false;
}
}
@ -760,7 +811,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = first; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k];
jlSpriteT *sp;
StSpriteCacheT *e;
int16_t px;
int16_t py;
if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
if (!spriteOnScreen(px, py)) {
continue;
}
sp = cachedSprite(sim, idx);
if (sp == 0) {
e = cachedSprite(sim, idx);
if (e == 0) {
continue;
}
jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]);
px = (int16_t)(px + e->offX);
py = (int16_t)(py + e->offY);
// A compiled cel wholly on the stage takes the trusted calls,
// which skip the per-call geometry checks; one that is clipped
// or interpreted needs the checked ones, and so does its restore.
d->trusted = e->compiled && px >= 0 && py >= 0 && px <= (int16_t)(SURFACE_WIDTH - e->w) && py <= (int16_t)(SURFACE_HEIGHT - e->h);
if (d->trusted) {
jlSpriteSaveAndDrawTrusted(stage, e->sprite, px, py, &gRender.backup[k]);
} else {
jlSpriteSaveAndDraw(stage, e->sprite, px, py, &gRender.backup[k]);
}
d->drawn = true;
d->x = sim->frame.x[idx];
d->y = sim->frame.y[idx];
@ -824,14 +885,6 @@ void stRenderInit(jlSurfaceT *stage) {
}
// Build (and compile) the cels the game draws every screen -- cab,
// exhaust, passenger, wreck, warp, intro cab and star -- up front, so
// no frame pays for a sprite build mid-play. A bar on the stage shows
// progress on the slower ports.
// Load the offline-baked, already-compiled cel bank into the pinned
// cache so no cel is JIT-compiled at boot (each 65816 compile costs
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
// as the play lookups: system ptr (level 0xFF), the level-wide colours.
// The first unused cache slot, or NULL when the cache is full.
static StSpriteCacheT *cacheFreeSlot(void) {
uint8_t k;
@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
}
if (got == n) {
slot->sprite = cels[k];
slot->compiled = true;
slot->offX = 0u;
slot->offY = 0u;
slot->w = ST_SPRITE_PX;
slot->h = ST_SPRITE_PX;
slot->compiled = (jlSpriteCompiledSize(cels[k]) != 0u);
} else {
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
if (bm == 0) {
continue;
}
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1);
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) {
if (!celCreate(slot, bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1)) {
continue;
}
slot->compiled = false;
@ -975,6 +1030,10 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
}
// Load the offline-baked, already-compiled cel bank into the pinned
// cache so no cel is JIT-compiled at boot (each 65816 compile costs
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
// as the play lookups: system ptr (level 0xFF), the level-wide colours.
// Returns false if the .spc is missing or built for another target/shift
// count (a stale bake), so the caller falls back to the runtime JIT
// prewarm below. Every port stages its own bake (make/<port>.mk).
@ -1002,6 +1061,10 @@ static bool loadPrecompiledCels(void) {
e->mc0 = kStCels[i].mc0;
e->mc1 = kStCels[i].mc1;
e->level = 0xFFu;
e->offX = 0u;
e->offY = 0u;
e->w = ST_SPRITE_PX;
e->h = ST_SPRITE_PX;
e->used = true;
e->pinned = true;
e->lastUse = 0u;
@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) {
jlStagePresent();
}
// Build (and compile) the cels the game draws every screen -- cab,
// exhaust, passenger, wreck and intro cab -- up front, so no frame pays
// for a sprite build mid-play. A bar on the stage shows progress on the
// slower ports.
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 };
static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 };
static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 };
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u);
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs));
uint8_t done = 0u;
bool ok;
uint8_t k;
@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
done++;
loadingBar(stage, done, total);
}
// The intro star is a hires sprite in colour 7. Build it as a moving
// (idx <= 2) cel so it compiles: the level-name intro draws seven of
// them every frame, and the interpreter cannot keep up.
sim->frame.multiMask = 0u;
sim->frame.ptr[2] = 0xDAu;
sim->frame.color[2] = 0x07u;
(void)cachedSprite(sim, 2u);
sim->frame = saved;
gPrewarming = false;
ok = prewarmReport(total);
@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *sim) {
// Turn on the level intro's palette-cycled starfield. Call it after
// stRenderSceneChanged (which turns it back off) on entry to the level
// intro. The cells were painted by stIntroEnter; from here the warp is
// one palette write every ST_STAR_STEP_FRAMES frames.
// one palette write every ST_STAR_FRAMES_PER_STEP intro frames.
void stRenderIntroStars(void) {
gRender.starMode = true;
gRender.starPhase = 0u;
gRender.starTick = 0u;
starPaletteApply();
}

View file

@ -100,8 +100,9 @@
// pixel rows.
//
// The contract between the two halves is just this colour range:
// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is
// the centre), and the renderer cycles that range. These are C64 colour
// stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its
// ray's flight (offset by the ray's launch delay), and the renderer
// cycles that range. These are C64 colour
// indices the intro screen never uses -- it draws in 0, 3 and, in demo
// mode, 4 and 5; the cab sprite adds 2, 6 and 7.
#define ST_STAR_RING_FIRST 8u
@ -112,12 +113,14 @@
// something else must fail to compile here instead of quietly making
// the warp skip rings.
typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1];
// Ring 0 is the centre, which in a warp is the VANISHING POINT: those
// stars are the far ones and get the small glyph. The big glyph belongs
// to the outer rings, the stars about to rush past the viewer.
#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings
#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings
#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant
// A C64 star flies $20 frames ($4500) and the painted field marks every
// ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on
// that many intro frames (introStepCount): the warp keeps the C64's speed at
// any frame rate.
#define ST_INTRO_STAR_FLIGHT 0x20u
#define ST_STAR_FRAMES_PER_STEP (ST_INTRO_STAR_FLIGHT / ST_STAR_RING_COUNT)
// Every star is the small glyph: the C64's star is a 5x3 speck.
#define ST_STAR_GLYPH 0x2Eu // '.'
// VIC sprite block pointers the game selects by name. The cab's bit 0 is
// the landing gear, so each facing is a pair.
@ -309,7 +312,11 @@ typedef struct {
uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
} StLevelT;
// One VIC sprite as the game sees it through its shadow tables.
// One VIC sprite as the game sees it through its shadow tables. Padded
// to eight bytes: the 65816 compiler indexes a 6-byte element through
// a call to its 32-bit multiply routine (sim is a far pointer), and
// every sim->spr[idx] in the game paid for one; a power-of-two stride
// is three shifts.
typedef struct {
uint8_t col; // $7175,X X low byte
uint8_t msb; // $7185,X X high bit (any non-zero value = set)
@ -317,7 +324,9 @@ typedef struct {
uint8_t enable; // $718E,X
uint8_t ptr; // $7197,X block pointer
uint8_t color; // $719F,X
uint8_t strideToPow2[2];
} StSpriteT;
typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1];
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the
// renderer draws this and the collision test evaluates it.
@ -542,6 +551,7 @@ typedef struct {
uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6
uint8_t introLoopCount; // $4508
uint8_t introStepCount; // port: intro frames run, for the painted warp
uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B
uint8_t spriteMc0; // $D025

View file

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

View file

@ -170,6 +170,7 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
uint16_t now;
uint32_t startMillis;
uint32_t endMillis;
uint16_t afterFirst;
bool slowOp;
count = 0UL;
@ -178,13 +179,20 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
startFrame = jlFrameCount();
startMillis = jlMillisElapsed();
// Calibrate: jlWaitVBL aligned us to a tick edge, so a sub-frame op
// cannot see the counter advance during a single call. If the first
// call DOES advance it, the op is slower than one frame -- poll the
// clock every call so a batch cannot overrun the window.
// Calibrate: an op slower than one frame advances the counter on
// every call -- poll the clock every call then, so a batch cannot
// overrun the window. One advance proves nothing: jlWaitVBL syncs to
// the display's retrace, but a port may count frames from another
// clock (DOS derives them from its millisecond clock), so a fast op's
// first call can straddle a frame edge, and polling every call of a
// fast op costs it up to 40% (rows on DOS came out 0.6x at random). A
// sub-half-frame op cannot advance the counter on two calls running.
op();
count++;
slowOp = ((uint16_t)(jlFrameCount() - startFrame) != 0u);
afterFirst = jlFrameCount();
op();
count++;
slowOp = (afterFirst != startFrame) && (jlFrameCount() != afterFirst);
for (;;) {
now = jlFrameCount();
@ -1032,6 +1040,84 @@ static void PROBE_ATTR op_probeVgaCheck(void) {
#endif
#ifdef JOEYLIB_PLATFORM_DVX
extern uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
// P13: DVX display-side parity, P12's battery on the DVX present, which
// expands dirty rows through per-palette tables into the window's content
// buffer (the desktop's pixel format, at the window's scale) or the
// full-screen backbuffer. The golden hashes gate the stage, never those.
// dvxPortProbeDisplayMismatch derives every pixel from the stage's own
// palette entries (not the present's tables) and counts the ones that
// differ. A strip of one-pixel stripes in all 16 colours stays on screen,
// so every table entry, and both pixels of a byte, are checked; the small
// rect is on odd pixels. After P12's steps (tall band, small inner rect,
// settle, idle) two more work the tables: the strip's rows moved to another
// palette (an SCB change: every row again), then that palette changed
// (its rows again; on the whole screen at 8 bits only the screen's
// palette); then the SCBs and the palette go back. Every count must be 0.
#define PROBE_DVX_PALETTE 1u
#define PROBE_DVX_STRIP_H 10
#define PROBE_DVX_SCB_LAST 99u
static void PROBE_ATTR op_probeDvxDisplayCheck(void) {
static const uint16_t probePalette[SURFACE_COLORS_PER_PALETTE] = {
0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF,
0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444
};
static const uint16_t changedPalette[SURFACE_COLORS_PER_PALETTE] = {
0x000, 0x123, 0x246, 0x369, 0x48C, 0x5AF, 0x6C2, 0x7E5,
0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8
};
uint8_t savedScb[SURFACE_HEIGHT];
uint16_t savedPalette[SURFACE_COLORS_PER_PALETTE];
uint32_t afterB;
uint32_t afterSettle;
uint32_t afterIdle;
uint32_t afterScb;
uint32_t afterPalette;
uint32_t afterRestore;
uint32_t compared;
int16_t x;
uint16_t y;
for (y = 0; y < SURFACE_HEIGHT; y++) {
savedScb[y] = jlScbGet(gStage, y);
}
jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette);
jlSurfaceClear(gStage, 3);
for (x = 0; x < SURFACE_WIDTH; x++) {
jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % SURFACE_COLORS_PER_PALETTE));
}
jlStagePresent();
jlFillRect(gStage, 0, 50, 320, 100, 3);
jlStagePresent();
jlFillRect(gStage, 101, 61, 37, 9, 9);
jlStagePresent();
afterB = dvxPortProbeDisplayMismatch(gStage, &compared);
jlStagePresent();
afterSettle = dvxPortProbeDisplayMismatch(gStage, &compared);
jlStagePresent();
afterIdle = dvxPortProbeDisplayMismatch(gStage, &compared);
jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE);
jlStagePresent();
afterScb = dvxPortProbeDisplayMismatch(gStage, &compared);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette);
jlStagePresent();
afterPalette = dvxPortProbeDisplayMismatch(gStage, &compared);
for (y = 0; y < SURFACE_HEIGHT; y++) {
jlScbSet(gStage, y, savedScb[y]);
}
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlStagePresent();
afterRestore = dvxPortProbeDisplayMismatch(gStage, &compared);
jlLogF("UBER-PROBE: dvxDisplayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterRestore=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterRestore, (unsigned long)compared);
}
#endif
#if defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_IIGS)
// P7-4 (perf-hunt-2 rank 4): gameFrameClean component split. The
// golden row = 3 copyRect window erases + 3 sprite draws + present;
@ -1928,6 +2014,9 @@ static void __attribute__((noinline)) runAllTests(void) {
#ifdef JOEYLIB_PLATFORM_DOS
op_probeVgaCheck();
#endif
#ifdef JOEYLIB_PLATFORM_DVX
op_probeDvxDisplayCheck();
#endif
#endif
jlLogF("UBER: ----- end -----");

View file

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

View file

@ -78,20 +78,11 @@
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
#elif defined(JOEYLIB_PLATFORM_X68000)
// BRING-UP STATE: chunky stage in main RAM, expanded to GVRAM by jlpPresent.
// This is deliberately the slow-but-correct path -- it makes every one of the
// 772 lines of src/generic work unmodified, so the port renders correctly
// before a single native primitive exists.
//
// The native storage model is NOT yet decided. The two candidates are GVRAM
// (2 bytes/pixel, trivial primitives) and the text plane at $E00000 (four
// 1bpp planes 128 KB apart -- byte-for-byte the Amiga layout with stride 40
// replaced by 128, so src/amiga adapts by changing one constant). Measured
// under MAME with the wait-state patch: TVRAM costs 1.92 cycles/access
// against GVRAM's 1.03, but needs only 0.25 word-writes per 16-colour pixel
// against GVRAM's 1.0 -- so the text plane still wins ~2x. See
// claudeDocs/x68000_mame_crtc_timing.md. Do not switch to JOEYLIB_NATIVE_PLANAR
// until that is settled and measured.
// The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c,
// the Amiga's layout), and a present copies its dirty rows into the text
// plane at $E00000, which is four 1bpp planes too. The text plane has one
// 16-colour palette, so per-line SCB palettes are reloaded as the beam scans
// by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s).
#define JOEYLIB_CPU_68000
#define JOEYLIB_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR
@ -136,9 +127,9 @@
#define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else
#define JL_HAS_TILE_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else
#define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro
#define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: inline word copy for
// short spans (llvm-mos memcpy is a slow far
// byte-loop, finding #79), function
#define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: iigsCopyRectRows
// [dp],y row copies (short rows lose to
// memcpy's per-call setup), function
#define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro
#define JL_HAS_SPRITE_SAVE // noop macro (codegen path saves)
#define JL_HAS_SPRITE_RESTORE // noop macro

View file

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

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).
TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk
.PHONY: all atarist clean-atarist clean
# The dirty-band marker's on-target check: tests/host/dirtyMarkHost.c against
# src/m68k/stageBands.s, the asm the Amiga and X68000 share. Not a shipped
# program, so it builds beside bin/; scripts/verify-atarist-dirtymark.sh runs
# it under Hatari. The host check runs 200,000 scenarios; this many takes
# under a minute of emulated time.
DIRTYMARK_SRC := $(REPO_DIR)/tests/host/dirtyMarkHost.c
DIRTYMARK_BIN := $(BUILD)/test/DIRTYMRK.PRG
DIRTYMARK_SCENARIOS := 4000ul
DIRTYMARK_LOG := DIRTYMRK.TXT
.PHONY: all atarist clean-atarist clean atarist-dirtymark atarist-verify-dirtymark
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@ -224,6 +234,15 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
$(DIRTYMARK_BIN): $(DIRTYMARK_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MARK_LOG='"$(DIRTYMARK_LOG)"' $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
atarist-dirtymark: $(DIRTYMARK_BIN)
atarist-verify-dirtymark: $(DIRTYMARK_BIN)
$(REPO_DIR)/scripts/verify-atarist-dirtymark.sh
$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
@ -271,10 +290,10 @@ $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN)
@mkdir -p $(dir $@)
$(SPRITEBAKE_BIN) $< $@
$(SPRITEBAKE_BIN):
$(SPRITEBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-atarist
$(STAXIBAKE_BIN):
$(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
# The cel bank is generated from the game's cel table (stCels.c).

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);
# make/iigs.mk, which does not use COMMON_CFLAGS, honours it too.
COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 $(EXTRA_CFLAGS)
# Rules that hand a host tool's build to make/tools.mk depend on FORCE, so
# tools.mk -- which owns each tool's source list -- is always asked; it does
# nothing when the tool is current. Without it an existing tool binary looked
# up to date forever, and edits to its sources never reached it (staxibake
# kept running a stale binary after stCels.c changed). Defining it must not
# change any port's default goal (most include this file before their `all`),
# so the goal in force is saved and put back around it.
COMMON_DEFAULT_GOAL := $(.DEFAULT_GOAL)
.PHONY: FORCE
FORCE:
.DEFAULT_GOAL := $(COMMON_DEFAULT_GOAL)

View file

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

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
# UBER's probe build (part of all; scripts/verify-dvx-display.sh): UBER
# with its UBER_PROBE rows, the display parity row among them, as a program
# of its own packaged apart in build/dvx/probe, so neither bin/ (joey-run's
# tree) nor the goldened UBER changes. -DUBER_FRAMES=1 as the IIgs probe
# build: the run is wanted for its probe rows, not its timings.
# (Its name, like every program's, is a DOS 8.3 name: DVX runs on DOS.)
uberprob_SRCS := $(uber_SRCS)
uberprob_TITLE := Uber Probe
uberprob_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder.
uberprob_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1
uberprob_BIN := $(BUILD)/probe
PROBE_APPS := uberprob
# Data the examples read from DATA/ beside the program: AUDIO its music and
# sound effect; TILEBANK the DOS bake of the tile-bank fixture (the DOS
# target byte: DVX shows the DOS stage), every other target's bake it must
@ -143,14 +156,16 @@ AUDIO_DATA_FILES := $(audio_DATA)/test.mod $(audio_DATA)/test.sfx
TILEBANK_OWN := $(tilebank_DATA)/tilebank/own.tbk
DOS_STAXI_DATA := $(REPO_DIR)/build/dos/bin/STAXI/DATA
.PHONY: all dvx dvx-lib dvx-apps clean-dvx clean $(addprefix app-,$(APPS))
.PHONY: all dvx dvx-lib dvx-apps dvx-probe clean-dvx clean $(addprefix app-,$(APPS) $(PROBE_APPS))
all dvx: dvx-lib dvx-apps
all dvx: dvx-lib dvx-apps dvx-probe
dvx-lib: $(LIB)
dvx-apps: $(addprefix app-,$(APPS))
dvx-probe: $(addprefix app-,$(PROBE_APPS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@)
$(DOS_CC) $(LIB_CFLAGS) -c $< -o $@
@ -181,7 +196,8 @@ $(LIB): $(LIB_OBJS)
# then let sdk.mk build and package it, and copy the package into bin/.
# sdk.mk rebuilds only what changed; the project files are rewritten only
# when their contents change, so an unchanged program is not relinked.
# $(1) = the program's name.
# $(1) = the program's name; $(1)_CFLAGS, if any, are added to its flags,
# and $(1)_BIN, if set, is where it goes instead of bin/.
define appRule
app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
@mkdir -p $(APPSDIR)/$(1)
@ -194,7 +210,7 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
'VENDOR = $(VENDOR)' \
'LIBS = $(LIB)' \
'DATA = $$($(1)_DATA)' \
"CFLAGS = $(APP_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \
"CFLAGS = $(APP_CFLAGS) $$($(1)_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \
'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/Makefile
@printf '%s\n' \
'.topic $(1).overview' \
@ -217,11 +233,11 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
'description text "JoeyLib example: $$($(1)_TITLE)"' \
| $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res
$$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package
@mkdir -p $(BINDIR)
rm -rf $(BINDIR)/apps/$(VENDOR)/$(1)
cp -R $(APPSDIR)/$(1)/pkg/apps $(BINDIR)/
@mkdir -p $$(or $$($(1)_BIN),$(BINDIR))
rm -rf $$(or $$($(1)_BIN),$(BINDIR))/apps/$(VENDOR)/$(1)
cp -R $(APPSDIR)/$(1)/pkg/apps $$(or $$($(1)_BIN),$(BINDIR))/
endef
$(foreach a,$(APPS),$(eval $(call appRule,$(a))))
$(foreach a,$(APPS) $(PROBE_APPS),$(eval $(call appRule,$(a))))
.PHONY: audio-data tilebank-data staxi-data
audio-data: $(AUDIO_DATA_FILES)

View file

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

View file

@ -39,8 +39,9 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
LDFLAGS := -lm
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
# Raw interrupt shims (timer tick, SCB raster, SCC receive): each saves every
# register and returns with RTE, which a C function cannot.
# Raw interrupt shims (timer tick, frame count, SCB raster, SCC receive): each
# saves every register and returns with RTE, which a C function cannot. And
# the present's copy (x68kPresent.s).
PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
# Sprite codegen is the shared 68k PLANAR emitter, unchanged. Its
@ -57,6 +58,7 @@ LIB_OBJS := \
$(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(BUILD)/obj/68k/planarFill.o \
$(BUILD)/obj/68k/planarPlot.o \
$(BUILD)/obj/68k/stageBands.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/codegen/spriteCompile.o \
$(BUILD)/obj/codegen/spriteStage.o
@ -133,7 +135,7 @@ STAXI_DIR := $(BINDIR)/STAXI
STAXI_BIN := $(STAXI_DIR)/STAXI.X
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c
.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean
.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-serial-scb x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean
all x68000: x68000-lib x68000-examples
@ -259,10 +261,10 @@ $(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(STAXI_GEN_DIR)/levels
python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels
$(SPRITEBAKE_BIN):
$(SPRITEBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000
$(STAXIBAKE_BIN):
$(STAXIBAKE_BIN): FORCE
$(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake
$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c
@ -294,6 +296,11 @@ x68000-verify-mouse: $(BINDIR)/KEYS.X
x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X
$(REPO_DIR)/scripts/verify-x68000-serial-mouse.sh
# Serial + mouse under per-line palette changes: the serial + mouse gate with
# examples/sermouse built for a dense SCB at 9600, 19200 and 38400 (~3 min).
x68000-verify-serial-scb: $(LIB) $(LIBXMP_AR)
$(REPO_DIR)/scripts/verify-x68000-serial-scb.sh
# Joystick gate: every direction, both triggers and every release on both
# ports, held through MAME's pads and read back from JOY.X's display (~10 s).
x68000-verify-joy: $(BINDIR)/JOY.X

View file

@ -18,8 +18,15 @@
# 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was
# set: the same hashes, the full-screen present's timings.
#
# --probe runs UBER's probe build instead (make dvx builds it: its UBER_PROBE
# rows, the display parity row among them, at one frame per timed op), its
# log copied to build/dvx/probe/joeylog.txt; scripts/verify-dvx-display.sh
# judges it. Either way DVX.LOG is copied beside the log, as dvx.log. The
# desktop is dvxGate.sh's (DVX_VIDEO_W/H/BPP).
#
# scripts/bench-dvx.sh # default 600 s timeout
# scripts/bench-dvx.sh --full-screen # on the whole screen
# scripts/bench-dvx.sh --probe # the probe build (with --full-screen too)
# TIMEOUT=900 scripts/bench-dvx.sh # override
set -euo pipefail
@ -31,19 +38,35 @@ joeyNeed DOSBOX dosbox
joeyNeed DVX_ROOT dvx
fullScreen=0
if [ "${1:-}" = "--full-screen" ]; then
program=uber
appDir=$(dvxAppDir uber)
out=$DVX_BIN/$DVX_LOG_NAME
for arg in "$@"; do
case "$arg" in
--full-screen)
fullScreen=1
elif [ $# -gt 0 ]; then
echo "usage: scripts/bench-dvx.sh [--full-screen]" >&2
;;
--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
fi
;;
esac
done
timeoutSeconds=${TIMEOUT:-600}
# The emulator's hard cap, a little past the wait, whatever this script does.
wallSeconds=$((timeoutSeconds + 30))
out=$DVX_BIN/$DVX_LOG_NAME
dvxLogOut=${out%/*}/dvx.log
doneLine="press any key to exit"
dvxNeedBuild uber
if [ ! -f "$appDir/$program.app" ]; then
echo "bench-dvx: $appDir/$program.app is missing; run: make dvx" >&2
exit 2
fi
work=$(mktemp -d -t joeylib-bench-dvx.XXXXXX)
guard=""
@ -56,20 +79,20 @@ cleanup() {
}
trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir uber)"
dvxStage "$work/c" "$appDir"
if [ "$fullScreen" = 1 ]; then
dvxFullScreen "$work/c" uber
dvxFullScreen "$work/c" "$program"
fi
dvxDosboxConf "$work/dosbox.conf" "$work/c"
mkdir -p "$work/xdg"
rm -f "$out"
rm -f "$out" "$dvxLogOut"
display=":$((RANDOM % 400 + 1700))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
xvfb=$!
sleep 1
echo "bench-dvx: running UBER in DVX headless (timeout ${timeoutSeconds}s)..." >&2
echo "bench-dvx: running $program in DVX headless on ${DVX_VIDEO_W}x${DVX_VIDEO_H}x${DVX_VIDEO_BPP} (timeout ${timeoutSeconds}s)..." >&2
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wallSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 &
@ -77,13 +100,13 @@ guard=$!
dosbox=$(joeyChild "$guard" || true)
start=$SECONDS
log=$(dvxAwaitLog "$work/c" uber "$doneLine" "$timeoutSeconds" "$guard" || true)
log=$(dvxAwaitLog "$work/c" "$program" "$doneLine" "$timeoutSeconds" "$guard" || true)
waited=$((SECONDS - start))
if [ -z "$log" ]; then
log=$(dvxProgramLog "$work/c" uber)
log=$(dvxProgramLog "$work/c" "$program")
fi
if [ -z "$log" ]; then
echo "bench-dvx: FAIL -- UBER wrote no joeylog.txt (DVX.LOG follows)" >&2
echo "bench-dvx: FAIL -- $program wrote no joeylog.txt (DVX.LOG follows)" >&2
dvxDvxLog "$work/c" >&2
exit 1
fi
@ -91,6 +114,7 @@ tr -d '\r' < "$log" > "$out"
joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox=""
guard=""
dvxDvxLogCopy "$work/c" "$dvxLogOut"
if [ "$fullScreen" = 1 ] && ! dvxDvxLog "$work/c" 200 | grep -q "$DVX_FULL_SCREEN_LOG"; then
echo "bench-dvx: FAIL -- --full-screen, but DVX.LOG has no \"$DVX_FULL_SCREEN_LOG\" (DVX.LOG follows)" >&2

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_BIN="$JOEY_REPO/build/dvx/bin"
# UBER's probe build (make dvx-probe), packaged apart from bin/.
DVX_PROBE_DIR="$JOEY_REPO/build/dvx/probe"
# The folder a program installs in under apps/, and the name of its log.
DVX_VENDOR=joeylib
DVX_LOG_NAME=joeylog.txt
@ -19,6 +21,9 @@ DVX_SETTINGS_FULL_SCREEN=fullScreen
# DVX.LOG's line when a program's full-screen mode is set (the port asks for
# the stage's size at 8 bits a pixel).
DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp"
# DVX.LOG's line when a program cannot be loaded (a missing file, a name
# that is not 8.3, an unresolved import).
DVX_LOAD_FAILED_LOG="DXE load failed"
# The runtime parts of toolchains/opt/dvx (all but the SDK).
DVX_RUNTIME="dvx.exe config system libs widgets apps"
# The desktop the gates run: 640x480 in 16-bit colour, DVX's own DOS test
@ -163,8 +168,8 @@ dvxProgramLog() {
# dvxAwaitLog DIR PROGRAM REGEX SECONDS PID -- wait up to SECONDS (or until PID
# exits) for PROGRAM's log to hold a line matching REGEX; print its path if
# it does.
# exits, or DVX.LOG says a program failed to load) for PROGRAM's log to hold
# a line matching REGEX; print its path if it does.
dvxAwaitLog() {
local log
local waited
@ -175,6 +180,7 @@ dvxAwaitLog() {
return 0
fi
joeyAlive "$5" || return 1
dvxDvxLog "$1" 400 | grep -q "$DVX_LOAD_FAILED_LOG" && return 1
sleep 1
done
return 1
@ -201,3 +207,13 @@ dvxDvxLog() {
[ -n "$log" ] && tr -d '\r' < "$log" | tail -"${2:-15}"
return 0
}
# dvxDvxLogCopy DIR FILE -- the staged DVX's whole DVX.LOG as FILE (Unix line
# ends); FILE is left out when there is none.
dvxDvxLogCopy() {
local log
log="$(find "$1/DVX" -maxdepth 1 -iname dvx.log -print -quit)"
[ -n "$log" ] && tr -d '\r' < "$log" > "$2"
return 0
}

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
pixel nibbles: the sentinel A 5 A 5, the count byte, a zero byte, then each
character, two pixels a byte, high nibble first. This finds that row in a
screenshot (on a desktop the window is wherever DVX put it) and checks it
holds exactly TARGET.
screenshot (on a desktop the window is wherever DVX put it, at the window's
scale: every stage pixel a SCALE x SCALE block) and checks it holds exactly
TARGET, and that every block of it is whole: each of its screen pixels the
same colour.
keysRow.py PNG TARGET [--size WxH] [--at X,Y]
keysRow.py PNG TARGET [--size WxH] [--at X,Y] [--scale S]
--size: the screenshot must be that size (the full-screen mode, not the
desktop). --at: the row must start exactly there (the stage's own row 198
at 1x on a full screen). Prints the row it found; exit 0 when it is right.
at 1x on a full screen). --scale: the row must be at that scale (else the
first of 1, 2, 3 it is found at). Prints the row it found; exit 0 when it
is right.
"""
import sys
@ -24,11 +28,56 @@ PAL12 = [(i << 8) | (i << 4) | i for i in range(16)]
PAL12[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF
PAL12[15] = 0xF00
PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12]
# A 16-bit desktop (5-6-5) is off by at most 8 a channel after the round
# A 15- or 16-bit desktop is off by at most 8 a channel after the round
# trip, the 6-bit VGA DAC by at most 3, while the palette entries sit 17
# apart: nearest is exact.
# apart: nearest is exact. (Every KEYS colour is also one of DVX's 256-colour
# desktop palette, its cube or its grey ramp, so 8 bits is exact too.)
MAX_DIST = 3 * 9 ** 2
NO_NIBBLE = -1
SENTINEL = (0xA, 0x5, 0xA, 0x5)
SCALES = (1, 2, 3)
def nibbleMap(img):
"""Every pixel's KEYS palette index, NO_NIBBLE where no entry is near."""
nearest = {}
for rgb in set(img.getdata()):
best, bestDist = NO_NIBBLE, MAX_DIST + 1
for n, pal in enumerate(PAL_RGB):
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
if d < bestDist:
best, bestDist = n, d
nearest[rgb] = best
return [nearest[rgb] for rgb in img.getdata()]
def findRow(nibs, pixels, w, h, scale, expect):
"""The (x, y, bytes) of the first row at scale whose header matches."""
header = expect[:4]
span = 2 * len(expect) * scale
for y in range(h):
base = y * w
for x in range(w - span + 1):
if nibs[base + x] != SENTINEL[0]:
continue
if tuple(nibs[base + x + i * scale] for i in range(len(SENTINEL))) != SENTINEL:
continue
row = bytes(((nibs[base + x + 2 * i * scale] & 0xF) << 4) | (nibs[base + x + (2 * i + 1) * scale] & 0xF) for i in range(len(expect)))
if row[:4] == header:
return x, y, row
return None
def splitBlocks(pixels, w, h, x, y, scale, count):
"""How many of the count stage pixels from (x, y) are not whole blocks."""
split = 0
for i in range(count):
left = x + i * scale
want = pixels[y * w + left]
block = [pixels[(y + dy) * w + left + dx] for dy in range(scale) if y + dy < h for dx in range(scale)]
if len(block) != scale * scale or any(p != want for p in block):
split += 1
return split
def main(argv):
@ -38,12 +87,15 @@ def main(argv):
png, target = argv[1], argv[2]
size = None
at = None
scales = SCALES
args = argv[3:]
while args:
if args[0] == "--size" and len(args) > 1:
size = tuple(int(n) for n in args[1].split("x"))
elif args[0] == "--at" and len(args) > 1:
at = tuple(int(n) for n in args[1].split(","))
elif args[0] == "--scale" and len(args) > 1:
scales = (int(args[1]),)
else:
sys.stderr.write(__doc__)
return 2
@ -51,43 +103,30 @@ def main(argv):
img = Image.open(png).convert("RGB")
w, h = img.size
px = img.load()
if size is not None and img.size != size:
print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1]))
return 1
nearest = {}
def nib(x, y):
rgb = px[x, y]
if rgb not in nearest:
best, bestDist = 0, 1 << 30
for n, pal in enumerate(PAL_RGB):
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
if d < bestDist:
best, bestDist = n, d
nearest[rgb] = best if bestDist <= MAX_DIST else -1
return nearest[rgb]
pixels = list(img.getdata())
nibs = nibbleMap(img)
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
found = None
for y in range(h):
for x in range(w - 2 * len(expect)):
if tuple(nib(x + i, y) for i in range(len(SENTINEL))) == SENTINEL:
row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect)))
if row[:4] == expect[:4]:
found = (x, y, row)
break
for scale in scales:
found = findRow(nibs, pixels, w, h, scale, expect)
if found:
break
if not found:
print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen" % len(target))
print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen at %s" % (len(target), " or ".join("%dx" % s for s in scales)))
return 1
x, y, row = found
print("keysRow: row at (%d,%d)=%s" % (x, y, row.hex().upper()))
print("keysRow: row at (%d,%d) at %dx=%s" % (x, y, scale, row.hex().upper()))
if row != expect:
print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
return 1
split = splitBlocks(pixels, w, h, x, y, scale, 2 * len(expect))
if split:
print("keysRow: FAIL - %d of the row's %d stage pixels are not whole %dx%d blocks" % (split, 2 * len(expect), scale, scale))
return 1
if at is not None and (x, y) != at:
print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1]))
return 1

View file

@ -5,14 +5,20 @@
#
# Kinds, in the order `all` runs them:
# host the shared layer on the host, no emulator: the new-port
# template (blank), input, millis, save, serial, sfxmix, tilebank
# template (blank), file roots, the dirty-band marker, input,
# millis, save, serial, sfxmix, tilebank
# golden UBER on each port, its 51 per-op surface hashes against
# tests/goldens/uber (x68000 against the IIgs's, its own gate;
# dvx against the DOS port's, whose stage it shows)
# atarist: also the dirty-band marker the three 68000 ports share
# (src/m68k/stageBands.s), held to the per-row widen under Hatari
# visual iigs: every launchable example on joey.2mg boots and draws
# dvx: full screen -- UBER's hashes through a suspend and a
# resume, and KEYS's row read off the 320x200 VGA screen, then
# in its window, then on the whole screen again
# dvx: also the display -- UBER's probe build on nine desktop
# depths and scales and the full screen, every displayed pixel
# checked against the stage
# x68000: the SCB -- every text-palette reload of a 147-change
# screen lands in the blanking before its band, timed on the beam
# tilebank each port: examples/tilebank loads the port's own .tbk bake
@ -20,9 +26,10 @@
# gate's surface hash, and refuses every other target's bake (the
# host half is check-tilebank.sh under `host`)
# input each port: a typed string reaches jlInputGetChar byte-exact
# (dvx: and the present shows it, decoded from the desktop)
# x68000: also both joysticks -- every direction, both triggers
# and every release, read back from examples/joy's display
# (dvx: and the present shows it, decoded from the desktop, also
# at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program)
# x68000 and dvx: also both joysticks -- every direction, both
# buttons and every release, read back from examples/joy's display
# audio six gates, two kinds:
# iigs DOC wavetable + sound IRQ vector (register tap)
# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap)
@ -44,6 +51,8 @@
# save iigs: a SAVES/ blob written and read back through GS/OS
# loss iigs: 4096 bytes through full-screen presents, lossless on the
# modem port at 9600 and a Super Serial Card at 2400
# x68000: both serial streams byte-exact and the mouse right while
# the SCB changes palette on every line of a run (9600, 19200, 38400)
# disk iigs: the one-disk STAXI (ROM 3 and ROM 01) and AGI images boot
# and draw
#
@ -91,6 +100,8 @@ AMIGA_UBER_SECONDS=400
# kind port name command...: every gate, in run order within its kind.
GATES=$(cat <<EOF
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 millis scripts/check-millis.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 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 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 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
visual iigs examples scripts/verify-iigs-all.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
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
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 dos input scripts/verify-dos-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 x68000 audio scripts/verify-x68000-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
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 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 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

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
# Xvfb and types the RetroNet definition-of-done string with xdotool (real X
# key events: ':' needs Shift, which DVX's keyboard layout applies before the
# window's onKey hands the character to jlInputGetChar). Two checks:
# 1. the present: a raw screenshot of the desktop must show KEYS's
# window's onKey hands the character to jlInputGetChar). Three checks:
# 1. DVX's reserved chord: Ctrl+Esc (the Task Manager) must not reach KEYS
# as Esc, which would end it (its typed line appears in its log); the
# Task Manager's Switch To gives KEYS the focus back;
# 2. the present: a raw screenshot of the desktop must show KEYS's
# verification scanline (row 198: sentinel, count, every received
# character as two pixel nibbles) somewhere -- the window's content,
# expanded from the stage through the port's palette tables into DVX's
# 16-bit desktop -- decoding to exactly the typed bytes
# expanded from the stage through the port's palette tables into the
# desktop's pixel format at the window's scale -- decoding to exactly
# the typed bytes, every stage pixel a whole scale x scale block
# (scripts/lib/keysRow.py);
# 2. the typed history: after Escape, KEYS logs every received character
# 3. the typed history: after Escape, KEYS logs every received character
# as hex to its joeylog.txt (in its settings folder), which must equal
# the typed ASCII.
# Each desktop given (default 640x480 at 16 bits, where the window is 1x) is
# a run of its own; DVX.LOG must show KEYS's window on that desktop at the
# scale given.
#
# Usage: scripts/verify-dvx-input.sh
# Usage: scripts/verify-dvx-input.sh [WxHxBPP:SCALE]...
# e.g. scripts/verify-dvx-input.sh 800x600x8:2 1024x768x32:3
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
# the keys program (make dvx).
set -uo pipefail
@ -27,10 +35,16 @@ joeyNeed DVX_ROOT dvx
program=keys
target="192.168.1.10:6510"
stageWidth=320
stageHeight=200
wall=180 # hard cap on DOSBox, whatever happens to this script
readySeconds=60 # DVX boot until its shell reports ready
settleSeconds=3 # then KEYS opening its window and taking the focus
stepSeconds=2 # a key, then DVX and KEYS acting on it
holdSeconds=0.3 # a key held that long: a quick tap can fall between two polls
logSeconds=30 # Escape until KEYS's typed line is in its log
specs=("$@")
[ ${#specs[@]} -gt 0 ] || specs=(640x480x16:1)
dvxNeedBuild "$program"
@ -38,6 +52,8 @@ work=$(mktemp -d -t joeylib-dvx-input.XXXXXX)
guard=""
dosbox=""
xvfb=""
win=""
fails=""
# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed".
cleanup() {
joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null
@ -45,72 +61,135 @@ cleanup() {
}
trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir "$program")"
dvxDosboxConf "$work/dosbox.conf" "$work/c"
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$work/shots" >> "$work/dosbox.conf"
mkdir -p "$work/xdg" "$work/shots"
# fail SPEC WHAT -- note a failed check; the gate goes on to report the rest.
fail() {
echo "verify-dvx-input: FAIL - $1: $2"
fails="${fails:+$fails; }$1: $2"
}
# hold KEY... -- press the keys in order, each held holdSeconds before the
# next (a chord's modifier goes down first), release them in reverse, then
# give DVX and KEYS time to act.
hold() {
local k
local i
for k in "$@"; do
DISPLAY="$display" xdotool keydown --window "$win" "$k"
sleep "$holdSeconds"
done
for ((i = $#; i >= 1; i--)); do
DISPLAY="$display" xdotool keyup --window "$win" "${!i}"
done
sleep "$stepSeconds"
}
# run SPEC -- one desktop: boot, type, Ctrl+Esc, screenshot, Escape, judge.
run() {
local spec="$1"
local dir="$work/$1"
local w
local h
local bpp
local scale
local ready=""
local waited
local log
local png
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then
echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE)" >&2
exit 2
fi
w=${BASH_REMATCH[1]}
h=${BASH_REMATCH[2]}
bpp=${BASH_REMATCH[3]}
scale=${BASH_REMATCH[4]}
DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp dvxStage "$dir/c" "$(dvxAppDir "$program")"
dvxDosboxConf "$dir/dosbox.conf" "$dir/c"
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf"
mkdir -p "$dir/xdg" "$dir/shots"
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$dir/dosbox.conf" > "$dir/dosbox.log" 2>&1 &
guard=$!
dosbox=$(joeyChild "$guard")
for ((waited = 0; waited < readySeconds; waited++)); do
if dvxDvxLog "$dir/c" 200 | grep -q "DVX Shell ready"; then
ready=1
break
fi
joeyAlive "$guard" || break
sleep 1
done
if [ -z "$ready" ]; then
fail "$spec" "DVX did not come up within ${readySeconds}s (DVX.LOG follows)"
dvxDvxLog "$dir/c" >&2
joeyStop "$dosbox" "$guard" 2> /dev/null
return
fi
sleep "$settleSeconds"
win=$(joeyFindWindow "$display" "$dosbox")
if [ -z "$win" ]; then
fail "$spec" "DOSBox window not found"
joeyStop "$dosbox" "$guard" 2> /dev/null
return
fi
DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true
DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target"
sleep "$stepSeconds"
# DVX's Task Manager chord, then its Switch To back to KEYS.
hold ctrl Escape
log=$(dvxProgramLog "$dir/c" "$program")
if [ -n "$log" ] && tr -d '\r' < "$log" | grep -q '^typed='; then
fail "$spec" "Ctrl+Esc reached KEYS as Esc (it ended)"
fi
hold alt s
# Ctrl+F5 = DOSBox screenshot hotkey.
DISPLAY="$display" xdotool key --window "$win" ctrl+F5
sleep "$stepSeconds"
DISPLAY="$display" xdotool key --window "$win" Escape
log=$(dvxAwaitLog "$dir/c" "$program" '^typed=' "$logSeconds" "$guard")
cp "$log" "$dir/joeylog.txt" 2> /dev/null
joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox=""
guard=""
if ! dvxDvxLog "$dir/c" 400 | grep -qF "JoeyLib: Keys: $((stageWidth * scale))x$((stageHeight * scale)) at ${scale}x on a ${w}x${h} ${bpp}-bit desktop"; then
fail "$spec" "DVX.LOG does not show KEYS at ${scale}x on a ${w}x${h} ${bpp}-bit desktop: $(dvxDvxLog "$dir/c" 400 | grep -F 'JoeyLib: Keys:')"
fi
png=$(ls "$dir"/shots/*.png 2> /dev/null | head -1)
if [ -z "$png" ]; then
fail "$spec" "no screenshot captured"
else
python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" --scale "$scale" || fail "$spec" "the present"
fi
if [ -z "$log" ]; then
fail "$spec" "KEYS logged no typed line within ${logSeconds}s of Escape"
else
dvxKeysTyped "$dir/joeylog.txt" "$target" || fail "$spec" "the log"
fi
}
display=":$((RANDOM % 400 + 2500))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
xvfb=$!
sleep 1
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 &
guard=$!
dosbox=$(joeyChild "$guard")
ready=""
for ((waited = 0; waited < readySeconds; waited++)); do
if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then
ready=1
break
fi
joeyAlive "$guard" || break
sleep 1
for spec in "${specs[@]}"; do
echo "verify-dvx-input: $spec"
run "$spec"
done
if [ -z "$ready" ]; then
echo "verify-dvx-input: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2
dvxDvxLog "$work/c" >&2
exit 1
fi
sleep "$settleSeconds"
win=$(joeyFindWindow "$display" "$dosbox")
if [ -z "$win" ]; then
echo "verify-dvx-input: FAIL - DOSBox window not found" >&2
exit 1
fi
DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true
DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target"
sleep 2
# Ctrl+F5 = DOSBox screenshot hotkey.
DISPLAY="$display" xdotool key --window "$win" ctrl+F5
sleep 2
DISPLAY="$display" xdotool key --window "$win" Escape
log=$(dvxAwaitLog "$work/c" "$program" '^typed=' "$logSeconds" "$guard")
cp "$log" "$work/joeylog.txt" 2> /dev/null
joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox=""
guard=""
png=$(ls "$work"/shots/*.png 2> /dev/null | head -1)
if [ -z "$png" ]; then
echo "verify-dvx-input: FAIL - no screenshot captured" >&2
exit 1
fi
if [ -z "$log" ]; then
echo "verify-dvx-input: FAIL - KEYS logged no typed line within ${logSeconds}s of Escape" >&2
exit 1
fi
fails=""
python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" || fails="the present"
dvxKeysTyped "$work/joeylog.txt" "$target" || fails="${fails:+$fails, }the log"
if [ -n "$fails" ]; then
echo "verify-dvx-input: FAIL - $fails"
exit 1
fi
echo "verify-dvx-input: PASS (typed \"$target\": all ${#target} chars reached jlInputGetChar, and the present shows them)"
echo "verify-dvx-input: PASS (${specs[*]}: typed \"$target\", all ${#target} chars reached jlInputGetChar, Ctrl+Esc did not end KEYS, and the present shows them)"

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_TOP = 16
local FRAME_SIZE = 80
local FRAME_INSET = 2
local DOT_SIZE = 8
local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2
local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 - FRAME_INSET
local BUTTON_W, BUTTON_H, BUTTON_GAP = 24, 16, 4
local BUTTON_TOP = STICK_TOP + FRAME_SIZE + 6
local ROW_GAP = 6
local BUTTON_TOP = STICK_TOP + FRAME_SIZE + ROW_GAP
local STATUS_W, STATUS_H = 80, 4
local STATUS_TOP = BUTTON_TOP + BUTTON_H + 6
local STATUS_TOP = BUTTON_TOP + BUTTON_H + ROW_GAP
local COLOR_BACKGROUND, COLOR_DOT_OFF, COLOR_DOT_ON = 0, 2, 3
local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5
local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7

View file

@ -64,6 +64,18 @@
# PASS: every Lua check right, the peer's stream PASS, at least MIN_BYTES each
# way, and the guest's quiet counts among the peer's quiet snapshots.
#
# Variants, built here into the work directory from examples/sermouse:
# X68K_SERMOUSE_BAUD=19200|38400 the line rate (default 9600, MAME's
# null_modem default); RSDRV.SYS, which the
# boot disk loads, is what reaches past 9600
# X68K_SERMOUSE_DENSE=n stage lines 0 .. n-1 change palette on
# every line (default 0: one palette), so
# the SCB raster interrupt holds IPL 6 for
# the whole run every frame while the
# streams and the mouse run
# scripts/verify-x68000-serial-scb.sh runs the dense combinations this port
# guarantees; see "Serial under a dense SCB" in claudeDocs/x68000_port.md.
#
# Requires: ./install.sh install x68000 -- the patched MAME
# (installer/patches/mame: 0002 for the FIFO depth, 0004 for the
# RS-232C slot and the mouse) and the Human68k system disk.
@ -81,8 +93,11 @@ PORT="${X68K_SERMOUSE_PORT:-6801}"
WALL="${X68K_SERMOUSE_WALL:-600}"
PASSES="${X68K_SERMOUSE_PASSES:-3}"
MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}"
BAUD="${X68K_SERMOUSE_BAUD:-9600}"
DENSE="${X68K_SERMOUSE_DENSE:-0}"
PROG="$repo/build/x68000/bin/SERMOUSE.X"
FIFO_DEPTH=3
DEFAULT_BAUD=9600
# The peer outlives MAME by this much, so it always sees the link close, and
# is given this long to report once MAME is gone.
PEER_MARGIN=30
@ -101,12 +116,27 @@ cleanup() {
}
trap cleanup EXIT
# A variant is examples/sermouse built against the same library with its
# line rate and SCB set (see the header).
if [ "$BAUD" != "$DEFAULT_BAUD" ] || [ "$DENSE" != 0 ]; then
joeyNeed X68K_ROOT toolchain:x68000
joeyNeed LIBXMP_DIR libxmp
"$X68K_ROOT/bin/m68k-xelf-gcc" -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 -DLINE_BAUD="${BAUD}u" -DDENSE_SCB_LINES="${DENSE}u" \
-I"$repo/include" -I"$LIBXMP_DIR/include" \
"$repo/examples/sermouse/sermouse.c" \
"$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \
-o "$work/SERMOUSE.X" || exit 1
PROG="$work/SERMOUSE.X"
fi
x68kBootDisk "$work/boot.xdf" 'A:\SERMOUSE.X' || exit 1
x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1
{
printf 'local PASSES = %d\n' "$PASSES"
printf 'local MIN_BYTES = %d\n' "$MIN_BYTES"
printf 'local BAUD = %d\n' "$BAUD"
printf 'local DENSE = %d\n' "$DENSE"
cat <<'LUA'
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
@ -293,8 +323,8 @@ end)
local function finish(verdict)
done = true
local s = prev or {}
io.write(string.format("VERIFY-X68K-SERMOUSE %s checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n",
verdict, checks, #fails, s.rx or 0, s.tx or 0,
io.write(string.format("VERIFY-X68K-SERMOUSE %s baud=%d dense=%d checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n",
verdict, BAUD, DENSE, checks, #fails, s.rx or 0, s.tx or 0,
quiet and string.format("%d:%d", quiet.rx, quiet.tx) or "none", iackFixed,
#fails > 0 and (" detail=" .. table.concat(fails, ",")) or ""))
io.flush()
@ -490,6 +520,11 @@ done
# MAME itself, which cleanup stops first (stopping only the timeout process
# could leave MAME running).
cd "$work" || exit 1
if [ "$BAUD" != "$DEFAULT_BAUD" ]; then
mkdir -p "$work/cfg"
python3 "$repo/scripts/lib/mameNullModemCfg.py" x68000 :rs232c:null_modem "$BAUD" \
> "$work/cfg/x68000.cfg" || exit 2
fi
JOEY_Z80SCC_RX_FIFO=$FIFO_DEPTH timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \
@ -538,4 +573,4 @@ fi
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "verify-x68000-serial-mouse: PASS (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)"
echo "verify-x68000-serial-mouse: PASS at $BAUD baud, $DENSE dense-SCB lines (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)"

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

View file

@ -288,9 +288,6 @@ extern jlSurfaceT *gStage;
extern uint16_t iigsGetTickWord(void);
#define jlpFrameCount() iigsGetTickWord()
// jlMemCopy: MVN block moves, split at bank boundaries in hal.c.
void iigsMemCopy(void *dst, const void *src, uint32_t bytes);
#define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n))
// The one Memory Manager owner ID every IIgs allocation uses (hal.c).
uint16_t iigsMemUserId(void);
// Interrupt-latency bound of the long interrupts-off paths (hal.c): a
@ -777,9 +774,6 @@ uint16_t jlpGenericFrameHz(void);
#endif
uint16_t jlpGenericFrameCount(void);
#if !defined(jlpMemCopy)
#define jlpMemCopy(_d, _s, _n) memcpy((_d), (_s), (size_t)(_n))
#endif
#if !defined(jlpFrameCount)
#if defined(JL_HAS_FRAME_COUNT)
uint16_t jlpFrameCount(void);

View file

@ -55,24 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
return;
}
#if defined(JOEYLIB_PLATFORM_IIGS)
// llvm-mos lowers memset to `jsl memset` -- libc's `while (n--)
// *d++ = c` byte loop behind a far call (the finding-#79 class; the
// old comment claiming an MVP/PEI lowering was wrong). That one call
// was essentially the whole cost of this op on the IIgs, and a C
// word-fill fares no better (llvm-mos re-derives the far pointer
// every store). One JSL into the asm [dp],y word fill instead.
{
uint16_t fill = (uint16_t)((uint16_t)paletteIndex | ((uint16_t)paletteIndex << 8));
iigsByteFill(&s->scb[firstLine], fill, (uint16_t)(last - firstLine + 1u));
}
#else
// memset is far cheaper than the per-iter loop (scb is uint8_t,
// sizeof(uint8_t)==1, so the call form below is exact); Amiga/ST/DOS
// libc memset is already a vectorized fill.
// sizeof(uint8_t)==1, so the call form below is exact); every port's
// libc memset is a tight fill (the IIgs's 16-bit stores since the
// 2026-10-04 runtime, 5.9 cycles a byte).
memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1));
#endif
if (s == gStage) {
gStageScbDirty = true;
}

View file

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

View file

@ -13,10 +13,25 @@
#define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t))
#define SURFACE_FILE_BYTES (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES)
// A band byte repeated in all four bytes of a long, for the 68000 fill.
#define STAGE_BAND_FILL(_b) ((uint32_t)(_b) * 0x01010101ul)
#if defined(JOEYLIB_CPU_68000)
// src/m68k/stageBands.s carries these as constants of its own.
typedef char stageBandsRows[(SURFACE_HEIGHT == 200) ? 1 : -1];
typedef char stageBandsGroupRows[(STAGE_DIRTY_GROUP4_ROWS == 4u) ? 1 : -1];
typedef char stageBandsGroupMinRows[(STAGE_MARK_GROUP_MIN_ROWS == 9u) ? 1 : -1];
#endif
// ----- Prototypes -----
// (public API declared in joey/surface.h)
// (internal prototypes declared in surfaceInternal.h)
#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000)
static inline void markGroup(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord);
static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord);
#endif
// ----- Module state -----
@ -60,9 +75,85 @@ uint16_t gStagePaletteDirtyMask = 0xFFFFu;
// ----- Internal helpers (alphabetical) -----
// (surfaceMarkDirtyRows inlines the per-row widen with walking pointers
// -- see below. The single-row widen is inlined by the surfaceMarkDirtyRect
// macro, and the IIgs multi-row path is the asm marker.)
#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000)
// Widen the STAGE_DIRTY_GROUP4_ROWS rows at minPtr / maxPtr. (Spelling the
// four rows out benched slower on DOS than this loop.)
static inline void markGroup(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord) {
uint16_t row;
for (row = 0u; row < STAGE_DIRTY_GROUP4_ROWS; row++) {
markRow(&minPtr[row], &maxPtr[row], minWord, maxWord);
}
}
// surfaceMarkDirtyRows's group path, for at least STAGE_MARK_GROUP_MIN_ROWS
// rows: the C twin of src/m68k/stageBands.s -- the same even-row groups and
// the same reuse of a group's result, which tests/host/dirtyMarkHost.c holds
// to the plain per-row widen. A function of its own so the short-mark
// path in surfaceMarkDirtyRows stays a tight loop; gcc inlines it there
// (forcing it out of line cost DOS's 16-row fills 2%).
static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
uint8_t *minPtr;
uint8_t *maxPtr;
StageBandGroupT *minGroup;
StageBandGroupT *maxGroup;
uint32_t keyMin;
uint32_t keyMax;
uint32_t bandMin;
uint32_t bandMax;
uint16_t rows;
uint16_t groups;
minPtr = &gStageMinWord[y];
maxPtr = &gStageMaxWord[y];
rows = (uint16_t)(yEnd - y);
if ((y & 1u) != 0u) {
markRow(minPtr++, maxPtr++, minWord, maxWord);
rows--;
}
groups = (uint16_t)(rows >> STAGE_DIRTY_GROUP4_SHIFT);
rows = (uint16_t)(rows & (STAGE_DIRTY_GROUP4_ROWS - 1u));
minGroup = (StageBandGroupT *)(void *)minPtr;
maxGroup = (StageBandGroupT *)(void *)maxPtr;
keyMin = *minGroup;
keyMax = *maxGroup;
markGroup((uint8_t *)(void *)minGroup, (uint8_t *)(void *)maxGroup, minWord, maxWord);
bandMin = *minGroup++;
bandMax = *maxGroup++;
while (--groups != 0u) {
if (*minGroup == keyMin && *maxGroup == keyMax) {
*minGroup = bandMin;
*maxGroup = bandMax;
} else {
keyMin = *minGroup;
keyMax = *maxGroup;
markGroup((uint8_t *)(void *)minGroup, (uint8_t *)(void *)maxGroup, minWord, maxWord);
bandMin = *minGroup;
bandMax = *maxGroup;
}
minGroup++;
maxGroup++;
}
minPtr = (uint8_t *)(void *)minGroup;
maxPtr = (uint8_t *)(void *)maxGroup;
while (rows != 0u) {
markRow(minPtr++, maxPtr++, minWord, maxWord);
rows--;
}
}
// Widen one row's band to cover [minWord, maxWord].
static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, uint8_t maxWord) {
if (minWord < *minPtr) {
*minPtr = minWord;
}
if (maxWord > *maxPtr) {
*maxPtr = maxWord;
}
}
#endif
// ----- Public API (alphabetical) -----
@ -282,104 +373,45 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) {
if (s != gStage) {
return;
}
// Whole-stage mark is a constant fill of both band arrays. On the
// IIgs, memset lowers to `jsl memset` -- a far-call byte loop (a C
// word-fill fared no better: llvm-mos re-derives the far pointer
// every store), so the fill is one JSL into the asm word-store
// loop; the other ports' libc memsets are already tight fills.
#if defined(JOEYLIB_PLATFORM_IIGS)
iigsStageBandsMarkAll();
// Whole-stage mark is a constant fill of both band arrays. The 68000
// ports share stageDirtyClearAll's one-pass fill; the x86 and IIgs
// memsets are already tight (the IIgs's matched the asm word-store
// loop it replaced, 2026-10-04).
#if defined(JOEYLIB_CPU_68000)
stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(0u), STAGE_BAND_FILL(STAGE_DIRTY_FULL_MAX));
#else
memset(gStageMinWord, 0, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
#endif
#if defined(JOEYLIB_PLATFORM_IIGS)
{
uint8_t grp;
for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) {
gStageDirtyGroup[grp] = 1u;
}
}
memset(gStageDirtyGroup, 1, STAGE_DIRTY_GROUPS);
#endif
}
// Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h):
// widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never
// compile this -- the macro routes them straight to the asm marker
// iigsMarkDirtyRowsInner instead.
#ifndef JOEYLIB_PLATFORM_IIGS
// widen rows y..yEnd-1 to cover [minWord, maxWord]. The IIgs and the 68000
// ports never compile this -- the macro routes them to their asm markers --
// so it is the x86 ports' and the host's. Tall marks go four rows at a time
// (markGroups); shorter ones walk the rows here, with the light entry the
// old per-row loop had. (Whole-tile marks never come here: tile.c's
// tileMarkDirty unrolls its constant 8 rows inline.)
#if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JOEYLIB_CPU_68000)
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
uint8_t *minPtr;
uint8_t *maxPtr;
// Straight-line inlined widen (no per-row helper call) with walking
// pointers, so a multi-row stage op -- sprite draw, fillRect --
// pays one tight loop instead of yEnd-y function calls. This is
// the ST/Amiga counterpart to the IIgs asm marker. (Whole-tile
// marks bypass this entirely since NATIVE-PERF Phase 2 R1 --
// tile.c's tileMarkDirty unrolls its constant 8 rows inline.)
//
// Loop shape is PER-PORT, decided by measurement (Phase 2 Step C,
// 2026-07-07): the x2 unroll lifted Amiga sprite gate rows +4-6%
// but cost the ST -5% in BOTH the pointer-walking and counted
// forms (gcc-mint materializes four live pointers either way),
// and the simple loop re-measured exactly at the ST baseline.
// DOS was flat either way, so only the Amiga takes the unroll.
stageMarkDirtyGroups(y, yEnd);
minPtr = &gStageMinWord[y];
maxPtr = &gStageMaxWord[y];
#if defined(JOEYLIB_PLATFORM_AMIGA)
{
uint16_t rows;
uint16_t pairs;
rows = (uint16_t)(yEnd - y);
pairs = (uint16_t)(rows >> 1);
while (pairs != 0u) {
if (minWord < minPtr[0]) {
minPtr[0] = minWord;
}
if (maxWord > maxPtr[0]) {
maxPtr[0] = maxWord;
}
if (minWord < minPtr[1]) {
minPtr[1] = minWord;
}
if (maxWord > maxPtr[1]) {
maxPtr[1] = maxWord;
}
minPtr += 2;
maxPtr += 2;
pairs--;
}
if ((rows & 1u) != 0u) {
if (minWord < *minPtr) {
*minPtr = minWord;
}
if (maxWord > *maxPtr) {
*maxPtr = maxWord;
}
}
}
#else
{
uint8_t *minEnd;
if ((uint16_t)(yEnd - y) >= STAGE_MARK_GROUP_MIN_ROWS) {
markGroups(y, yEnd, minWord, maxWord);
return;
}
minPtr = &gStageMinWord[y];
maxPtr = &gStageMaxWord[y];
minEnd = &gStageMinWord[yEnd];
while (minPtr < minEnd) {
if (minWord < *minPtr) {
*minPtr = minWord;
markRow(minPtr++, maxPtr++, minWord, maxWord);
}
if (maxWord > *maxPtr) {
*maxPtr = maxWord;
}
minPtr++;
maxPtr++;
}
}
#endif
}
#endif
@ -439,12 +471,16 @@ bool stageAlloc(void) {
void stageDirtyClearAll(void) {
// Constant fill of both band arrays, same form as its sibling
// surfaceMarkDirtyAll. This runs after EVERY jlStagePresent, so on
// the IIgs the two memset far-call byte loops here were ~10 ms of
// every present's fixed base; the asm word-store loop fixes that.
// The other ports keep memset (their libc fills are already tight).
// surfaceMarkDirtyAll. This runs after EVERY jlStagePresent. The IIgs
// clears only the 8-row groups its group map says were marked
// (joeyDraw.s), which no fill can match on an idle present. The
// 68000 ports fill both arrays in one pass of long stores (about half
// what two libc memsets cost there); the x86 ports keep memset, whose
// rep stos is already the fastest fill they have.
#if defined(JOEYLIB_PLATFORM_IIGS)
iigsStageBandsClearAll();
#elif defined(JOEYLIB_CPU_68000)
stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MIN), STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MAX));
#else
memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);

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 gStageMaxWord[SURFACE_HEIGHT];
// Four rows' clean test in one load, for presents on CPUs with a 32-bit
// load (DOS, DVX): a clean row's min word is the CLEAN sentinel (0xFF) and a
// touched row's is at most STAGE_DIRTY_FULL_MAX, so rows 4g .. 4g+3 are all
// clean exactly when their four min-word bytes read 0xFFFFFFFF. It reads the
// per-row bands themselves, so marking has nothing extra to keep in step --
// unlike the IIgs's gStageDirtyGroup below, which exists only because the
// 65816 has no 32-bit load. gStageMinWord is aligned(2), which x86 (and a
// 68000 long load) accepts.
#define STAGE_DIRTY_GROUP4_SHIFT 2
#define STAGE_DIRTY_GROUP4_ROWS (1u << STAGE_DIRTY_GROUP4_SHIFT)
#define STAGE_DIRTY_GROUP4_ALL_CLEAN 0xFFFFFFFFul
typedef uint32_t __attribute__((may_alias)) StageBandGroupT;
#define STAGE_DIRTY_GROUP4_CLEAN(_g) (((const StageBandGroupT *)(const void *)gStageMinWord)[_g] == STAGE_DIRTY_GROUP4_ALL_CLEAN)
// Present-path telemetry (NATIVE-PERF Phase 6 W0, IIgs added W2).
// The presents accumulate how many bytes they moved toward the
// display and how many band rows they scanned; the UBER_PROBE build
@ -159,19 +173,29 @@ extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS];
// Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1
// (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes
// straight to the asm marker so a multi-row mark stays a single JSL;
// elsewhere it is the C loop in surface.c.
// straight to the asm marker so a multi-row mark stays a single JSL; the
// 68000 ports share an asm marker too (src/m68k/stageBands.s), handed the
// rows' addresses because that file cannot spell the arrays' C symbols for
// all three toolchains; elsewhere it is the C loop in surface.c. The 68000
// and C markers take the rows four at a time from an even row, and reuse a
// group's result for the next group that held the same bytes (groups of
// STAGE_DIRTY_GROUP4_ROWS, above, as one long each).
// Two whole groups after at most one row to reach an even row: the least that
// lets a group reuse the one before it. Below this the rows go one by one.
#define STAGE_MARK_GROUP_MIN_ROWS (2u * STAGE_DIRTY_GROUP4_ROWS + 1u)
#ifdef JOEYLIB_PLATFORM_IIGS
void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) iigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max))
// Constant-fill both band arrays without libc memset (llvm-mos lowers
// memset to a far-call byte loop -- ~10 ms per present; joeyDraw.s).
// ClearAll = CLEAN sentinels (post-present); MarkAll = every row fully
// dirty. iigsByteFill is the same fix for arbitrary far-pointer fills
// (SCB range): value replicated in both bytes of `fill`, count >= 1.
// Reset the bands of every 8-row group the group map says was marked to
// the CLEAN sentinels, and clear the map (joeyDraw.s): what
// stageDirtyClearAll does after each present, skipping clean groups.
void iigsStageBandsClearAll(void);
void iigsStageBandsMarkAll(void);
void iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count);
#elif defined(JOEYLIB_CPU_68000)
// src/m68k/stageBands.s. Fill stores minFill / maxFill (a byte repeated four
// times) over every row of the two arrays.
void stageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, uint32_t minFill, uint32_t maxFill);
void stageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord);
#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) stageBandsMark68k(&gStageMinWord[_y], &gStageMaxWord[_y], (uint16_t)((_yEnd) - (_y)), (uint8_t)(_min), (uint8_t)(_max))
#else
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
#endif

View file

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

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 <stdlib.h>
#include <string.h>
#include <time.h> // uclock / UCLOCKS_PER_SEC
#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/exceptn.h>
#include <sys/farptr.h>
#include <sys/nearptr.h>
#include "dosTimer.h"
#include "port.h"
#include "surfaceInternal.h"
#include "x86Expand.h"
@ -40,7 +41,10 @@
// ----- Prototypes -----
static void crashHandler(int sig);
void dosPitProgram(uint8_t ctrl, uint16_t divisor);
static void installCrashLog(void);
static uint64_t pitClocksNow(void);
static void pitClockStart(void);
static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src);
@ -50,6 +54,15 @@ static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false;
static FILE *gCrashLog = NULL;
// The millisecond clock (pitClocksNow, jlpMillisElapsed).
static uint64_t gPitStart = 0u; // PIT clocks at jlpInit
static uint64_t gPitDayClocks = 0u; // PIT clocks of the midnights passed
static uint32_t gPitLastTicks = 0u; // BIOS ticks at the last read
static uint32_t gMillisLast = 0u; // value returned last
static uint32_t gMillisRebase = 0u; // added to the current source's ms
static uint16_t gMillisLastHz = 0u; // source of gMillisLast: 0 = PIT, else the audio ISR's rate
static uint32_t gMillisLastIsr = 0u; // audio ISR fires at the last read
// (The old $3DA edge-poll frame-counter state lived here; removed with
// the poll-rate-dependent counter itself -- see jlpFrameCount.)
@ -140,6 +153,14 @@ static void crashHandler(int sig) {
}
// Program PIT counter 0 (dosTimer.h). Shared with src/dos/audio.c.
void dosPitProgram(uint8_t ctrl, uint16_t divisor) {
outportb(PIT_CTRL_PORT, ctrl);
outportb(PIT_TIMER0_PORT, (uint8_t)(divisor & 0xFFu));
outportb(PIT_TIMER0_PORT, (uint8_t)((divisor >> 8) & 0xFFu));
}
// Open CRASH.LOG in cwd (DOSBox's mounted dir on the host) and install
// signal handlers that route fault reports there. This runs before the
// VGA mode change so its file handle is established while the screen
@ -167,6 +188,62 @@ static void installCrashLog(void) {
// memcmp per present (NATIVE-PERF Phase 2 R9). The SCB flag is not
// consumed here: mode 13h has one DAC; the per-line SCB choice is
// applied in the pixel expansion, which reads src->scb directly.
// PIT clocks (1.193182 MHz) since the BIOS's midnight, plus the midnights
// seen: BIOS ticks * 65536 and the position inside the current tick from
// counter 0 (mode 2, dosTimer.h). All three are read with interrupts off,
// and a tick IRQ0 has already raised but the BIOS has not yet counted is
// added by hand. DJGPP's uclock() missed that case, so its time stepped
// back a tick and forward again; a frame difference taken across the step
// came out huge or short depending only on when the call landed, and UBER
// rows measured on DOS ended early by a code-layout lottery.
static uint64_t pitClocksNow(void) {
uint32_t ticks;
uint16_t count;
uint16_t intoTick;
uint8_t irr;
int wasEnabled;
wasEnabled = disable();
outportb(PIT_CTRL_PORT, PIT_CTRL_T0_LATCH);
count = inportb(PIT_TIMER0_PORT);
count |= (uint16_t)((uint16_t)inportb(PIT_TIMER0_PORT) << 8);
ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
outportb(PIC1_CMD, PIC_OCW3_READ_IRR);
irr = inportb(PIC1_CMD);
if (wasEnabled) {
enable();
}
// Mode 2 counts 65536 (read as 0), 65535 .. 1, so this is the PIT
// clocks since the tick began.
intoTick = (uint16_t)(0u - count);
// IRQ0 pending with the count early in a tick: the wrap came before
// the latch and the BIOS has not counted it. (Pending with the count
// late in a tick means it wrapped after the latch: nothing to add.)
if ((irr & PIC_IRQ0_BIT) != 0u && intoTick < (uint16_t)(BIOS_TICK_PERIOD / 2u)) {
ticks++;
}
if (ticks < gPitLastTicks) {
gPitDayClocks += (uint64_t)BIOS_TICKS_PER_DAY * BIOS_TICK_PERIOD;
}
gPitLastTicks = ticks;
return gPitDayClocks + (uint64_t)ticks * BIOS_TICK_PERIOD + intoTick;
}
// Put counter 0 in mode 2 at the BIOS's own rate (the IRQ0 rate does not
// change) and take the clock's zero.
static void pitClockStart(void) {
int wasEnabled;
wasEnabled = disable();
dosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR);
if (wasEnabled) {
enable();
}
gPitStart = pitClocksNow();
}
static void uploadPaletteIfNeeded(const jlSurfaceT *src) {
if (!gStagePaletteDirty) {
return;
@ -184,6 +261,7 @@ bool jlpInit(const jlConfigT *config) {
(void)config;
installCrashLog();
pitClockStart();
memset(&regs, 0, sizeof(regs));
regs.x.ax = 0x0013;
@ -224,25 +302,21 @@ void jlpPresent(const jlSurfaceT *src) {
// bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
// gives the byte range x86ExpandRow needs.
// Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed
// ST/Amiga idle-scan trick): a clean row's minWord is the 0xFF
// CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX
// (79), so 4 clean rows read exactly 0xFFFFFFFF and the whole
// group skips. A failing group keeps the per-row CLEAN test, so a
// clean row INSIDE a dirty group is skipped exactly as before --
// written bytes and counters are byte-identical to the row loop.
// ST/Amiga idle-scan trick; STAGE_DIRTY_GROUP4_CLEAN). A failing
// group keeps the per-row CLEAN test, so a clean row INSIDE a dirty
// group is skipped exactly as before -- written bytes and counters are
// byte-identical to the row loop.
{
typedef uint32_t __attribute__((may_alias)) BandGroupT;
const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
uint8_t g;
for (g = 0u; g < (uint8_t)(SURFACE_HEIGHT / 4u); g++) {
for (g = 0u; g < (uint8_t)(SURFACE_HEIGHT >> STAGE_DIRTY_GROUP4_SHIFT); g++) {
int16_t yEnd;
if (minGroups[g] == 0xFFFFFFFFul) {
if (STAGE_DIRTY_GROUP4_CLEAN(g)) {
continue;
}
yEnd = (int16_t)((uint16_t)g * 4u + 4u);
for (y = (int16_t)((uint16_t)g * 4u); y < yEnd; y++) {
yEnd = (int16_t)(((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT) + STAGE_DIRTY_GROUP4_ROWS);
for (y = (int16_t)((uint16_t)g << STAGE_DIRTY_GROUP4_SHIFT); y < yEnd; y++) {
// Canonical clean-row test (finding #47): min > max is the
// CLEAN sentinel; see STAGE_DIRTY_ROW_CLEAN.
if (STAGE_DIRTY_ROW_CLEAN(y)) {
@ -319,8 +393,8 @@ void jlpWaitVBL(void) {
}
// Frame count derived from the millisecond clock (PIT ISR fires or
// uclock -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The
// Frame count derived from the millisecond clock (PIT clocks or audio
// ISR fires -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The
// old implementation edge-polled $3DA bit 3 and only advanced when a
// call happened to land DURING the ~1ms retrace pulse, so any caller
// doing more than ~1ms of work between polls silently lost frames
@ -340,56 +414,58 @@ uint16_t jlpFrameHz(void) {
}
// Wall-clock ms has two possible sources:
// Wall-clock ms has two possible sources, joined into one timeline:
//
// 1. When the audio HAL has reprogrammed PIT counter 0 for the
// 60 Hz sound dispatcher, uclock()'s sub-tick read is wrong
// (uclock assumes 0..65535 cycles; ours is 0..gPitDivisor).
// We bypass uclock and derive ms from the ISR fire counter,
// which advances exactly once per 1/gAudioIsrHz second wall.
// 1. pitClocksNow (above), from jlpInit's zero.
// 2. While the audio HAL drives counter 0 at its own rate the count no
// longer spans a BIOS tick, so the audio ISR's fire counter is used
// instead: each fire is 1000/hz ms, hardware-IRQ-locked.
//
// 2. Otherwise (audio ISR not installed), uclock works correctly
// because PIT 0 is at the BIOS default and DJGPP's sub-tick
// math holds.
// When the source changes -- audio starts, stops, re-registers at another
// rate or restarts its counter -- the new source is rebased onto the last
// value returned, so the clock never jumps back.
//
// gAudioIsr* are owned by src/dos/audio.c.
extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive;
static uclock_t gMillisBase = 0;
static bool gMillisBaseSet = false;
uint32_t jlpMillisElapsed(void) {
uclock_t delta;
uint32_t ms;
uint32_t fires;
uint16_t hz;
if (gAudioIsrActive && gAudioIsrHz > 0u) {
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
// wall-clock-correct and monotonic. uint64 promotion in
// the multiply keeps 60 Hz * 4M ticks (~18 hours) from
// overflowing.
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
/ (uint32_t)gAudioIsrHz);
hz = (gAudioIsrActive && gAudioIsrHz > 0u) ? gAudioIsrHz : 0u;
if (hz != 0u) {
// uint64 promotion keeps 60 Hz * 4M fires (~18 hours) in range.
fires = gAudioIsrFires;
ms = (uint32_t)(((uint64_t)fires * 1000uL) / (uint32_t)hz);
} else {
// Pre-scale by 1000 before dividing by the PIT rate: dividing
// by a truncated clocks-per-ms (1193) would run ~0.015% fast.
fires = 0u;
ms = (uint32_t)(((pitClocksNow() - gPitStart) * 1000uL) / PIT_CLOCK_HZ);
}
// Use an explicit one-shot flag rather than gMillisBase == 0, so a
// legitimate uclock() value of 0 on the first call cannot re-arm
// the base every call.
if (!gMillisBaseSet) {
gMillisBase = uclock();
gMillisBaseSet = true;
return 0u;
if (hz != gMillisLastHz || fires < gMillisLastIsr) {
gMillisRebase = gMillisLast - ms;
gMillisLastHz = hz;
}
// Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of
// dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not
// 1193.18), which would run the clock ~0.015% fast. The uint64
// intermediate keeps delta*1000 from overflowing.
delta = uclock() - gMillisBase;
return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
gMillisLastIsr = fires;
gMillisLast = ms + gMillisRebase;
return gMillisLast;
}
void jlpShutdown(void) {
__dpmi_regs regs;
int wasEnabled;
// Counter 0 back to the BIOS's own mode 3 at its own rate.
wasEnabled = disable();
dosPitProgram(PIT_CTRL_T0_MODE3, PIT_BIOS_DIVISOR);
if (wasEnabled) {
enable();
}
if (gNearEnabled) {
__djgpp_nearptr_disable();

View file

@ -42,7 +42,6 @@
// ----- Constants -----
#define NO_VOICE 0 // audio.lib voice ids are > 0
#define NO_TIMER (-1)
// JoeyLib SFX are signed 8-bit; DVX's 8-bit PCM is unsigned.
#define PCM8_SIGN_FLIP 0x80u
@ -69,10 +68,6 @@
#define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ
#define NOISE_MIN_HZ (NOISE_CURVE_TOP_HZ - NOISE_PITCH_MAX * NOISE_CURVE_STEP_HZ)
// Pinned for the timer interrupt: the tick counter's code (one small
// function; a page is as fine a grain as pinning has) and data.
#define TICK_ISR_LOCK_BYTES 4096u
// ----- Types -----
// One SFX slot: a one-shot sample or a stream (or neither), its voice and
@ -142,9 +137,8 @@ static uint32_t gPcmSize = 0;
// jlAudioTickRegister: the callback, its DVX timer and the ticks the timer
// has counted that are not delivered yet.
static jlAudioTickFnT gTickFn = NULL;
static int32_t gTickTimer = NO_TIMER;
static int32_t gTickTimer = DVX_PORT_NO_TIMER;
static volatile uint32_t gTicksDue = 0;
static bool gTickPinned = false;
// ----- Internal helpers (alphabetical) -----
@ -275,17 +269,12 @@ static void tickIsr(void *userData) {
static void tickStop(void) {
if (gTickTimer != NO_TIMER) {
platformTimerRemove(gTickTimer);
gTickTimer = NO_TIMER;
if (gTickTimer != DVX_PORT_NO_TIMER) {
dvxPortTimerStop(gTickTimer, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue));
gTickTimer = DVX_PORT_NO_TIMER;
}
gTickFn = NULL;
gTicksDue = 0;
if (gTickPinned) {
(void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true);
(void)platformUnlockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false);
gTickPinned = false;
}
}
@ -462,19 +451,10 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
if (fn == NULL) {
return true;
}
if (platformLockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true) != 0) {
return false;
}
if (platformLockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false) != 0) {
(void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true);
return false;
}
gTickPinned = true;
gTickFn = fn;
gTickTimer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, tickIsr, NULL);
if (gTickTimer < 0) {
gTickTimer = NO_TIMER;
tickStop();
gTickTimer = dvxPortTimerStart(hz, tickIsr, (const void *)&gTicksDue, sizeof(gTicksDue));
if (gTickTimer == DVX_PORT_NO_TIMER) {
gTickFn = NULL;
return false;
}
return true;

View file

@ -13,6 +13,8 @@
#include <stdbool.h>
#include <stdint.h>
#include "joey/surface.h"
// The program's task stack. DVX's default (32 KB) is far below what a DOS
// program gets, and JoeyLib programs keep sizable buffers on the stack.
#define DVX_PORT_STACK_BYTES (256 * 1024)
@ -40,6 +42,15 @@ void dvxPortExit(int status) __attribute__((noreturn));
// main's status.
int32_t dvxPortRun(DxeAppContextT *ctx);
// A timer-service callback at hz with its code (the page(s) from fn on) and
// dataBytes of data pinned, as DVX requires of anything its timer interrupt
// runs. Returns the timer, or DVX_PORT_NO_TIMER with nothing left pinned.
// dvxPortTimerStop removes the timer (if any) and unpins both.
#define DVX_PORT_NO_TIMER (-1)
#define DVX_PORT_ISR_LOCK_BYTES 4096u
int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes);
void dvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes);
// ----- hal.c -----
extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown
@ -52,6 +63,11 @@ void dvxPortEndIfClosed(void);
// Alt+Enter); the choice is kept for the program's next run.
void dvxPortFullScreenToggle(void);
// UBER_PROBE's display parity row (examples/uber declares it itself): the
// pixels where the stage is shown that differ from the stage, and how many
// were compared; 0xFFFFFFFF with nowhere to show it.
uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
// Called where a JoeyLib program comes back to the port every frame (the
// input poll, the present): yield to the desktop if the program has not
// this virtual frame, then dvxPortEndIfClosed.

View file

@ -75,6 +75,7 @@
// $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF).
#define RGB12_RED_SHIFT 8
#define RGB12_GREEN_SHIFT 4
#define RGB12_BLUE_SHIFT 0
#define RGB12_CHANNEL_MASK 0x0Fu
#define RGB12_TO_RGB24 17u
@ -88,6 +89,9 @@
#define PALETTES_NONE 0x0000u
#define PALETTES_ALL 0xFFFFu
// dvxPortProbeDisplayMismatch with nowhere the stage is shown.
#define PROBE_NO_TARGET 0xFFFFFFFFu
// Game menu commands.
#define CMD_GAME_FULL_SCREEN 100
#define CMD_GAME_HELP 101
@ -126,6 +130,7 @@ typedef struct {
static uint64_t clockFrame(void);
static uint64_t clockMicros(void);
static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1);
static void frameTickIsr(void *userData);
static void fullSet(bool on);
static void lutBuild(const jlSurfaceT *src, uint16_t palettes);
static LutKindE lutKind(const DisplayT *d);
@ -135,13 +140,17 @@ static void onFullChange(WindowT *win, ExclusiveStateE state, void *u
static void onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty);
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show);
static uint32_t probePixelRead(const uint8_t *p, int32_t bytesPerPixel);
static uint32_t probeStagePixel(const DisplayT *d, LutKindE kind, const jlSurfaceT *src, int32_t x, int32_t y);
static void redrawAll(void);
static uint8_t rgb12Channel(uint16_t rgb, uint8_t shift);
static bool settingFullScreenLoad(void);
static void settingFullScreenSave(bool on);
static const DisplayT *targetDisplay(void);
static bool targetGet(PresentTargetT *t);
static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h);
static bool windowCreate(AppContextT *ac);
static void yieldMark(uint64_t frame);
// ----- Module state -----
@ -151,7 +160,9 @@ static int32_t gScale = 1; // window pixels per stage pixe
static GameFramePacerT gPacer;
static bool gClockStarted = 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,
// until the mode can no longer be set) and whether the screen is its own now
@ -273,6 +284,14 @@ static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y,
}
// The frame timer (DVX_FRAME_HZ, in DVX's timer interrupt): the next
// dvxPortService yields.
static void frameTickIsr(void *userData) {
(void)userData;
gYieldDue = 1u;
}
// Full screen on or off; the Game menu's check mark follows. On: DVX sets
// the mode and tells onFullChange (Active) before this returns. Off: the
// desktop comes back, and the window is redrawn.
@ -315,9 +334,9 @@ static void lutBuild(const jlSurfaceT *src, uint16_t palettes) {
}
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
rgb = src->palette[p][c];
r = (uint8_t)(((rgb >> RGB12_RED_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
g = (uint8_t)(((rgb >> RGB12_GREEN_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
r = rgb12Channel(rgb, RGB12_RED_SHIFT);
g = rgb12Channel(rgb, RGB12_GREEN_SHIFT);
b = rgb12Channel(rgb, RGB12_BLUE_SHIFT);
index = (uint8_t)((p << NIBBLE_BITS) | c);
switch (gLutKind) {
case LUT_INDEXED:
@ -446,6 +465,7 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
int32_t runY0;
int32_t runX0;
int32_t runX1;
int32_t clean;
uint8_t maxWord;
if (!targetGet(&t) || t.pixels == NULL) {
@ -455,15 +475,25 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
runX0 = 0;
runX1 = 0;
for (y = 0; y < t.rows; y++) {
if ((palettes & (1u << src->scb[y])) != 0) {
x0 = 0;
x1 = SURFACE_WIDTH;
} else if (STAGE_DIRTY_ROW_CLEAN(y)) {
// Clean rows end the run: four at a time where no palette change
// forces a redraw (STAGE_DIRTY_GROUP4_CLEAN), else one.
clean = 0;
if (palettes == PALETTES_NONE && (y & (int32_t)(STAGE_DIRTY_GROUP4_ROWS - 1u)) == 0 && y + (int32_t)STAGE_DIRTY_GROUP4_ROWS <= t.rows && STAGE_DIRTY_GROUP4_CLEAN(y >> STAGE_DIRTY_GROUP4_SHIFT)) {
clean = (int32_t)STAGE_DIRTY_GROUP4_ROWS;
} else if ((palettes & (1u << src->scb[y])) == 0 && STAGE_DIRTY_ROW_CLEAN(y)) {
clean = 1;
}
if (clean != 0) {
if (runY0 >= 0 && show) {
targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
}
runY0 = -1;
y += clean - 1;
continue;
}
if ((palettes & (1u << src->scb[y])) != 0) {
x0 = 0;
x1 = SURFACE_WIDTH;
} else {
// Clamp a band past the row end, as the DOS present does.
maxWord = gStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? gStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
@ -492,6 +522,46 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
}
// A pixel of the target, bytesPerPixel wide, as expandRow stores them.
static uint32_t probePixelRead(const uint8_t *p, int32_t bytesPerPixel) {
switch (bytesPerPixel) {
case 1:
return *p;
case 2:
return *(const uint16_t *)p;
default:
return *(const uint32_t *)p;
}
}
// The pixel stage pixel (x, y) must be on display d with tables of the
// given kind, derived from its palette entry rather than through gLut.
static uint32_t probeStagePixel(const DisplayT *d, LutKindE kind, const jlSurfaceT *src, int32_t x, int32_t y) {
uint8_t scb;
uint8_t packed;
uint8_t index;
uint16_t rgb;
uint8_t r;
uint8_t g;
uint8_t b;
scb = src->scb[y];
packed = src->pixels[y * SURFACE_BYTES_PER_ROW + x / STAGE_PIXELS_PER_BYTE];
index = (x % STAGE_PIXELS_PER_BYTE) != 0 ? (uint8_t)(packed & NIBBLE_MASK) : (uint8_t)(packed >> NIBBLE_BITS);
if (kind == LUT_INDEXED) {
return (uint32_t)((scb << NIBBLE_BITS) | index);
}
rgb = src->palette[scb][index];
r = rgb12Channel(rgb, RGB12_RED_SHIFT);
g = rgb12Channel(rgb, RGB12_GREEN_SHIFT);
b = rgb12Channel(rgb, RGB12_BLUE_SHIFT);
return kind == LUT_NEAREST ? gamePalNearest(d->palette, r, g, b) : packColor(d, r, g, b);
}
// Full screen came or went: new tables, and every row again, wherever the
// stage goes now. Before jlInit has the stage the first present does it.
static void redrawAll(void) {
@ -504,6 +574,12 @@ static void redrawAll(void) {
}
// Channel shift of a $0RGB colour, widened to 8 bits.
static uint8_t rgb12Channel(uint16_t rgb, uint8_t shift) {
return (uint8_t)(((rgb >> shift) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
}
// The saved choice: whether the program was on the whole screen when the
// user last switched.
static bool settingFullScreenLoad(void) {
@ -663,6 +739,15 @@ static bool windowCreate(AppContextT *ac) {
}
// The program has yielded in frame `frame`: the next yield is due when frame
// + 1 starts (rounded up to the microsecond, so the frame number there is
// already frame + 1), or when the frame timer next ticks.
static void yieldMark(uint64_t frame) {
gYieldDue = 0u;
gYieldNextUs = ((frame + 1u) * PLATFORM_US_PER_SEC + DVX_FRAME_HZ - 1u) / DVX_FRAME_HZ;
}
// ----- Port API (alphabetical) -----
void dvxPortEndIfClosed(void) {
@ -679,14 +764,89 @@ void dvxPortFullScreenToggle(void) {
}
void dvxPortService(void) {
uint64_t frame;
// UBER_PROBE's display parity row (examples/uber): the pixels of where the
// stage is shown now -- the window's content buffer, or the full-screen
// backbuffer while the program has the screen -- that differ from the
// stage, every screen pixel of each stage pixel's scale x scale block
// counted; on the whole screen at 8 bits also the screen's palette entries
// that differ from the stage's palettes (a palette change there moves no
// pixel). *compared gets how many pixels and entries were checked. What a
// pixel must be comes from the stage's own palette entry, not through the
// present's tables, so a stale or wrong table, row or scale shows; where
// the stage goes is the present's own targetGet, which the screenshot
// gates check. Always compiled: the probe build links the stock library.
uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) {
PresentTargetT t;
const DisplayT *d;
const uint8_t *row;
const uint8_t *entry;
LutKindE kind;
uint32_t want;
uint32_t bad;
int32_t x;
int32_t y;
int32_t sx;
int32_t sy;
uint8_t p;
uint8_t c;
uint16_t rgb;
frame = clockFrame();
if (frame != gYieldFrame) {
gYieldFrame = frame;
*compared = 0u;
if (src == NULL || !targetGet(&t) || t.pixels == NULL) {
return PROBE_NO_TARGET;
}
d = targetDisplay();
kind = lutKind(d);
bad = 0u;
for (y = 0; y < t.rows; y++) {
for (x = 0; x < t.cols; x++) {
want = probeStagePixel(d, kind, src, x, y);
for (sy = 0; sy < t.scale; sy++) {
row = t.pixels + (y * t.scale + sy) * t.pitch + x * t.scale * t.bytesPerPixel;
for (sx = 0; sx < t.scale; sx++) {
if (probePixelRead(row + sx * t.bytesPerPixel, t.bytesPerPixel) != want) {
bad++;
}
(*compared)++;
}
}
}
}
if (kind == LUT_INDEXED) {
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
rgb = src->palette[p][c];
entry = &d->palette[((p << NIBBLE_BITS) | c) * PAL_CHANNELS];
if (entry[0] != rgb12Channel(rgb, RGB12_RED_SHIFT) || entry[1] != rgb12Channel(rgb, RGB12_GREEN_SHIFT) || entry[2] != rgb12Channel(rgb, RGB12_BLUE_SHIFT)) {
bad++;
}
(*compared)++;
}
}
}
return bad;
}
// Yield at most once a frame. With the frame timer this is a byte test: a
// clock read costs ~5 us under DOSBox (a PIT latch), paid by every present,
// input poll and serial call. Without it, a clock read and a compare, the
// division to the frame number once a frame (yieldMark).
void dvxPortService(void) {
uint64_t now;
if (gFrameTimer != DVX_PORT_NO_TIMER) {
if (gYieldDue != 0u) {
gYieldDue = 0u;
tsYield();
}
} else {
now = clockMicros();
if (now >= gYieldNextUs) {
yieldMark(now * DVX_FRAME_HZ / PLATFORM_US_PER_SEC);
tsYield();
}
}
dvxPortEndIfClosed();
}
@ -725,6 +885,8 @@ bool jlpInit(const jlConfigT *config) {
if (!windowCreate(gDvxPortProgram->shellCtx)) {
return false;
}
// Paces dvxPortService's yields; without it the service reads the clock.
gFrameTimer = dvxPortTimerStart(DVX_FRAME_HZ, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue));
if (settingFullScreenLoad()) {
fullSet(true);
}
@ -767,6 +929,10 @@ void jlpPresent(const jlSurfaceT *src) {
// Destroying the window gives the screen back too when the program has it
// (DVX tells onFullChange nothing then).
void jlpShutdown(void) {
if (gFrameTimer != DVX_PORT_NO_TIMER) {
dvxPortTimerStop(gFrameTimer, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue));
gFrameTimer = DVX_PORT_NO_TIMER;
}
if (gDvxPortWindow != NULL) {
dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow);
gDvxPortWindow = NULL;
@ -785,11 +951,11 @@ void jlpWaitVBL(void) {
if (gFullActive) {
tsYield();
dvxExclusiveWaitRetrace();
gYieldFrame = clockFrame();
yieldMark(clockFrame());
} else {
gPacer.frame = clockFrame();
(void)gameFrameWait(&gPacer);
gYieldFrame = gPacer.frame;
yieldMark(gPacer.frame);
}
dvxPortEndIfClosed();
}

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) -----
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
// blocked store merging; memcpy asserts non-overlap and compiles to one
// 32-bit move per row on DOS (DJGPP). Only DOS/BLANK run these generics --
// IIgs uses asm macros and Amiga/ST override every tile op -- so the IIgs
// byte-loop-libc memcpy caveat (PERF-AUDIT #79) does not apply here.
// IIgs uses asm macros and Amiga/ST override every tile op.
#define GENERIC_TILE_COPY_ROWS(_d, _dStride, _s, _sStride) do { \
uint8_t copyRow_; \
for (copyRow_ = 0; copyRow_ < TILE_PIXELS_PER_SIDE; copyRow_++) { \

View file

@ -66,13 +66,6 @@ typedef void *Handle;
extern uint16_t iigsGetTickWord(void);
extern uint16_t iigsVblInstall(void);
extern uint16_t iigsVblRemove(void);
// MVN block copy (peislam.s): one run that crosses no bank on either side.
typedef struct {
uint32_t dst;
uint32_t src;
uint16_t len;
} IigsMvnArgsT;
extern void iigsMvnCopy(const IigsMvnArgsT *args);
/* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */
extern uint16_t iigsReadHzParam(void);
static uint16_t gFrameHz = 60u;
@ -266,10 +259,9 @@ volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group
// start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
// get cleared here after upload.
// Word stores, NOT memcpy: the clang IIgs libc memcpy is a ~30 cyc/byte
// byte loop (finding #79 in palette.c), so the 712 bytes here cost ~21k
// cycles -- 7.6 ms, a third of a frame -- every time both flags fire.
// 16-bit stores land at ~8 cyc/word, ~2.8k cycles for the pair.
// Word stores, NOT memcpy: 16-bit stores land at ~8 cycles a word, ~2.8k
// cycles for the pair, where libc memcpy's MVN moves a byte at a time at
// 7 cycles a byte (~5k).
static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) {
uint16_t i;
@ -455,39 +447,6 @@ void iigsIrqLatencyTighten(void) {
}
// jlMemCopy: MVN moves of up to a bank at a time. MVN's X and Y wrap
// inside their banks, so a run ends where either side would cross one;
// Memory Manager blocks are free to straddle banks.
void iigsMemCopy(void *dst, const void *src, uint32_t bytes) {
static IigsMvnArgsT args;
uint32_t d = (uint32_t)dst;
uint32_t s = (uint32_t)src;
while (bytes != 0u) {
uint32_t run = bytes;
uint32_t toD = 0x10000UL - (d & 0xFFFFu);
uint32_t toS = 0x10000UL - (s & 0xFFFFu);
if (run > toD) {
run = toD;
}
if (run > toS) {
run = toS;
}
if (run > 0xFFFFu) {
run = 0xFFFFu;
}
args.dst = d;
args.src = s;
args.len = (uint16_t)run;
iigsMvnCopy(&args);
d += run;
s += run;
bytes -= run;
}
}
// The Memory Manager owner ID for every JoeyLib allocation on the IIgs
// (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates
// a fresh application ID per call, so it is called exactly once here
@ -617,7 +576,9 @@ void jlpStageFreePixels(uint8_t *pixels) {
// jlpSurfaceCopyRect override (NATIVE-PERF Phase 7; asm since hunt-3
// rank 1). History of this function IS the llvm-mos lesson arc: the
// generic per-row memcpy was a far byte-loop CALL (finding #79); the
// generic per-row memcpy was a far byte-loop CALL (finding #79; libc's
// memcpy is MVN now, but its per-call setup still loses on rows of a few
// words); the
// Phase-7 replacement inlined a C word copy, which the -S output at
// the time showed as lda [dp],y / sta -- but the hunt-3 probe measured
// the SHIPPED build at ~5.3 ms per 16x16 erase, and the current -S

View file

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

View file

@ -5424,26 +5424,18 @@ mdrExit:
; ====================================================================
; iigsStageBandsClearAll(void) / iigsStageBandsMarkAll(void)
; iigsStageBandsClearAll(void)
;
; Constant-fill BOTH dirty-band arrays (gStageMinWord/gStageMaxWord,
; 200 bytes each) without libc memset. llvm-mos lowers memset to
; `jsl memset` -- libc's `while (n--) *d++ = c` byte loop behind a far
; call (finding #79 class) -- and stageDirtyClearAll runs after EVERY
; present, so the two memsets were ~10 ms of every present's fixed
; base. A C word-fill rewrite still emitted ~35 instructions per store
; (llvm-mos re-derives the far-pointer address each iteration), so the
; fill lives here: 100 descending abs,x word stores per array,
; ~12 cyc/word.
; Reset the dirty bands (gStageMinWord/gStageMaxWord, 200 bytes each) to
; the CLEAN sentinels after a present -- min = $FF (STAGE_DIRTY_CLEAN_MIN),
; max = $00 (CLEAN_MAX) -- for only the 8-row groups gStageDirtyGroup says
; were marked, clearing those group bytes too. (It began as a blind fill
; when libc memset was a far-call byte loop; libc's memset is a tight
; 16-bit fill now, but no fill can skip the clean groups.)
;
; ClearAll: min = $FF (STAGE_DIRTY_CLEAN_MIN), max = $00 (CLEAN_MAX)
; MarkAll: min = $00, max = $4F (STAGE_DIRTY_FULL_MAX = 79)
;
; ABI: no args, no return. Same DBR-inherited abs,x write path as
; ABI: no args, no return. Same DBR-inherited abs,x/abs,y write path as
; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT
; phk/plb). php/plp preserves caller widths; A/X are cdecl scratch.
; SURFACE_HEIGHT = 200 -> last word offset 198; `bpl` exits after the
; offset-0 store wraps X to $FFFE (N set).
; phk/plb). php/plp preserves caller widths; A/X/Y are cdecl scratch.
; ====================================================================
.section .text.iigsStageBands,"ax"
.globl iigsStageBandsClearAll
@ -5521,97 +5513,6 @@ isbcDone:
plp
rtl
.globl iigsStageBandsMarkAll
iigsStageBandsMarkAll:
php
rep #0x30
.a16
.i16
ldx #198
lda #0 ; minWord 0 in both bytes
isbmMinLoop:
sta gStageMinWord,x
dex
dex
bpl isbmMinLoop
ldx #198
lda #0x4F4F ; STAGE_DIRTY_FULL_MAX in both bytes
isbmMaxLoop:
sta gStageMaxWord,x
dex
dex
bpl isbmMaxLoop
plp
rtl
; ====================================================================
; iigsByteFill(uint8_t *dst, uint16_t fill, uint16_t count)
;
; Fill `count` bytes at far pointer `dst` with the byte value
; replicated in BOTH bytes of `fill`. Replaces llvm-mos memset
; lowering (`jsl memset` far-call byte loop) for the SCB range fill;
; [dp],y long-indirect stores ignore DBR and the 65816 has no
; alignment fault, so any dst parity works. Odd trailing byte is
; stored first in M=8, then descending word stores. count >= 1
; (caller guards the empty range) and count < 32768 (the descending
; loop exits on N -- fine for the 200-byte SCB and any surface row).
;
; ABI: arg0 dst in A(lo16):X(bank). Stack after JSL: fill @4,s,
; count @6,s; after php+phd+phx+pha (+7): fill @11,s, count @13,s.
; D = SP via tsc/tcd, and SP points at the next FREE byte, so the
; pushed dst long pointer's bytes (lo, hi, bank -- pha last, phx
; first) sit at D+1..D+3: ibfDst = 1, NOT 0. (The 0 equate read one
; garbage byte into the pointer's low byte and slid the rest, writing
; the fill into a layout-roulette address -- DRAW happened to survive
; it, UBER crashed at boot.)
; ====================================================================
.globl iigsByteFill
iigsByteFill:
ibfDst = 1
php
phd
rep #0x30
.a16
.i16
phx ; dst bank word -> D+2..3
pha ; dst lo16 -> D+0..1
tsc
tcd ; D = SP: [ibfDst] = dst far ptr
lda 13,s ; count
tay
lsr a
bcc ibfEven ; even count
dey ; Y = count-1 = offset of odd tail
sep #0x20
.a8
lda 11,s ; fill value (low byte)
sta [ibfDst],y
rep #0x20
.a16
tya ; A = even remainder
bra ibfWords
ibfEven:
tya ; A = count
ibfWords:
beq ibfDone ; count was 1 -> only the tail
tay
dey
dey ; Y = last word offset
lda 11,s ; fill word
ibfLoop:
sta [ibfDst],y
dey
dey
bpl ibfLoop
ibfDone:
pla ; drop dst lo16
plx ; drop dst bank
pld
plp
rtl
; ====================================================================
; iigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank,

View file

@ -121,57 +121,3 @@ iigsReadHzParam:
jsl 0xe10000
pla ; A = result (return value)
rtl
; ----------------------------------------------------------------
; void iigsMvnCopy(const IigsMvnArgsT *args) A = args low, X = bank
;
; +0 uint32_t dst 24-bit destination
; +4 uint32_t src 24-bit source
; +8 uint16_t len bytes, 1..65535, and hal.c guarantees neither
; [dst, dst+len) nor [src, src+len) crosses a bank
; (MVN's X/Y wrap inside one bank).
;
; One MVN: 7 cycles a byte against libc memcpy's byte loop of ~40. The
; two bank operands are patched into the instruction itself (this
; segment is RAM; the write goes through B = K, same section), then
; B is restored -- MVN leaves B pointing at the destination bank.
; ----------------------------------------------------------------
.section .text.iigsMvnCopy,"ax"
.globl iigsMvnCopy
iigsMvnCopy:
php
rep #0x30
.a16
.i16
phb
sta 0xe0 ; args low
txa
sta 0xe2 ; args bank
phk
plb ; B = K for the operand patch
ldy #6
lda [0xe0],y ; src bank (byte at +6, junk high)
sep #0x20
.a8
sta mvnOp+2 ; MVN encodes source bank second
ldy #2
lda [0xe0],y ; dst bank
sta mvnOp+1 ; destination bank first
rep #0x20
.a16
ldy #8
lda [0xe0],y
dec a ; A = len - 1
pha
ldy #4
lda [0xe0],y ; src low
tax
ldy #0
lda [0xe0],y ; dst low
tay
pla
mvnOp:
mvn 0, 0 ; patched: dst bank, src bank (54 dd ss)
plb
plp
rtl

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
// into GVRAM. That is deliberately the slow-but-correct path: it makes all of
// src/generic work unmodified, so the port renders correctly before any native
// primitive exists. Nothing here claims a JL_HAS_* override yet.
// The stage is native planar (src/x68000/halPlanar.c) and jlpPresent copies
// its dirty rows into the four 1bpp TEXT planes at $E00000, which are planar
// too (x68kPresentText, src/x68000/x68kPresent.s). The text plane has one
// 16-colour palette, so the SCB's per-line palettes are reloaded as the beam
// scans by a CRTC raster interrupt (x68kRasterIsr.s). The graphics-plane
// present (X68K_PRESENT_TEXT 0) is the alternative that needs no interrupt.
//
// GVRAM lives at $C00000. In 16-colour mode a pixel occupies the low nibble of
// its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so
// one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address
// space per displayed pixel -- the reason this path is a stepping stone rather
// than the destination. See the storage-model note in joey/platform.h.
//
// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path
// renders, and UBER's captured hashes are byte-identical to the Apple IIgs
// golden reference. Input, serial and audio are separately proven -- see
// claudeDocs/x68000_port.md for the evidence for each.
// VERIFIED UNDER EMULATION (MAME 0.264 + installer/patches/mame): UBER's
// hashes match the Apple IIgs golden reference 51/51, and every subsystem has
// its own gate -- see claudeDocs/x68000_port.md for the evidence for each.
#include <x68k/dos.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 uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL;
// Palette words by lookup, built once from x68kColorFromRgb12 by colorLutBuild:
// a $0RGB colour's red and green nibbles index one table and its blue nibble
// the other, and the two entries OR into exactly x68kColorFromRgb12's word.
// A present converts every colour of each palette that changed, and the
// function's shifts cost ~250 cycles a colour (6.4 ms for all 256, measured on
// examples/scb), the lookup ~70.
#define X68K_LUT_RED_GREEN_ENTRIES 256u
#define X68K_LUT_RED_GREEN_SHIFT 4u
#define X68K_LUT_RED_GREEN_MASK 0xFFu
#define X68K_LUT_BLUE_ENTRIES 16u
#define X68K_LUT_BLUE_MASK 0x0Fu
static uint16_t gColorLutRedGreen[X68K_LUT_RED_GREEN_ENTRIES];
static uint16_t gColorLutBlue[X68K_LUT_BLUE_ENTRIES];
#if X68K_PRESENT_TEXT
// ----- SCB band tables (text-plane present) -----------------------------------
//
@ -346,6 +355,9 @@ static uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL;
// reloads from a table built here -- see "SCB per-band palette" below.
#define X68K_MAX_BANDS SURFACE_HEIGHT
#define X68K_NO_PALETTE 0xFFu // no line's SCB value; starts the first band
#define X68K_BAND_TABLES 2u // the one being walked and the one being built
#define X68K_ALL_PALETTES 0xFFFFu // a bit per palette
typedef char x68kAllPalettesFit[(SURFACE_PALETTE_COUNT == 16u) ? 1 : -1];
// One band as the raster interrupt reads it. x68kRasterIsr.s addresses the
// fields by offset; the size checks below hold both sides to one layout.
@ -357,11 +369,17 @@ typedef struct {
// Everything one frame of reloads reads. There are two: the interrupt walks
// one while the main thread builds the other, and takes the new one up at the
// frame wrap, so rebuilding never disturbs the frame being scanned.
// frame wrap, so rebuilding never disturbs the frame being scanned. A table is
// rebuilt only as far as the stage changed since it was last built: the
// palettes whose bits are clear in currentPalettes, and the bands unless
// bandsCurrent. Both start clear, so a table's first build is a whole one.
typedef struct {
const X68kBandT *end; // one past the last band
X68kBandT bands[X68K_MAX_BANDS];
uint16_t palettes[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
uint16_t bandCount;
uint16_t currentPalettes; // bit p: palettes[p] is the stage's palette p
bool bandsCurrent; // bands[] follow the stage's SCB
} X68kBandTableT;
typedef char x68kBandPaletteAt0[(offsetof(X68kBandT, palette) == 0u) ? 1 : -1];
@ -373,18 +391,35 @@ typedef char x68kTableBandsAt4[(offsetof(X68kBandTableT, bands) == 4u) ? 1 : -1]
extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s
// The present's copy, src/x68000/x68kPresent.s. It carries the geometry as
// constants of its own; these hold each one to the C definition it mirrors.
extern void x68kPresentText(uint8_t *const *planes, const uint8_t *minWord, const uint8_t *maxWord, volatile uint8_t *text);
typedef char x68kPresentRows[(SURFACE_HEIGHT == 200u) ? 1 : -1];
typedef char x68kPresentCleanMin[(STAGE_DIRTY_CLEAN_MIN == 0xFFu) ? 1 : -1];
typedef char x68kPresentFullMax[(STAGE_DIRTY_FULL_MAX == 79u) ? 1 : -1];
typedef char x68kPresentRowBytes[(X68K_BYTES_PER_ROW == 40u) ? 1 : -1];
typedef char x68kPresentPlanes[(X68K_BITPLANES == 4) ? 1 : -1];
typedef char x68kPresentTextStride[(X68K_TVRAM_STRIDE == 128u) ? 1 : -1];
typedef char x68kPresentPlaneStep[(X68K_TVRAM_PLANE_STEP == 0x20000ul) ? 1 : -1];
// It moves longs, so both ends of every band must be at even addresses: the
// plane buffers are malloc'd, and the stage's first byte in the text plane
// is this far into its row.
typedef char x68kPresentOriginEven[((X68K_ORIGIN_BYTE % 2u) == 0u) ? 1 : -1];
// Shared with x68kRasterIsr.s, hence not static. Written by the main thread
// only with every interrupt masked; the interrupt runs to completion.
X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked
X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap
const X68kBandT *volatile gX68kRasterCursor = NULL; // the next band to reload
static X68kBandTableT gBandTables[2];
static X68kBandTableT gBandTables[X68K_BAND_TABLES];
static bool gRasterArmed = false;
static uint32_t gRasterOldVec = 0ul; // $138 before we took it
#endif
// ----- Prototypes -----
static inline uint16_t colorLookup(uint16_t rgb12);
static void colorLutBuild(void);
static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps);
static void installTickIsr(void);
static void installVdispIsr(void);
@ -400,7 +435,7 @@ static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src);
static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src);
static void clearTextPlane(void);
static void disarmRaster(void);
static void installScb(const jlSurfaceT *src);
static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChanged);
#endif
#if !X68K_PRESENT_TEXT
static void buildSpreadTables(void);
@ -436,6 +471,29 @@ static inline void x68kInterruptsRestore(uint16_t sr) {
}
// ----- Palette words -----------------------------------------------------------
static inline uint16_t colorLookup(uint16_t rgb12) {
return (uint16_t)(gColorLutRedGreen[(rgb12 >> X68K_LUT_RED_GREEN_SHIFT) & X68K_LUT_RED_GREEN_MASK] | gColorLutBlue[rgb12 & X68K_LUT_BLUE_MASK]);
}
// Each entry is x68kColorFromRgb12 of its channels alone, the others zero. The
// fields do not overlap, and each entry carries the shared intensity bit when
// its own channels are non-zero, so the OR has it exactly when the colour is
// not black -- the same word as converting the whole colour.
static void colorLutBuild(void) {
uint16_t i;
for (i = 0u; i < X68K_LUT_RED_GREEN_ENTRIES; i++) {
gColorLutRedGreen[i] = x68kColorFromRgb12((uint16_t)(i << X68K_LUT_RED_GREEN_SHIFT));
}
for (i = 0u; i < X68K_LUT_BLUE_ENTRIES; i++) {
gColorLutBlue[i] = x68kColorFromRgb12(i);
}
}
// ----- Lifecycle ------------------------------------------------------------
bool jlpInit(const jlConfigT *config) {
@ -446,6 +504,7 @@ bool jlpInit(const jlConfigT *config) {
// and stay there. _dos_super(0) returns the old USP, or a negative value
// if we were already supervisor -- in which case do not switch back.
gPrevSsp = _dos_super(0);
colorLutBuild();
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
#if X68K_PRESENT_TEXT
// 16 colours, because the present writes the four 1bpp TEXT planes directly
@ -766,6 +825,9 @@ static void vdispPoll(void) {
// of the previous one is reached by staying in the interrupt, so a run of
// per-line changes holds the interrupt, at IPL 6, for the whole run: about
// one raster (31.8 us) per transition, and about two for a band it re-arms for.
// Interrupt level 5 -- the SCC: the RS-232C port and the mouse -- waits out
// the whole run, which bounds the runs a serial program can use (see "Serial
// under a dense SCB" in claudeDocs/x68000_port.md).
// One band per RUN of equal SCB values, in the raster units the compare wants.
@ -805,16 +867,21 @@ static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src) {
}
// Quantise every palette once into the form the interrupt stores.
// Quantise the palettes the table does not have current into the form the
// interrupt stores.
static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src) {
uint16_t pal;
uint16_t col;
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
if ((table->currentPalettes & (uint16_t)(1u << pal)) != 0u) {
continue;
}
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
table->palettes[pal][col] = x68kColorFromRgb12(src->palette[pal][col]);
table->palettes[pal][col] = colorLookup(src->palette[pal][col]);
}
}
table->currentPalettes = X68K_ALL_PALETTES;
}
@ -845,11 +912,22 @@ static void disarmRaster(void) {
// being scanned finishes with the bands it started with: a fade or an SCB
// change can lag the pixels by up to a frame but never tears one. A
// single-band screen needs no interrupt: its palette is written here, once.
static void installScb(const jlSurfaceT *src) {
// paletteMask has bit p set for each stage palette changed since the last
// call, scbChanged whether the SCB did; both tables note what they now lack,
// and the one being built makes up only that. A palette fade converts one
// palette and keeps its bands, where rebuilding everything cost ~4.8 ms.
static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChanged) {
X68kBandTableT *table;
uint16_t sr;
uint16_t col;
uint16_t t;
for (t = 0u; t < X68K_BAND_TABLES; t++) {
gBandTables[t].currentPalettes &= (uint16_t)~paletteMask;
if (scbChanged) {
gBandTables[t].bandsCurrent = false;
}
}
// Withdraw any queued table first, so the interrupt cannot take up the one
// about to be rewritten; after that the table it walks cannot change.
sr = x68kInterruptsOff();
@ -857,7 +935,11 @@ static void installScb(const jlSurfaceT *src) {
table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0];
x68kInterruptsRestore(sr);
buildScbPalettes(table, src);
if (buildScbBands(table, src) == 1u) {
if (!table->bandsCurrent) {
table->bandCount = buildScbBands(table, src);
table->bandsCurrent = true;
}
if (table->bandCount == 1u) {
disarmRaster();
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
X68K_TEXT_PALETTE[col] = table->bands[0].palette[col];
@ -900,7 +982,7 @@ static void uploadGfxPalette(const jlSurfaceT *src) {
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
X68K_GFX_PALETTE[(pal << 4) | col] = x68kColorFromRgb12(src->palette[pal][col]);
X68K_GFX_PALETTE[(pal << 4) | col] = colorLookup(src->palette[pal][col]);
}
}
}
@ -1020,55 +1102,13 @@ static void clearTextPlane(void) {
}
// Copy one row of one plane into the text plane. Both ends sit at the same
// offset from a 4-aligned base -- the stage plane's row stride is 40 and the
// text plane's is 128, both multiples of 4, and the centring offset
// X68K_ORIGIN_BYTE is 12 -- so once the leading bytes are stepped off, the rest
// moves a long at a time with no shifting. Inline rather than memcpy because a
// full present is 800 of these calls and the call alone was costing more than
// the bytes.
static inline __attribute__((always_inline))
void x68kCopyRowToText(volatile uint8_t *dst, const uint8_t *srcRow, uint16_t count) {
while (count != 0u && (((uintptr_t)dst & 3u) != 0u)) {
*dst++ = *srcRow++;
count--;
}
while (count >= 4u) {
*(volatile uint32_t *)dst = *(const uint32_t *)srcRow;
dst += 4;
srcRow += 4;
count -= 4u;
}
while (count != 0u) {
*dst++ = *srcRow++;
count--;
}
}
// Present to the TEXT plane. The stage and the text plane are both four 1bpp
// planes, so this is a copy -- no pixel conversion, no table, 0.5 bytes of bus
// traffic per pixel. The SCB is honoured by the raster interrupt above rather
// than by a per-pixel palette index.
// than by a per-pixel palette index. The copy, dirty-band walk included, is
// x68kPresentText (src/x68000/x68kPresent.s), whose header has the numbers.
void jlpPresent(const jlSurfaceT *src) {
X68kPlanarT *pd;
const uint8_t *sp;
uint16_t g;
uint16_t y;
uint16_t yEnd;
uint16_t plane;
uint16_t firstByte;
uint16_t rowBytes;
// Four rows of dirty band read as ONE long. A clean row's minWord is the
// 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so
// four clean rows read exactly 0xFFFFFFFF and the whole group skips. This
// is the trick src/dos/hal.c already used and this port was missing:
// visiting all two hundred rows one at a time cost roughly 0.9 ms of EVERY
// present, dirty or not. A failing group still runs the per-row clean test,
// so the rows written are byte-identical to the plain walk.
typedef uint32_t __attribute__((may_alias)) BandGroupT;
const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
if (src == NULL) {
return;
@ -1079,38 +1119,15 @@ void jlpPresent(const jlSurfaceT *src) {
return;
}
// gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume
// both here the way src/iigs/hal.c does. Either one rebuilds the bands,
// because every band table carries its own copy of the palettes.
// both here the way src/iigs/hal.c does. Either one updates the band
// tables, which carry their own copies of the palettes.
if (gStagePaletteDirty || gStageScbDirty) {
installScb(src);
installScb(src, gStagePaletteDirtyMask, gStageScbDirty);
gStagePaletteDirty = 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
void jlpPresent(const jlSurfaceT *src) {

View file

@ -1,11 +1,5 @@
// Sharp X68000 per-surface planar storage.
//
// Step 1 of the storage-model conversion (see claudeDocs/x68000_port.md). Nothing
// includes this yet -- the port still ships the chunky-plus-expand path, which
// is hash-verified against the Apple IIgs golden reference. This header lands
// first so the conversion can proceed in verifiable increments instead of one
// unlandable change.
//
// WHY THIS IS ALMOST amigaPlanar.h
// -------------------------------
// A 320px-wide 4bpp surface is 40 bytes per row per plane on ANY planar
@ -24,13 +18,13 @@
// plane 2 $E40000 plane 3 $E60000
// pixel(x,y) -> plane + y*128 + x/8, bit 7-(x&7), plane 0 = LSB
//
// so jlpPresent scatters the compact 40-byte rows into that 128-byte stride.
// That is the ONLY place the 128 appears, and it is where the win comes from:
// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000
// bytes of GVRAM word writes on the chunky path. Bus traffic is what the
// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see
// claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher
// per-access cost by roughly 2x overall.
// so the present (src/x68000/x68kPresent.s) scatters the compact 40-byte rows
// into that 128-byte stride. That is the ONLY place the 128 matters, and it is
// where the win comes from: 4 planes x 200 rows x 40 bytes = 32,000 bytes per
// frame, against 128,000 bytes of GVRAM word writes on the chunky path. Bus
// traffic is what the measured wait states punish (TVRAM 1.92 cycles/access,
// GVRAM 1.03 -- see claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic
// cut beats TVRAM's higher per-access cost by roughly 2x overall.
//
// Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc,
// because nothing but the CPU ever reads them.
@ -107,15 +101,9 @@ static inline X68kPlanarT *x68kSurfacePlanar(const jlSurfaceT *s) {
}
// Byte offset of pixel (x,y) within one DISPLAY plane, and the bit within it.
// Split out so the present loop and any future asm agree on one definition.
// Byte offset of pixel (x,y) within one DISPLAY plane (x a multiple of 8).
static inline uint32_t x68kTvramOffset(uint16_t x, uint16_t y) {
return ((uint32_t)y * X68K_TVRAM_STRIDE) + (uint32_t)(x >> 3);
}
static inline uint8_t x68kTvramBit(uint16_t x) {
return (uint8_t)(0x80u >> (x & 7u));
}
#endif

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
| consumed here, so running RSDRV's handler afterwards would be wrong.
|
| ONE INTERRUPT, EVERY WAITING BYTE. Whatever holds interrupt level 5 off --
| the SCB raster interrupt staying through a run of per-line palette changes,
| the IOCS Timer C dispatcher -- lets bytes pile up in the chip's FIFO, and the
| handler reads until RR0 says the FIFO is empty rather than one byte per
| interrupt. On the chip that saves an exception per byte after such a hold.
| Under MAME 0.264 it is what keeps the channel working at all: its z80scc
| clears a source's pending bit when the CPU acknowledges it and sets it again
| only when a NEW byte arrives, so a byte left behind by a one-byte handler
| waits for the next one and the channel runs a byte behind, one byte from
| overrun, for good (measured by verify-x68000-serial-mouse.sh's dense-SCB
| variant: after a hold, 1 byte was left in the FIFO after 93% of reads).
|
| RR0 is read without a select: the register pointer is 0 whenever this can
| run (see jlpSerialWrite in serial.c). RR1 needs one, so its select and read
| are made with every interrupt masked -- the Timer C dispatcher, a level 6
| interrupt that runs the IOCS mouse solicitation's own select-then-write on
| channel B, must not come between them. A byte with an error stops the loop:
| the chip holds it at the head of the FIFO for the special-condition handler
| below, which drops it.
|
| Ring protocol (single consumer, single producer): the mainline reads gX68kRxHead,
| this ISR writes gX68kRxTail; the byte is stored BEFORE the tail moves, and the
| this ISR writes gX68kRxTail; each byte is stored BEFORE the tail moves, and the
| 68000 is strictly in-order, so no further interlock is needed. A full ring
| drops the byte -- the frame CRC upstream turns that into a clean retransmit-less
| drop rather than corruption.
@ -24,6 +44,16 @@
.equ SCC_A_CTRL, 0xE98005 | channel A command/status
.equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer)
.equ RING_MASK, 4095 | X68K_RX_RING_CAP - 1 (power of two)
.equ RR0_RX_AVAILABLE, 0 | RR0 bit: a byte is waiting
.equ SCC_RR1, 1 | the register the select names
.equ RR1_ERRORS, 0x70 | framing, overrun, parity
.equ WR0_RESET_IUS, 0x38 | reset highest interrupt under service
.equ WR0_ERROR_RESET, 0x30
.equ SR_MASK_ALL, 0x0700
| Bytes one interrupt reads at most: the chip's three-byte FIFO and one more
| that can complete while it is being read. The bound keeps a stream faster
| than the loop from holding level 5 indefinitely.
.equ DRAIN_MAX, 4
.text
.even
@ -32,19 +62,33 @@
| Channel A "receive character available".
x68kSccRxIsr:
movem.l %d0-%d2/%a0,-(%sp)
move.b SCC_A_DATA,%d0 | the received byte
movem.l %d0-%d3/%a0,-(%sp)
lea gX68kRxRing,%a0
move.w gX68kRxTail,%d1
moveq #DRAIN_MAX - 1,%d3
rxByte:
move.b SCC_A_DATA,%d0 | the received byte
move.w %d1,%d2
addq.w #1,%d2
andi.w #RING_MASK,%d2 | next tail
cmp.w gX68kRxHead,%d2
beq.s rxFull | ring full: drop, CRC upstream recovers
lea gX68kRxRing,%a0
move.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail
move.w %d2,gX68kRxTail
rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS
movem.l (%sp)+,%d0-%d2/%a0
move.w %d2,%d1
move.w %d1,gX68kRxTail
rxFull:
btst #RR0_RX_AVAILABLE,SCC_A_CTRL
beq.s rxDone | the FIFO is empty
move.w %sr,%d0
ori.w #SR_MASK_ALL,%sr
move.b #SCC_RR1,SCC_A_CTRL
move.b SCC_A_CTRL,%d2
move.w %d0,%sr
andi.b #RR1_ERRORS,%d2
dbne %d3,rxByte | the next byte, unless it is in error
rxDone:
move.b #WR0_RESET_IUS,SCC_A_CTRL
movem.l (%sp)+,%d0-%d3/%a0
rte
| Channel A "special receive condition" (overrun / framing error). The byte is
@ -53,7 +97,7 @@ rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS
x68kSccSpIsr:
move.l %d0,-(%sp)
move.b SCC_A_DATA,%d0 | unlatch the offending byte
move.b #0x30,SCC_A_CTRL | WR0: error reset
move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS
move.b #WR0_ERROR_RESET,SCC_A_CTRL
move.b #WR0_RESET_IUS,SCC_A_CTRL
move.l (%sp)+,%d0
rte

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;
}