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