More X68k fixes.
This commit is contained in:
parent
c520b16179
commit
10232e73bb
20 changed files with 2515 additions and 802 deletions
17
README.md
17
README.md
|
|
@ -113,11 +113,12 @@ the DOS stage, is diffed against `tests/goldens/uber/dos.txt`
|
||||||
(`scripts/bench-dvx.sh`, then `tools/diff-uber-hashes`):
|
(`scripts/bench-dvx.sh`, then `tools/diff-uber-hashes`):
|
||||||
|
|
||||||
```
|
```
|
||||||
make -f make/x68000.mk x68000-verify-golden # ~70 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Both 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -262,11 +263,13 @@ void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
|
|
||||||
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
||||||
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
|
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
|
||||||
via the SCB. Every port honours it. The X68000 does it the same way
|
via the SCB. Every port honours it. The X68000 presents to its text
|
||||||
the DOS port does: it renders through the 256-colour graphics plane,
|
plane, which is planar like the stage (so a present is a copy) but has
|
||||||
so all 16 palettes are resident at once and each pixel byte is
|
one 16-colour palette: a raster interrupt reloads that palette wherever
|
||||||
`(scb[y] << 4) | nibble` -- the pixel value selects the band, with no
|
the SCB changes, which for a single-palette screen is never. A build
|
||||||
raster interrupt involved.
|
switch (`X68K_PRESENT_TEXT=0`) uses the 256-colour graphics plane the
|
||||||
|
way the DOS port uses its DAC instead -- each pixel byte is
|
||||||
|
`(scb[y] << 4) | nibble`, exact with no interrupt, but slower.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||||
|
|
|
||||||
|
|
@ -192,11 +192,13 @@ uint16_t jlFrameHz(void);
|
||||||
|
|
||||||
*Parameters:* none.
|
*Parameters:* none.
|
||||||
|
|
||||||
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs,
|
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs
|
||||||
ST NTSC and X68000, 70 on VGA mode 13h and on DVX, whose window has a virtual
|
and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has
|
||||||
70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the same
|
a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the
|
||||||
frame count). The real cadence may drift slightly; this is the figure to
|
same frame count). The X68000 times its display at `jlInit` and rounds to the
|
||||||
divide by when converting frames to seconds.
|
nearest whole number: 55.46 Hz on the machine, 56.07 Hz under MAME, which
|
||||||
|
therefore reports 56. The real cadence may drift slightly; this is the figure
|
||||||
|
to divide by when converting frames to seconds.
|
||||||
|
|
||||||
=== jlMillisElapsed
|
=== jlMillisElapsed
|
||||||
|
|
||||||
|
|
@ -205,9 +207,9 @@ divide by when converting frames to seconds.
|
||||||
uint32_t jlMillisElapsed(void);
|
uint32_t jlMillisElapsed(void);
|
||||||
----
|
----
|
||||||
|
|
||||||
Monotonic millisecond counter since `jlInit`. DOS, ST and DVX read a real
|
Monotonic millisecond counter since `jlInit`. DOS, ST, X68000 and DVX read a
|
||||||
hardware timer; IIgs and Amiga derive it from the frame counter, so the value steps at
|
real hardware timer; IIgs and Amiga derive it from the frame counter, so the
|
||||||
frame granularity there. Two calls separated by a long render report the true
|
value steps at frame granularity there. Two calls separated by a long render report the true
|
||||||
elapsed time, not the number of vertical blanks that fired.
|
elapsed time, not the number of vertical blanks that fired.
|
||||||
|
|
||||||
*Parameters:* none.
|
*Parameters:* none.
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,9 @@ bool jlJoystickConnected(jlJoystickE js);
|
||||||
|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`.
|
|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
*Returns:* `true` if the platform reports a stick on that port.
|
*Returns:* `true` if the platform reports a stick on that port. On the ST and
|
||||||
|
X68000 both ports always report one: an empty port cannot be told from a stick
|
||||||
|
at rest.
|
||||||
|
|
||||||
=== jlJoystickX / jlJoystickY
|
=== jlJoystickX / jlJoystickY
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ The API is uniform; these are the places where what you get differs.
|
||||||
|50 Hz (PAL)
|
|50 Hz (PAL)
|
||||||
|60 Hz (NTSC)
|
|60 Hz (NTSC)
|
||||||
|70 Hz
|
|70 Hz
|
||||||
|60 Hz
|
|55 Hz (56 under MAME)
|
||||||
|70 Hz (virtual; the real retrace on the whole screen)
|
|70 Hz (virtual; the real retrace on the whole screen)
|
||||||
|
|
||||||
|Pixel storage
|
|Pixel storage
|
||||||
|
|
@ -266,6 +266,29 @@ about 3 ms per frame there. It does not re-probe on its own. If a user plugs a
|
||||||
stick in while the program is running, you must call `jlJoystickReset` to
|
stick in while the program is running, you must call `jlJoystickReset` to
|
||||||
resume polling.
|
resume polling.
|
||||||
|
|
||||||
|
On the X68000 both joystick ports always report a stick, as on the ST: an
|
||||||
|
empty port cannot be told from a stick at rest. `JOY_BUTTON_0` is a stick's
|
||||||
|
trigger A and `JOY_BUTTON_1` its trigger B. Holding both directions of an axis
|
||||||
|
reads as the positive one (right, down), as on the Amiga and ST.
|
||||||
|
|
||||||
|
The X68000's display refreshes at 55.46 Hz. `jlFrameHz` times it when the
|
||||||
|
program starts and rounds to the nearest whole number, so it reports 55 --
|
||||||
|
and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
|
||||||
|
`jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system
|
||||||
|
timer to the millisecond.
|
||||||
|
|
||||||
|
Per-scanline palettes cost something on the X68000, and only there. Its
|
||||||
|
display has one 16-colour palette, so the port reloads it as the beam scans: an
|
||||||
|
interrupt stores the next band's colours in the horizontal blanking before the
|
||||||
|
band's first line. A screen with one palette costs nothing and arms no
|
||||||
|
interrupt. Each change of palette between one line and the next costs about
|
||||||
|
one line of CPU time (32 microseconds); a band more than two lines from the
|
||||||
|
one before costs about two. A screen that changes palette on every line
|
||||||
|
therefore spends a third of each frame in that interrupt, during which serial
|
||||||
|
and mouse interrupts wait. Palette and SCB changes take effect at the start of
|
||||||
|
the next frame rather than mid-frame, so a fade can lag its pixels by one
|
||||||
|
frame but never tears.
|
||||||
|
|
||||||
=== A Portability Checklist
|
=== A Portability Checklist
|
||||||
|
|
||||||
Before you call a program portable, confirm:
|
Before you call a program portable, confirm:
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,9 @@ rows. That is the one case where you have to mark the region yourself.
|
||||||
|
|
||||||
==== Frame-Rate Independence
|
==== Frame-Rate Independence
|
||||||
|
|
||||||
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs,
|
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz
|
||||||
ST NTSC, X68000) and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop
|
(X68000), 60 Hz (IIgs, ST NTSC) and 70 Hz (VGA mode 13h, and DVX's virtual
|
||||||
|
rate). A loop
|
||||||
that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than
|
that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than
|
||||||
on a PAL Amiga.
|
on a PAL Amiga.
|
||||||
|
|
||||||
|
|
|
||||||
162
examples/scb/scb.c
Normal file
162
examples/scb/scb.c
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
// SCB stress screen: the per-scanline palette under the heaviest load a
|
||||||
|
// program can reasonably put on it. Written for the ports that honour the SCB
|
||||||
|
// by reloading ONE hardware palette as the beam scans (the X68000's text-plane
|
||||||
|
// present, the Atari ST's Timer B), where every palette change is a deadline
|
||||||
|
// an interrupt has to meet. Portable C only: nothing here knows which port it
|
||||||
|
// runs on.
|
||||||
|
//
|
||||||
|
// All sixteen palettes are in use, and colour i of each palette differs from
|
||||||
|
// colour i of every other palette, so a line shown in the wrong palette shows
|
||||||
|
// the wrong colour everywhere but in colour 0 (black in every palette by
|
||||||
|
// library contract). Twenty 16-pixel vertical stripes run the full height,
|
||||||
|
// stripe s in colour s mod 16, so every line uses every colour.
|
||||||
|
//
|
||||||
|
// Colour i of palette p is $0RGB with red = p, green = i and
|
||||||
|
// blue = (p * 7 + i) mod 16.
|
||||||
|
//
|
||||||
|
// The SCB, in three zones:
|
||||||
|
// lines 0- 63 a new palette every line: 0, 1, ... 15, four times over
|
||||||
|
// lines 64-135 runs of irregular length: run k is 1 + (k * 5 mod 7) lines
|
||||||
|
// (1, 6, 4, 2, 7, 5, 3, repeating) in palette (k * 7 + 3) mod 16,
|
||||||
|
// the last run cut short by the zone's end
|
||||||
|
// lines 136-199 two palettes alternating every line: 6 on even lines, 9 on
|
||||||
|
// odd ones
|
||||||
|
// Every boundary is a palette change, the frame wrap included (line 199 is
|
||||||
|
// palette 9, line 0 palette 0): 147 changes a frame, back to back for the 64
|
||||||
|
// lines of each outer zone. scripts/verify-x68000-scb.sh computes the same
|
||||||
|
// screen from the same rules -- change both together.
|
||||||
|
//
|
||||||
|
// Every pass of the loop redraws and presents the whole stage, so the palette
|
||||||
|
// reloads land while the present is streaming into video memory, and sets all
|
||||||
|
// sixteen palettes again -- to the same colours, so the picture never changes
|
||||||
|
// -- so a port that keeps per-band state rebuilds it every pass while the
|
||||||
|
// frame it describes is on screen. Typed characters are read and dropped, so a
|
||||||
|
// gate can keep the keyboard interrupt busy during the check; ESC exits.
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <joey/joey.h>
|
||||||
|
|
||||||
|
// ----- Constants -----
|
||||||
|
|
||||||
|
#define STRIPE_WIDTH 16u // two bytes of a plane: whole-byte fills
|
||||||
|
#define STRIPE_COUNT (SURFACE_WIDTH / STRIPE_WIDTH)
|
||||||
|
#define COLOR_CHANNEL_MASK 0x0Fu
|
||||||
|
#define RED_SHIFT 8u
|
||||||
|
#define GREEN_SHIFT 4u
|
||||||
|
#define BLUE_PALETTE_STEP 7u
|
||||||
|
|
||||||
|
// Zone boundaries: [0, EVERY_LINE_END) changes every line,
|
||||||
|
// [EVERY_LINE_END, IRREGULAR_END) runs irregularly, the rest alternates.
|
||||||
|
#define EVERY_LINE_END 64u
|
||||||
|
#define IRREGULAR_END 136u
|
||||||
|
#define RUN_LENGTH_STEP 5u
|
||||||
|
#define RUN_LENGTH_SPAN 7u
|
||||||
|
#define RUN_PALETTE_STEP 7u
|
||||||
|
#define RUN_PALETTE_BASE 3u
|
||||||
|
#define EVEN_LINE_PALETTE 6u
|
||||||
|
#define ODD_LINE_PALETTE 9u
|
||||||
|
|
||||||
|
#define CODEGEN_DEFAULT 0u
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void buildPalettes(jlSurfaceT *screen);
|
||||||
|
static void buildScb(jlSurfaceT *screen);
|
||||||
|
static void drawStripes(jlSurfaceT *screen);
|
||||||
|
static bool escapeTyped(void);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void buildPalettes(jlSurfaceT *screen) {
|
||||||
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
uint16_t pal;
|
||||||
|
uint16_t col;
|
||||||
|
uint16_t blue;
|
||||||
|
|
||||||
|
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
|
||||||
|
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
|
||||||
|
blue = (uint16_t)((pal * BLUE_PALETTE_STEP + col) & COLOR_CHANNEL_MASK);
|
||||||
|
colors[col] = (uint16_t)((pal << RED_SHIFT) | (col << GREEN_SHIFT) | blue);
|
||||||
|
}
|
||||||
|
jlPaletteSet(screen, (uint8_t)pal, colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void buildScb(jlSurfaceT *screen) {
|
||||||
|
uint16_t line;
|
||||||
|
uint16_t run;
|
||||||
|
uint16_t runLeft;
|
||||||
|
uint8_t runPalette;
|
||||||
|
|
||||||
|
for (line = 0u; line < EVERY_LINE_END; line++) {
|
||||||
|
jlScbSet(screen, line, (uint8_t)(line % SURFACE_PALETTE_COUNT));
|
||||||
|
}
|
||||||
|
run = 0u;
|
||||||
|
runLeft = 0u;
|
||||||
|
runPalette = 0u;
|
||||||
|
for (line = EVERY_LINE_END; line < IRREGULAR_END; line++) {
|
||||||
|
if (runLeft == 0u) {
|
||||||
|
runLeft = (uint16_t)(1u + (run * RUN_LENGTH_STEP) % RUN_LENGTH_SPAN);
|
||||||
|
runPalette = (uint8_t)((run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % SURFACE_PALETTE_COUNT);
|
||||||
|
run++;
|
||||||
|
}
|
||||||
|
jlScbSet(screen, line, runPalette);
|
||||||
|
runLeft--;
|
||||||
|
}
|
||||||
|
for (line = IRREGULAR_END; line < SURFACE_HEIGHT; line++) {
|
||||||
|
jlScbSet(screen, line, (uint8_t)(((line & 1u) != 0u) ? ODD_LINE_PALETTE : EVEN_LINE_PALETTE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void drawStripes(jlSurfaceT *screen) {
|
||||||
|
uint16_t stripe;
|
||||||
|
|
||||||
|
for (stripe = 0u; stripe < STRIPE_COUNT; stripe++) {
|
||||||
|
jlFillRect(screen, (int16_t)(stripe * STRIPE_WIDTH), 0, STRIPE_WIDTH, SURFACE_HEIGHT, (uint8_t)(stripe % SURFACE_COLORS_PER_PALETTE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Drain the typed-character queue; true if ESC was among them.
|
||||||
|
static bool escapeTyped(void) {
|
||||||
|
int ch;
|
||||||
|
bool escape;
|
||||||
|
|
||||||
|
escape = false;
|
||||||
|
for (ch = jlInputGetChar(); ch >= 0; ch = jlInputGetChar()) {
|
||||||
|
if (ch == JL_CHAR_ESCAPE) {
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escape;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
jlConfigT config;
|
||||||
|
jlSurfaceT *screen;
|
||||||
|
|
||||||
|
config.codegenBytes = CODEGEN_DEFAULT;
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
screen = jlStageGet();
|
||||||
|
buildScb(screen);
|
||||||
|
for (;;) {
|
||||||
|
drawStripes(screen);
|
||||||
|
buildPalettes(screen);
|
||||||
|
jlStagePresent();
|
||||||
|
jlInputPoll();
|
||||||
|
if (escapeTyped()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jlShutdown();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
@ -428,14 +428,13 @@
|
||||||
#define JL_HAS_PRESENT // expand the chunky stage into GVRAM
|
#define JL_HAS_PRESENT // expand the chunky stage into GVRAM
|
||||||
#define JL_HAS_INPUT_INIT
|
#define JL_HAS_INPUT_INIT
|
||||||
#define JL_HAS_INPUT_SHUTDOWN
|
#define JL_HAS_INPUT_SHUTDOWN
|
||||||
#define JL_HAS_INPUT_POLL // _iocs_bitsns key bitmaps -> gKeyState[]
|
#define JL_HAS_INPUT_POLL // IOCS work area keys/mouse, PPI joystick ports
|
||||||
#define JL_HAS_JOYSTICK_RESET
|
#define JL_HAS_JOYSTICK_RESET
|
||||||
#define JL_HAS_WAIT_VBL // _iocs_vsync
|
#define JL_HAS_WAIT_VBL // the V-DISP frame count moving
|
||||||
#define JL_HAS_FRAME_COUNT
|
#define JL_HAS_FRAME_COUNT // V-DISP edges, counted on MFP Timer A via _VDISPST
|
||||||
#define JL_HAS_FRAME_HZ // ~55 Hz in the 31 kHz modes
|
#define JL_HAS_FRAME_HZ // measured at init: 55 on hardware, 56 under MAME
|
||||||
// Chains Human68k's MFP Timer C (~100 Hz). Without this the port fell back to
|
// Human68k's MFP Timer C: 100 Hz off the crystal, read to the millisecond
|
||||||
// the generic clock, which derives ms from the POLLED frame counter and
|
// through its live count (src/x68000/hal.c, "Clocks").
|
||||||
// undercounts exactly in proportion to how slow the op is.
|
|
||||||
#define JL_HAS_MILLIS_ELAPSED
|
#define JL_HAS_MILLIS_ELAPSED
|
||||||
// Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL.
|
// Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL.
|
||||||
#define JL_HAS_DISK_FREE
|
#define JL_HAS_DISK_FREE
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ LIB := $(LIBDIR)/libjoey.a
|
||||||
# RS-232C port (SCC channel A) while the mouse (channel B) moves.
|
# RS-232C port (SCC channel A) while the mouse (channel B) moves.
|
||||||
# TILEBANK the tile-bank gate's program: this port's .tbk bake loaded both
|
# TILEBANK the tile-bank gate's program: this port's .tbk bake loaded both
|
||||||
# ways and pixel-checked, every other target's bake refused.
|
# ways and pixel-checked, every other target's bake refused.
|
||||||
|
# SCB the SCB gate's program: all 16 palettes, 147 palette changes a
|
||||||
|
# frame (every line for 128 of them), for the raster interrupt
|
||||||
|
# that reloads the text plane's one palette as the beam scans.
|
||||||
HELLO_SRC := $(EXAMPLES)/hello/hello.c
|
HELLO_SRC := $(EXAMPLES)/hello/hello.c
|
||||||
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
||||||
DRAW_SRC := $(EXAMPLES)/draw/draw.c
|
DRAW_SRC := $(EXAMPLES)/draw/draw.c
|
||||||
|
|
@ -106,6 +109,7 @@ SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
|
||||||
SERMOUSE_SRC := $(EXAMPLES)/sermouse/sermouse.c
|
SERMOUSE_SRC := $(EXAMPLES)/sermouse/sermouse.c
|
||||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||||
TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c
|
TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c
|
||||||
|
SCB_SRC := $(EXAMPLES)/scb/scb.c
|
||||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||||
|
|
@ -113,7 +117,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c
|
||||||
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
|
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
|
||||||
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
|
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
|
||||||
AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
|
AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
|
||||||
EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK JOY SPRITE AUDIO UBER ADV ADV2 AGI
|
EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 AGI
|
||||||
EXAMPLE_BINS := $(foreach e,$(EXAMPLES_X),$(BINDIR)/$(e).X)
|
EXAMPLE_BINS := $(foreach e,$(EXAMPLES_X),$(BINDIR)/$(e).X)
|
||||||
|
|
||||||
# AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program.
|
# AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program.
|
||||||
|
|
@ -129,7 +133,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-tilebank 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-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
|
||||||
|
|
||||||
|
|
@ -290,13 +294,24 @@ 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
|
||||||
|
|
||||||
|
# Joystick gate: every direction, both triggers and every release on both
|
||||||
|
# ports, held through MAME's pads and read back from JOY.X's display (~10 s).
|
||||||
|
x68000-verify-joy: $(BINDIR)/JOY.X
|
||||||
|
$(REPO_DIR)/scripts/verify-x68000-joy.sh
|
||||||
|
|
||||||
# Tile-bank gate: this port's own .tbk bake loaded both ways must hash like
|
# Tile-bank gate: this port's own .tbk bake loaded both ways must hash like
|
||||||
# the host gate's, and every other target's bake must be refused (~1 min).
|
# the host gate's, and every other target's bake must be refused (~1 min).
|
||||||
x68000-verify-tilebank: $(BINDIR)/TILEBANK.X $(TILEBANK_DATA)
|
x68000-verify-tilebank: $(BINDIR)/TILEBANK.X $(TILEBANK_DATA)
|
||||||
$(REPO_DIR)/scripts/verify-x68000-tilebank.sh
|
$(REPO_DIR)/scripts/verify-x68000-tilebank.sh
|
||||||
|
|
||||||
# Golden-hash gate against the Apple IIgs reference (~70 min: UBER on the
|
# SCB gate: every text-palette reload of a 147-change screen, timed on the
|
||||||
# generic renderer is slow and must reach jlLogFlush before hashes exist).
|
# beam, lands between the last stage pixel of the line above its band and the
|
||||||
|
# band's first (~10 s).
|
||||||
|
x68000-verify-scb: $(BINDIR)/SCB.X
|
||||||
|
$(REPO_DIR)/scripts/verify-x68000-scb.sh
|
||||||
|
|
||||||
|
# Golden-hash gate against the Apple IIgs reference (~1 min: UBER with
|
||||||
|
# one-frame windows, which is all the hashes need).
|
||||||
x68000-verify-golden: $(LIB) $(LIBXMP_AR)
|
x68000-verify-golden: $(LIB) $(LIBXMP_AR)
|
||||||
$(REPO_DIR)/scripts/verify-x68000-golden.sh
|
$(REPO_DIR)/scripts/verify-x68000-golden.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
||||||
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
||||||
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||||
-- one bit from each plane at the same offset.
|
-- one bit from each plane at the same offset.
|
||||||
-- gfx -- crtmod 13, 256-colour GRAPHICS plane at \$C00000, one
|
-- gfx -- crtmod 12, 256-colour GRAPHICS plane at \$C00000, one
|
||||||
-- 16-bit word per pixel, 512 words per row.
|
-- 16-bit word per pixel, 512 words per row.
|
||||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||||
local presentPlane = "$PRESENT_PLANE"
|
local presentPlane = "$PRESENT_PLANE"
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,16 @@
|
||||||
# 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
|
||||||
|
# x68000: the SCB -- every text-palette reload of a 147-change
|
||||||
|
# screen lands in the blanking before its band, timed on the beam
|
||||||
# tilebank each port: examples/tilebank loads the port's own .tbk bake
|
# tilebank each port: examples/tilebank loads the port's own .tbk bake
|
||||||
# of the host fixture both ways, pixel-exact and with the host
|
# of the host fixture both ways, pixel-exact and with the host
|
||||||
# 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)
|
||||||
|
# x68000: also both joysticks -- every direction, both triggers
|
||||||
|
# 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)
|
||||||
|
|
@ -101,6 +105,7 @@ golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/g
|
||||||
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 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
|
||||||
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
|
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
|
||||||
|
|
@ -109,6 +114,7 @@ tilebank dos tilebank scripts/verify-dos-tilebank.sh
|
||||||
tilebank dvx tilebank scripts/verify-dvx-tilebank.sh
|
tilebank dvx tilebank scripts/verify-dvx-tilebank.sh
|
||||||
input iigs input scripts/verify-iigs-input.sh
|
input iigs input scripts/verify-iigs-input.sh
|
||||||
input x68000 input scripts/verify-x68000-input.sh
|
input x68000 input scripts/verify-x68000-input.sh
|
||||||
|
input x68000 joy scripts/verify-x68000-joy.sh
|
||||||
input atarist input scripts/verify-atarist-input.sh
|
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
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
||||||
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
||||||
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||||
-- one bit from each plane at the same offset.
|
-- one bit from each plane at the same offset.
|
||||||
-- gfx -- crtmod 13, 256-colour GRAPHICS plane at \$C00000, one
|
-- gfx -- crtmod 12, 256-colour GRAPHICS plane at \$C00000, one
|
||||||
-- 16-bit word per pixel, 512 words per row.
|
-- 16-bit word per pixel, 512 words per row.
|
||||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||||
local presentPlane = "$PRESENT_PLANE"
|
local presentPlane = "$PRESENT_PLANE"
|
||||||
|
|
|
||||||
280
scripts/verify-x68000-joy.sh
Executable file
280
scripts/verify-x68000-joy.sh
Executable file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-x68000-joy.sh - Joystick gate for the X68000: both ports, every
|
||||||
|
# direction, both triggers, and every release, driven through MAME's joystick
|
||||||
|
# inputs and read back from what examples/joy draws.
|
||||||
|
#
|
||||||
|
# The X68000's two Atari-style ports are 8255 PPI ports A and B ($E9A001,
|
||||||
|
# $E9A003); MAME plugs an FM Towns two-button pad into each (x68k.cpp
|
||||||
|
# MSX_GENERAL_PURPOSE_PORT "townspad"), whose lines reach the PPI exactly as a
|
||||||
|
# stick's would: up, down, left, right on bits 0-3 and triggers A and B on bits
|
||||||
|
# 5 and 6, all active low. The Lua below holds those lines with
|
||||||
|
# ioport_field:set_value, which MAME applies without its 4/8-way filter, so
|
||||||
|
# diagonals are exact.
|
||||||
|
#
|
||||||
|
# JOY.X (examples/joy) redraws, for each stick, a dot at one of nine positions
|
||||||
|
# (the axis signs; green off-centre, grey at rest), two trigger squares, and a
|
||||||
|
# patch that says whether the port reports a stick. The Lua decodes all of it
|
||||||
|
# from the text plane and compares the WHOLE state of BOTH sticks after every
|
||||||
|
# step: each direction alone, two diagonals, each trigger 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 between the
|
||||||
|
# ports shows up too.
|
||||||
|
#
|
||||||
|
# Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128 bytes
|
||||||
|
# per row, leftmost pixel in bit 7; the 320x200 stage is centred at origin
|
||||||
|
# (96, 156).
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-x68000-joy.sh
|
||||||
|
# Env: JOY_PROGRAM run this build of JOY.X instead (negative controls)
|
||||||
|
# Requires: MAME and the X68000 firmware and CG ROM; build/x68000/bin/JOY.X
|
||||||
|
# (make x68000).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
source "$repo/scripts/lib/joeyEnv.sh"
|
||||||
|
joeyNeed MAME mame
|
||||||
|
joeyNeed X68K_DIR x68kFirmware
|
||||||
|
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
|
||||||
|
source "$repo/scripts/lib/x68kDisk.sh"
|
||||||
|
joeyNeedFile "$X68K_SYSTEM_DISK" x68kFirmware
|
||||||
|
|
||||||
|
program="${JOY_PROGRAM:-$repo/build/x68000/bin/JOY.X}"
|
||||||
|
wall=600 # hard cap on MAME, whatever happens to this script
|
||||||
|
frames=20000 # emulated frames before the Lua gives up on the program
|
||||||
|
|
||||||
|
if [ ! -f "$program" ]; then
|
||||||
|
echo "verify-x68000-joy: $program is not built; run: make x68000" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-x68-joy.XXXXXX)
|
||||||
|
mame=""
|
||||||
|
mameChild=""
|
||||||
|
cleanup() {
|
||||||
|
joeyStop "$mameChild" "$mame"
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# The boot disk carries only Human68k and AUTOEXEC; the program lives on the
|
||||||
|
# data disk in B:, which AUTOEXEC makes current.
|
||||||
|
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\JOY.X' || exit 1
|
||||||
|
x68kDataDisk "$work/data.xdf" || exit 1
|
||||||
|
x68kXdf add "$work/data.xdf" "$program" JOY.X > /dev/null || exit 1
|
||||||
|
|
||||||
|
cat > "$work/joy.lua" <<LUA
|
||||||
|
local mem = manager.machine.devices[":maincpu"].spaces["program"]
|
||||||
|
|
||||||
|
local FRAMES = $frames
|
||||||
|
local SAMPLE = 30 -- frames between looks for the program
|
||||||
|
local SETTLE = 20 -- frames between holding the lines and reading back
|
||||||
|
|
||||||
|
-- The port's display contract (src/x68000/x68kPlanar.h).
|
||||||
|
local ORIGIN_X, ORIGIN_Y = 96, 156
|
||||||
|
local TVRAM, PLANE_STEP, TVRAM_STRIDE, PLANES = 0xE00000, 0x20000, 128, 4
|
||||||
|
|
||||||
|
-- examples/joy/joy.c: its layout and colours.
|
||||||
|
local STICK_LEFT = { 24, 216 }
|
||||||
|
local STICK_TOP = 16
|
||||||
|
local FRAME_SIZE = 80
|
||||||
|
local DOT_SIZE = 8
|
||||||
|
local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2
|
||||||
|
local BUTTON_W, BUTTON_H, BUTTON_GAP = 24, 16, 4
|
||||||
|
local BUTTON_TOP = STICK_TOP + FRAME_SIZE + 6
|
||||||
|
local STATUS_W, STATUS_H = 80, 4
|
||||||
|
local STATUS_TOP = BUTTON_TOP + BUTTON_H + 6
|
||||||
|
local COLOR_BACKGROUND, COLOR_DOT_OFF, COLOR_DOT_ON = 0, 2, 3
|
||||||
|
local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5
|
||||||
|
local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7
|
||||||
|
|
||||||
|
-- MAME's pad on each port (townspad.cpp): one field per line.
|
||||||
|
local LINE = { up = 0x001, down = 0x002, left = 0x004, right = 0x008, a = 0x010, b = 0x020 }
|
||||||
|
local PAD = { ":joy1:townspad:PAD", ":joy2:townspad:PAD" }
|
||||||
|
|
||||||
|
local function out(text)
|
||||||
|
io.write("JOY-X68K " .. text .. "\n")
|
||||||
|
io.flush()
|
||||||
|
end
|
||||||
|
|
||||||
|
local fields = {}
|
||||||
|
for stick = 1, 2 do
|
||||||
|
local port = manager.machine.ioport.ports[PAD[stick]]
|
||||||
|
fields[stick] = {}
|
||||||
|
if port then
|
||||||
|
for _, f in pairs(port.fields) do
|
||||||
|
for name, mask in pairs(LINE) do
|
||||||
|
if f.mask == mask then
|
||||||
|
fields[stick][name] = f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for name in pairs(LINE) do
|
||||||
|
if not fields[stick][name] then
|
||||||
|
out("error: " .. PAD[stick] .. " has no " .. name .. " line")
|
||||||
|
manager.machine:exit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stage pixel (x, y) -> its colour index, one bit from each text plane.
|
||||||
|
local function stagePixel(x, y)
|
||||||
|
local sx = ORIGIN_X + x
|
||||||
|
local addr = TVRAM + (ORIGIN_Y + y) * TVRAM_STRIDE + (sx >> 3)
|
||||||
|
local bit = 7 - (sx & 7)
|
||||||
|
local v = 0
|
||||||
|
for p = 0, PLANES - 1 do
|
||||||
|
v = v | (((mem:read_u8(addr + p * PLANE_STEP) >> bit) & 1) << p)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
-- What JOY shows for one stick, as "x,y,a,b,connected": x and y are the axis
|
||||||
|
-- signs (-1, 0, 1), read from which of the nine dot positions is lit.
|
||||||
|
local function readStick(stick)
|
||||||
|
local left = STICK_LEFT[stick]
|
||||||
|
local lit = {}
|
||||||
|
for sy = -1, 1 do
|
||||||
|
for sx = -1, 1 do
|
||||||
|
local c = stagePixel(left + FRAME_SIZE // 2 - DOT_SIZE // 2 + sx * DOT_TRAVEL + DOT_SIZE // 2,
|
||||||
|
STICK_TOP + FRAME_SIZE // 2 - DOT_SIZE // 2 + sy * DOT_TRAVEL + DOT_SIZE // 2)
|
||||||
|
if c ~= COLOR_BACKGROUND then
|
||||||
|
local want = (sx == 0 and sy == 0) and COLOR_DOT_OFF or COLOR_DOT_ON
|
||||||
|
lit[#lit + 1] = (c == want) and string.format("%d,%d", sx, sy) or string.format("dot%d?", c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local dot = (#lit == 1) and lit[1] or ("dots[" .. table.concat(lit, ";") .. "]")
|
||||||
|
local centreX = left + FRAME_SIZE // 2
|
||||||
|
local function button(x)
|
||||||
|
local c = stagePixel(x + BUTTON_W // 2, BUTTON_TOP + BUTTON_H // 2)
|
||||||
|
if c == COLOR_BUTTON_ON then return "1" end
|
||||||
|
if c == COLOR_BUTTON_OFF then return "0" end
|
||||||
|
return "btn" .. c .. "?"
|
||||||
|
end
|
||||||
|
local statusColor = stagePixel(left + (FRAME_SIZE - STATUS_W) // 2 + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
|
||||||
|
local status = (statusColor == COLOR_CONNECTED) and "1" or ((statusColor == COLOR_DISCONNECTED) and "0" or ("st" .. statusColor .. "?"))
|
||||||
|
return dot .. "," .. button(centreX - BUTTON_W - BUTTON_GAP // 2) .. "," .. button(centreX + BUTTON_GAP // 2) .. "," .. status
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The state a set of held lines must produce: positive wins a pair, as on the
|
||||||
|
-- Amiga and ST (src/x68000/hal.c joyUpdate); every port reports a stick.
|
||||||
|
local function expected(held)
|
||||||
|
local function sign(neg, pos)
|
||||||
|
if held[pos] then return 1 end
|
||||||
|
if held[neg] then return -1 end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return string.format("%d,%d,%d,%d,1", sign("left", "right"), sign("up", "down"),
|
||||||
|
held.a and 1 or 0, held.b and 1 or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hold(stick, held)
|
||||||
|
for name, f in pairs(fields[stick]) do
|
||||||
|
if held[name] then
|
||||||
|
f:set_value(1)
|
||||||
|
else
|
||||||
|
f:clear_value()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Each step holds these lines on sticks 1 and 2, then reads both back.
|
||||||
|
local NONE = {}
|
||||||
|
local steps = { { "rest", NONE, NONE } }
|
||||||
|
local singles = {
|
||||||
|
{ "up", { up = true } }, { "down", { down = true } },
|
||||||
|
{ "left", { left = true } }, { "right", { right = true } },
|
||||||
|
{ "upLeft", { up = true, left = true } }, { "downRight", { down = true, right = true } },
|
||||||
|
{ "a", { a = true } }, { "b", { b = true } }, { "ab", { a = true, b = true } },
|
||||||
|
}
|
||||||
|
for stick = 1, 2 do
|
||||||
|
for _, s in ipairs(singles) do
|
||||||
|
local one = (stick == 1) and s[2] or NONE
|
||||||
|
local two = (stick == 2) and s[2] or NONE
|
||||||
|
steps[#steps + 1] = { "s" .. stick .. s[1], one, two }
|
||||||
|
steps[#steps + 1] = { "s" .. stick .. s[1] .. "Released", NONE, NONE }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
steps[#steps + 1] = { "bothSticks", { up = true, right = true, a = true }, { down = true, left = true, b = true } }
|
||||||
|
steps[#steps + 1] = { "bothReleased", NONE, NONE }
|
||||||
|
|
||||||
|
local frame = 0
|
||||||
|
local readyAt = 0
|
||||||
|
local stepAt = 0
|
||||||
|
local step = 0
|
||||||
|
local checks = 0
|
||||||
|
local fails = {}
|
||||||
|
local done = false
|
||||||
|
|
||||||
|
emu.register_frame_done(function()
|
||||||
|
frame = frame + 1
|
||||||
|
if done then return end
|
||||||
|
if readyAt == 0 then
|
||||||
|
-- JOY is up once both status patches are painted, in either colour.
|
||||||
|
if frame % SAMPLE == 0 then
|
||||||
|
local one = stagePixel(STICK_LEFT[1] + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
|
||||||
|
local two = stagePixel(STICK_LEFT[2] + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
|
||||||
|
if (one == COLOR_CONNECTED or one == COLOR_DISCONNECTED) and
|
||||||
|
(two == COLOR_CONNECTED or two == COLOR_DISCONNECTED) then
|
||||||
|
readyAt = frame + SAMPLE
|
||||||
|
out("up f" .. frame)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if frame > FRAMES then
|
||||||
|
out("verdict FAIL: JOY never came up (f" .. frame .. ")")
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if frame < readyAt then return end
|
||||||
|
if step == 0 or frame == stepAt + SETTLE then
|
||||||
|
if step > 0 then
|
||||||
|
local s = steps[step]
|
||||||
|
local want = expected(s[2]) .. " " .. expected(s[3])
|
||||||
|
local got = readStick(1) .. " " .. readStick(2)
|
||||||
|
checks = checks + 1
|
||||||
|
if got ~= want then
|
||||||
|
fails[#fails + 1] = s[1] .. ": got " .. got .. " want " .. want
|
||||||
|
out("step " .. s[1] .. " FAIL got " .. got .. " want " .. want)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
step = step + 1
|
||||||
|
if step > #steps then
|
||||||
|
out(string.format("checks=%d fails=%d", checks, #fails))
|
||||||
|
out("verdict " .. ((#fails == 0) and "PASS" or "FAIL"))
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
hold(1, steps[step][2])
|
||||||
|
hold(2, steps[step][3])
|
||||||
|
stepAt = frame
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
LUA
|
||||||
|
|
||||||
|
# From the work dir, so MAME's cfg/ and nvram/ never persist between runs.
|
||||||
|
# Under timeout so a hung run cannot outlive $wall whatever happens to this
|
||||||
|
# script; mameChild is MAME itself, which cleanup stops first.
|
||||||
|
cd "$work" || exit 1
|
||||||
|
timeout -s KILL "$wall" "$MAME" x68000 -bios ipl10 \
|
||||||
|
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
|
||||||
|
-video none -sound none -nothrottle \
|
||||||
|
-autoboot_script "$work/joy.lua" < /dev/null > "$work/mame.log" 2>&1 &
|
||||||
|
mame=$!
|
||||||
|
mameChild=$(joeyChild "$mame")
|
||||||
|
wait "$mame"
|
||||||
|
mame=""
|
||||||
|
mameChild=""
|
||||||
|
|
||||||
|
out=$(grep '^JOY-X68K' "$work/mame.log")
|
||||||
|
echo "${out:-JOY-X68K: no report from the Lua (MAME died, or hit the ${wall}s cap)}"
|
||||||
|
if grep -q '^JOY-X68K verdict PASS' <<< "$out"; then
|
||||||
|
echo "verify-x68000-joy: PASS"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "verify-x68000-joy: FAIL" >&2
|
||||||
|
exit 1
|
||||||
|
|
@ -22,10 +22,9 @@
|
||||||
# five 127-unit steps (635 > 319 even if two adjacent steps merge into
|
# five 127-unit steps (635 > 319 even if two adjacent steps merge into
|
||||||
# one clamped 127-unit packet).
|
# one clamped 127-unit packet).
|
||||||
#
|
#
|
||||||
# GVRAM geometry (crtmod 13, 256-colour graphics plane): 16-bit word
|
# Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128
|
||||||
# per pixel at $C00000, 512 words per row; the 320x200 stage is centred
|
# bytes per row, leftmost pixel in bit 7; the 320x200 stage is centred at
|
||||||
# at origin (96, 156). Mouse verification row = stage y 199 -> GVRAM
|
# origin (96, 156). Mouse verification row = stage y 199 -> screen y 355.
|
||||||
# y 355.
|
|
||||||
#
|
#
|
||||||
# bash scripts/verify-x68000-mouse.sh
|
# bash scripts/verify-x68000-mouse.sh
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
|
||||||
577
scripts/verify-x68000-scb.sh
Executable file
577
scripts/verify-x68000-scb.sh
Executable file
|
|
@ -0,0 +1,577 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-x68000-scb.sh - SCB gate for the X68000: proves that the per-band
|
||||||
|
# palette reloads the text-plane present depends on land where they must, on
|
||||||
|
# a screen that changes palette 147 times a frame.
|
||||||
|
#
|
||||||
|
# The X68000 port presents to the text plane, which has ONE 16-entry palette,
|
||||||
|
# so it honours the SCB by reloading that palette as the beam scans: a CRTC
|
||||||
|
# raster interrupt (src/x68000/x68kRasterIsr.s) stores a band's sixteen
|
||||||
|
# colours in the horizontal blanking before the band's first line. Each reload
|
||||||
|
# has a window of about 400 dots (~18 us): after the last stage pixel of the
|
||||||
|
# line above and before the first stage pixel of the band. Land early and the
|
||||||
|
# end of the line above shows the new colours; land late and the start of the
|
||||||
|
# band shows the old ones; miss and the whole band does.
|
||||||
|
#
|
||||||
|
# Boots Human68k headless under MAME with SCB.X (examples/scb): all sixteen
|
||||||
|
# palettes, every colour distinct per palette, a change on every line for 128
|
||||||
|
# of the 200 lines, irregular runs for the rest, 20 stripes using every colour
|
||||||
|
# on every line, the whole stage redrawn and every palette set again on every
|
||||||
|
# pass. Once the stripes are up and have settled, the Lua below taps every
|
||||||
|
# write to the text palette ($E82200), to the raster compare (CRTC R09), to
|
||||||
|
# MFP ISRA (the interrupt's exit) and every read of the $138 vector (its
|
||||||
|
# entry), timestamped on the emulated clock -- MAME's x68000 runs the
|
||||||
|
# cycle-exact 68000 core, so a write's timestamp is its bus cycle -- and
|
||||||
|
# converts each to a beam position with the screen's own timing. It then
|
||||||
|
# checks CAPTURE whole frames, computing what every stage line shows from the
|
||||||
|
# program's SCB and palette rules (the same rules as examples/scb/scb.c, not
|
||||||
|
# the output):
|
||||||
|
#
|
||||||
|
# timeline the palette in effect at each line's first stage pixel is that
|
||||||
|
# line's palette, and no palette write lands between its first and
|
||||||
|
# last stage pixel. This is what a CRT shows: a palette write takes
|
||||||
|
# effect at once. Every band start's reload is also measured: how
|
||||||
|
# long after the last stage pixel of the line above it began, and
|
||||||
|
# how long before the band's first stage pixel it finished.
|
||||||
|
# bitmap MAME's own rendering, read back with screen:pixels() on SNAPSHOTS
|
||||||
|
# frames, shows each line in its palette -- or, only where the next
|
||||||
|
# line starts a band, partly in the next line's: MAME draws a line
|
||||||
|
# when the NEXT line's display begins (x68k_crtc.cpp hsync ->
|
||||||
|
# update_partial(scan - 1)), so a reload that lands correctly in
|
||||||
|
# the blanking before a line is drawn onto the line above it, unless
|
||||||
|
# a raster interrupt drew that line first (raster_irq). MAME's
|
||||||
|
# bitmap therefore cannot place a reload to the line; the timeline
|
||||||
|
# can. This check catches gross misplacement independently of the
|
||||||
|
# taps.
|
||||||
|
# exit after ESC the program exits and gives the $138 vector and MFP
|
||||||
|
# channel 14 back -- and the two clock vectors it held while it
|
||||||
|
# ran, which are checked then: $114 (x68kTickIsr, chained onto
|
||||||
|
# Human68k's Timer C) and $134 (x68kVdispIsr, the frame counter
|
||||||
|
# _VDISPST installs, with MFP channel 13 enabled).
|
||||||
|
#
|
||||||
|
# It also reports the interrupt's cost (time inside it per frame), the latency
|
||||||
|
# from each raster match to the vector fetch, and the slack of the compare
|
||||||
|
# write the interrupt's timing rests on (x68kRasterIsr.s step 2).
|
||||||
|
#
|
||||||
|
# Human68k's Timer C tick runs throughout, and text is typed during the whole
|
||||||
|
# capture so the IOCS keyboard interrupt runs too: both are MFP interrupts at
|
||||||
|
# IPL 6, like the raster interrupt, and a raster match that falls inside one of
|
||||||
|
# them is taken late (the Timer C dispatcher used to make reloads miss their
|
||||||
|
# line; see src/x68000/x68kTickIsr.s). The DMAC-driven ADPCM path is not
|
||||||
|
# exercised: JoeyLib's audio does its work on the main thread
|
||||||
|
# (src/x68000/audioPcm.c).
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-x68000-scb.sh
|
||||||
|
# Env: SCB_PROGRAM run this build of SCB.X instead (negative controls)
|
||||||
|
# SCB_SETTLE frames between the stripes appearing and the capture
|
||||||
|
# (default 120; the emulation is deterministic, so a
|
||||||
|
# different value is what samples a different phase
|
||||||
|
# between the frame and Timer C)
|
||||||
|
# SCB_CAPTURE frames captured and checked (default 60)
|
||||||
|
# Requires: MAME and the X68000 firmware and CG ROM; build/x68000/bin/SCB.X
|
||||||
|
# (make x68000).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
source "$repo/scripts/lib/joeyEnv.sh"
|
||||||
|
joeyNeed MAME mame
|
||||||
|
joeyNeed X68K_DIR x68kFirmware
|
||||||
|
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
|
||||||
|
source "$repo/scripts/lib/x68kDisk.sh"
|
||||||
|
joeyNeedFile "$X68K_SYSTEM_DISK" x68kFirmware
|
||||||
|
|
||||||
|
program="${SCB_PROGRAM:-$repo/build/x68000/bin/SCB.X}"
|
||||||
|
wall=600 # hard cap on MAME, whatever happens to this script
|
||||||
|
frames=20000 # emulated frames before the Lua gives up on the program
|
||||||
|
settle="${SCB_SETTLE:-120}"
|
||||||
|
capture="${SCB_CAPTURE:-60}"
|
||||||
|
|
||||||
|
if [ ! -f "$program" ]; then
|
||||||
|
echo "verify-x68000-scb: $program is not built; run: make x68000" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-x68-scb.XXXXXX)
|
||||||
|
mame=""
|
||||||
|
mameChild=""
|
||||||
|
cleanup() {
|
||||||
|
joeyStop "$mameChild" "$mame"
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# The boot disk carries only Human68k and AUTOEXEC; the program lives on the
|
||||||
|
# data disk in B:, which AUTOEXEC makes current.
|
||||||
|
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\SCB.X' || exit 1
|
||||||
|
x68kDataDisk "$work/data.xdf" || exit 1
|
||||||
|
x68kXdf add "$work/data.xdf" "$program" SCB.X > /dev/null || exit 1
|
||||||
|
|
||||||
|
cat > "$work/scb.lua" <<LUA
|
||||||
|
local cpu = manager.machine.devices[":maincpu"]
|
||||||
|
local mem = cpu.spaces["program"]
|
||||||
|
local scr = manager.machine.screens[":screen"]
|
||||||
|
local nat = manager.machine.natkeyboard
|
||||||
|
|
||||||
|
local FRAMES = $frames
|
||||||
|
local CAPTURE = $capture
|
||||||
|
local SETTLE = $settle
|
||||||
|
local SAMPLE = 30 -- frames between looks for the stripes
|
||||||
|
local SNAPSHOTS = { [10] = true, [30] = true, [50] = true }
|
||||||
|
local KEY_EVERY = 60 -- frames between ESC presses once the verdict is in
|
||||||
|
local TYPED = "the quick brown fox jumps over the lazy dog 0123456789 "
|
||||||
|
local EXIT_FRAMES = 1200 -- frames the program gets to exit and hand back \$138
|
||||||
|
|
||||||
|
-- The port's display contract (src/x68000/x68kPlanar.h).
|
||||||
|
local ORIGIN_X, ORIGIN_Y = 96, 156
|
||||||
|
local STAGE_W, STAGE_H = 320, 200
|
||||||
|
local TVRAM, PLANE_STEP, TVRAM_STRIDE = 0xE00000, 0x20000, 128
|
||||||
|
local TEXT_PALETTE, COLORS = 0xE82200, 16
|
||||||
|
local CRTC, CRTC_R09 = 0xE80000, 0xE80012
|
||||||
|
local MFP_IERA, MFP_IMRA, MFP_ISRA = 0xE88007, 0xE88013, 0xE8800F
|
||||||
|
local RASTER_VECTOR = 0x138
|
||||||
|
local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: x68kTickIsr
|
||||||
|
local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: x68kVdispIsr
|
||||||
|
local TIMER_A_BIT = 0x20 -- channel 13 in IERA / IMRA
|
||||||
|
local PROGRAM_RAM_END = 0x01000000 -- IOCS's own test for "a handler is installed"
|
||||||
|
local GPIP6_BIT = 0x40
|
||||||
|
local ISRA_EXIT = 0xBF -- x68kRasterIsr's in-service ack
|
||||||
|
local CPU_HZ = 10000000 -- 40 MHz / 4 (MAME x68k.cpp)
|
||||||
|
local IRQ_HPOS = 2 -- x68k_crtc.cpp: R09 = N fires at (N - 1, 2)
|
||||||
|
|
||||||
|
-- examples/scb/scb.c: the picture, from its rules.
|
||||||
|
local STRIPE_W = 16
|
||||||
|
local EVERY_LINE_END, IRREGULAR_END = 64, 136
|
||||||
|
local RUN_LENGTH_STEP, RUN_LENGTH_SPAN = 5, 7
|
||||||
|
local RUN_PALETTE_STEP, RUN_PALETTE_BASE = 7, 3
|
||||||
|
local EVEN_LINE_PALETTE, ODD_LINE_PALETTE = 6, 9
|
||||||
|
local BLUE_PALETTE_STEP = 7
|
||||||
|
|
||||||
|
local scb = {}
|
||||||
|
do
|
||||||
|
for y = 0, EVERY_LINE_END - 1 do
|
||||||
|
scb[y] = y % COLORS
|
||||||
|
end
|
||||||
|
local run, left, pal = 0, 0, 0
|
||||||
|
for y = EVERY_LINE_END, IRREGULAR_END - 1 do
|
||||||
|
if left == 0 then
|
||||||
|
left = 1 + (run * RUN_LENGTH_STEP) % RUN_LENGTH_SPAN
|
||||||
|
pal = (run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % COLORS
|
||||||
|
run = run + 1
|
||||||
|
end
|
||||||
|
scb[y] = pal
|
||||||
|
left = left - 1
|
||||||
|
end
|
||||||
|
for y = IRREGULAR_END, STAGE_H - 1 do
|
||||||
|
scb[y] = (y % 2 == 1) and ODD_LINE_PALETTE or EVEN_LINE_PALETTE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Colour i of palette p as \$0RGB (colour 0 is black in every palette).
|
||||||
|
local function rgb12(p, i)
|
||||||
|
if i == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return (p << 8) | (i << 4) | ((p * BLUE_PALETTE_STEP + i) % COLORS)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A text palette word is GGGGG RRRRR BBBBB I (MAME x68k.cpp GGGGGRRRRRBBBBBI);
|
||||||
|
-- the top four bits of each five-bit field are the \$0RGB channel.
|
||||||
|
local function wordRgb12(w)
|
||||||
|
return (((w >> 7) & 15) << 8) | (((w >> 12) & 15) << 4) | ((w >> 2) & 15)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A rendered pixel: the top four bits of each 8-bit channel.
|
||||||
|
local function pixelRgb12(px)
|
||||||
|
return (((px >> 20) & 15) << 8) | (((px >> 12) & 15) << 4) | ((px >> 4) & 15)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function out(s)
|
||||||
|
io.write("SCB-X68K " .. s .. "\n")
|
||||||
|
io.flush()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stagePixel(px, py)
|
||||||
|
local x = ORIGIN_X + px
|
||||||
|
local byteOff = (ORIGIN_Y + py) * TVRAM_STRIDE + (x >> 3)
|
||||||
|
local bit = 0x80 >> (x & 7)
|
||||||
|
local v = 0
|
||||||
|
for plane = 0, 3 do
|
||||||
|
if (mem:read_u8(TVRAM + plane * PLANE_STEP + byteOff) & bit) ~= 0 then
|
||||||
|
v = v | (1 << plane)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stripesUp()
|
||||||
|
for s = 0, STAGE_W // STRIPE_W - 1 do
|
||||||
|
if stagePixel(s * STRIPE_W + STRIPE_W // 2, STAGE_H // 2) ~= s % COLORS then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Screen timing, filled in when the capture starts.
|
||||||
|
local fp, sp, pp, dotsPerLine, vbegin, stageDot0, zero0
|
||||||
|
local function posTime(frameIdx, line, dot)
|
||||||
|
return zero0 + frameIdx * fp + line * sp + dot * pp
|
||||||
|
end
|
||||||
|
local function dotsOf(seconds)
|
||||||
|
return seconds / pp
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Event log: kind 1 palette write, 2 R09 write, 3 vector fetch, 4 ISRA exit.
|
||||||
|
local evT, evKind, evOff, evData = {}, {}, {}, {}
|
||||||
|
local function log(kind, offset, data)
|
||||||
|
local n = #evT + 1
|
||||||
|
evT[n], evKind[n], evOff[n], evData[n] = emu.time(), kind, offset, data
|
||||||
|
end
|
||||||
|
|
||||||
|
local taps, snaps = {}, {}
|
||||||
|
local initialPalette = {}
|
||||||
|
local frame, stripesAt, captureAt, captured = 0, 0, 0, 0
|
||||||
|
local verdictAt, exitDeadline, isrAddress = 0, 0, 0
|
||||||
|
local tickAddress, vdispAddress = 0, 0
|
||||||
|
local done, failed = false, false
|
||||||
|
|
||||||
|
local function fail(msg)
|
||||||
|
failed = true
|
||||||
|
out("FAIL " .. msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startCapture()
|
||||||
|
local r = {}
|
||||||
|
for i = 0, 8 do
|
||||||
|
r[i] = mem:read_u16(CRTC + i * 2)
|
||||||
|
end
|
||||||
|
fp, sp, pp = scr.frame_period, scr.scan_period, scr.pixel_period
|
||||||
|
dotsPerLine = math.floor(sp / pp + 0.5)
|
||||||
|
local lines = math.floor(fp / sp + 0.5)
|
||||||
|
-- x68k_crtc.cpp refresh_mode: hbegin = R02 * 8 + 1; the text row on screen
|
||||||
|
-- line L is L - vbegin, vbegin = R06 (x68k_v.cpp get_text_pixel).
|
||||||
|
vbegin = r[6]
|
||||||
|
stageDot0 = r[2] * 8 + 1 + ORIGIN_X
|
||||||
|
out(string.format("crtc R00=%d R02=%d R03=%d R04=%d R06=%d R07=%d R20=%04X frame=%.3fms line=%.3fus dots=%d lines=%d",
|
||||||
|
r[0], r[2], r[3], r[4], r[6], r[7], mem:read_u16(CRTC + 40), fp * 1e3, sp * 1e6, dotsPerLine, lines))
|
||||||
|
if lines ~= r[4] + 1 then
|
||||||
|
fail(string.format("the display is interlaced or not the mode the port documents (%d lines a frame, R04+1 = %d)", lines, r[4] + 1))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i = 0, COLORS - 1 do
|
||||||
|
initialPalette[i] = mem:read_u16(TEXT_PALETTE + i * 2)
|
||||||
|
end
|
||||||
|
zero0 = emu.time() + scr:time_until_pos(0, 0)
|
||||||
|
taps[#taps + 1] = mem:install_write_tap(TEXT_PALETTE, TEXT_PALETTE + COLORS * 2 - 1, "scbPal", function(offset, data, mask)
|
||||||
|
log(1, offset, data)
|
||||||
|
end)
|
||||||
|
taps[#taps + 1] = mem:install_write_tap(CRTC_R09, CRTC_R09 + 1, "scbR09", function(offset, data, mask)
|
||||||
|
log(2, offset, data)
|
||||||
|
end)
|
||||||
|
taps[#taps + 1] = mem:install_read_tap(RASTER_VECTOR, RASTER_VECTOR + 3, "scbVec", function(offset, data, mask)
|
||||||
|
if offset == RASTER_VECTOR then
|
||||||
|
log(3, offset, data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
taps[#taps + 1] = mem:install_write_tap(MFP_ISRA - 1, MFP_ISRA, "scbIsra", function(offset, data, mask)
|
||||||
|
if (data & mask & 0xFF) == ISRA_EXIT then
|
||||||
|
log(4, offset, data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
isrAddress = mem:read_u32(RASTER_VECTOR)
|
||||||
|
tickAddress = mem:read_u32(TICK_VECTOR)
|
||||||
|
vdispAddress = mem:read_u32(VDISP_VECTOR)
|
||||||
|
local ch13 = mem:read_u8(MFP_IERA) & mem:read_u8(MFP_IMRA) & TIMER_A_BIT
|
||||||
|
out(string.format("clocks: \$114 = %08X, \$134 = %08X, channel 13 %s", tickAddress, vdispAddress, (ch13 ~= 0) and "on" or "off"))
|
||||||
|
if tickAddress >= PROGRAM_RAM_END or vdispAddress >= PROGRAM_RAM_END or ch13 == 0 then
|
||||||
|
fail("the clock interrupts are not the program's while it runs")
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stopTaps()
|
||||||
|
for _, t in ipairs(taps) do
|
||||||
|
t:remove()
|
||||||
|
end
|
||||||
|
taps = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stage span of row y in frame k: [first stage pixel, end of the last).
|
||||||
|
-- Row -1 is the border line above the stage: black in every palette, but it
|
||||||
|
-- is the line whose stage columns a reload for row 0 must not reach.
|
||||||
|
local function spanStart(k, y)
|
||||||
|
return posTime(k, vbegin + ORIGIN_Y + y, stageDot0)
|
||||||
|
end
|
||||||
|
local function spanEnd(k, y)
|
||||||
|
return posTime(k, vbegin + ORIGIN_Y + y, stageDot0 + STAGE_W)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stats(list)
|
||||||
|
table.sort(list)
|
||||||
|
local n = #list
|
||||||
|
if n == 0 then
|
||||||
|
return "none"
|
||||||
|
end
|
||||||
|
local sum = 0
|
||||||
|
for _, v in ipairs(list) do
|
||||||
|
sum = sum + v
|
||||||
|
end
|
||||||
|
return string.format("min %.0f median %.0f mean %.0f max %.0f", list[1], list[(n + 1) // 2], sum / n, list[n])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The timeline check: what a CRT shows, line by line, from the write log.
|
||||||
|
local function checkTimeline()
|
||||||
|
local state = {}
|
||||||
|
for i = 0, COLORS - 1 do
|
||||||
|
state[i] = initialPalette[i]
|
||||||
|
end
|
||||||
|
local ev, nEv = 1, #evT
|
||||||
|
local lines, wrong, torn, bands, missed = 0, 0, 0, 0, 0
|
||||||
|
local early, late = {}, {}
|
||||||
|
local reported = 0
|
||||||
|
-- Frame 0 of the capture is partial (the taps went in mid-frame); frames
|
||||||
|
-- 1..CAPTURE-1 are checked whole.
|
||||||
|
for k = 1, CAPTURE - 1 do
|
||||||
|
for y = 0, STAGE_H - 1 do
|
||||||
|
local s, e = spanStart(k, y), spanEnd(k, y)
|
||||||
|
local prevEnd = (y == 0) and spanEnd(k - 1, STAGE_H - 1) or spanEnd(k, y - 1)
|
||||||
|
local first, last = nil, nil
|
||||||
|
while ev <= nEv and evT[ev] <= s do
|
||||||
|
if evKind[ev] == 1 then
|
||||||
|
state[(evOff[ev] - TEXT_PALETTE) // 2] = evData[ev]
|
||||||
|
if evT[ev] > prevEnd then
|
||||||
|
first = first or evT[ev]
|
||||||
|
last = evT[ev]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ev = ev + 1
|
||||||
|
end
|
||||||
|
local ok = true
|
||||||
|
for i = 0, COLORS - 1 do
|
||||||
|
if wordRgb12(state[i]) ~= rgb12(scb[y], i) then
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local tearAt = nil
|
||||||
|
local look = ev
|
||||||
|
while look <= nEv and evT[look] < e do
|
||||||
|
if evKind[look] == 1 then
|
||||||
|
tearAt = evT[look]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
look = look + 1
|
||||||
|
end
|
||||||
|
lines = lines + 1
|
||||||
|
if not ok then
|
||||||
|
wrong = wrong + 1
|
||||||
|
end
|
||||||
|
if tearAt then
|
||||||
|
torn = torn + 1
|
||||||
|
end
|
||||||
|
local prevY = (y == 0) and (STAGE_H - 1) or (y - 1)
|
||||||
|
if scb[prevY] ~= scb[y] then
|
||||||
|
bands = bands + 1
|
||||||
|
if first then
|
||||||
|
early[#early + 1] = dotsOf(first - spanEnd(k, y - 1))
|
||||||
|
late[#late + 1] = dotsOf(s - last)
|
||||||
|
else
|
||||||
|
missed = missed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (not ok or tearAt) and reported < 12 then
|
||||||
|
reported = reported + 1
|
||||||
|
local shows = {}
|
||||||
|
for i = 0, COLORS - 1 do
|
||||||
|
shows[#shows + 1] = string.format("%03X", wordRgb12(state[i]))
|
||||||
|
end
|
||||||
|
out(string.format(" frame %d line %d: palette %d expected, shows [%s]%s", k, y, scb[y],
|
||||||
|
table.concat(shows, " "),
|
||||||
|
tearAt and string.format(", write %.0f dots into the line's stage pixels", dotsOf(tearAt - s)) or ""))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out(string.format("timeline lines=%d wrong=%d torn=%d bandStarts=%d reloadsFound=%d", lines, wrong, torn, bands, bands - missed))
|
||||||
|
out(" reload start, dots after the last stage pixel of the line above: " .. stats(early))
|
||||||
|
out(" reload end, dots before the band's first stage pixel: " .. stats(late))
|
||||||
|
out(string.format(" one dot = %.1f ns, one CPU cycle = %.2f dots", pp * 1e9, (1 / CPU_HZ) / pp))
|
||||||
|
return wrong == 0 and torn == 0 and missed == 0 and bands > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Interrupt cost, latency and slack from the entry/exit/R09 events. The first
|
||||||
|
-- R09 write after an entry re-points the compare at the raster of the line
|
||||||
|
-- above the band (x68kRasterIsr.s step 2); it must land before that raster
|
||||||
|
-- begins, and its slack is how far ahead it did. The last R09 write before an
|
||||||
|
-- entry is the arm that raised it.
|
||||||
|
local function reportInterrupt()
|
||||||
|
local inside, entries, latencies, slacks = 0, 0, {}, {}
|
||||||
|
local entryAt, armValue, armAt = nil, nil, nil
|
||||||
|
local firstT, lastT = nil, nil
|
||||||
|
local awaitReference = false
|
||||||
|
for n = 1, #evT do
|
||||||
|
local kind = evKind[n]
|
||||||
|
if kind == 3 then
|
||||||
|
entryAt = evT[n]
|
||||||
|
if armValue and armAt then
|
||||||
|
-- The raster match: the first (armValue - 1, IRQ_HPOS) after the arm.
|
||||||
|
local k = math.floor((armAt - zero0) / fp)
|
||||||
|
local assertAt = posTime(k, armValue - 1, IRQ_HPOS)
|
||||||
|
if assertAt < armAt then
|
||||||
|
assertAt = assertAt + fp
|
||||||
|
end
|
||||||
|
latencies[#latencies + 1] = (entryAt - assertAt) * CPU_HZ
|
||||||
|
end
|
||||||
|
armValue = nil
|
||||||
|
awaitReference = true
|
||||||
|
elseif kind == 2 then
|
||||||
|
armValue, armAt = evData[n], evT[n]
|
||||||
|
if awaitReference then
|
||||||
|
local k = math.floor((evT[n] - zero0) / fp)
|
||||||
|
slacks[#slacks + 1] = (posTime(k, evData[n] - 1, IRQ_HPOS) - evT[n]) * CPU_HZ
|
||||||
|
awaitReference = false
|
||||||
|
end
|
||||||
|
elseif kind == 4 and entryAt then
|
||||||
|
inside = inside + (evT[n] - entryAt)
|
||||||
|
entries = entries + 1
|
||||||
|
firstT = firstT or entryAt
|
||||||
|
lastT = evT[n]
|
||||||
|
entryAt = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if entries == 0 then
|
||||||
|
out("interrupt never ran during the capture")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local span = (lastT - firstT) / fp
|
||||||
|
out(string.format("interrupt entries=%d over %.1f frames: %.1f per frame, %.2f ms inside it per frame (%.1f%% of a frame)",
|
||||||
|
entries, span, entries / span, (inside / span) * 1e3, (inside / span) / fp * 100))
|
||||||
|
out(" latency, raster match -> vector fetch (CPU cycles): " .. stats(latencies))
|
||||||
|
out(" slack, step-2 compare write -> its raster (CPU cycles): " .. stats(slacks))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The bitmap check: MAME's own rendering, within MAME's one-line look-ahead.
|
||||||
|
local function checkSnapshot(pix, w, label)
|
||||||
|
local rows, exact, ahead, bad = 0, 0, 0, 0
|
||||||
|
local stripes = STAGE_W // STRIPE_W
|
||||||
|
for y = 0, STAGE_H - 1 do
|
||||||
|
local row = ORIGIN_Y + y -- visible area starts at vbegin
|
||||||
|
local here = scb[y]
|
||||||
|
local next = scb[(y + 1) % STAGE_H]
|
||||||
|
local isExact, isAhead = true, true
|
||||||
|
for s = 0, stripes - 1 do
|
||||||
|
local x = ORIGIN_X + s * STRIPE_W + STRIPE_W // 2
|
||||||
|
local c = pixelRgb12(string.unpack("<I4", pix, (row * w + x) * 4 + 1))
|
||||||
|
local col = s % COLORS
|
||||||
|
if c ~= rgb12(here, col) then
|
||||||
|
isExact = false
|
||||||
|
end
|
||||||
|
if c ~= rgb12(here, col) and c ~= rgb12(next, col) then
|
||||||
|
isAhead = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rows = rows + 1
|
||||||
|
if isExact then
|
||||||
|
exact = exact + 1
|
||||||
|
elseif isAhead then
|
||||||
|
ahead = ahead + 1
|
||||||
|
else
|
||||||
|
bad = bad + 1
|
||||||
|
if bad <= 6 then
|
||||||
|
out(string.format(" bitmap %s line %d: neither palette %d nor the next line's %d", label, y, here, next))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out(string.format("bitmap %s lines=%d exact=%d nextLineAhead=%d wrong=%d", label, rows, exact, ahead, bad))
|
||||||
|
return bad == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
emu.register_frame_done(function()
|
||||||
|
frame = frame + 1
|
||||||
|
if done then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if stripesAt == 0 then
|
||||||
|
if frame % SAMPLE == 0 and stripesUp() then
|
||||||
|
stripesAt = frame
|
||||||
|
captureAt = frame + SETTLE
|
||||||
|
out("stripes up f" .. frame)
|
||||||
|
end
|
||||||
|
elseif captureAt ~= 0 and frame == captureAt then
|
||||||
|
if not startCapture() then
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
nat:post(TYPED .. TYPED .. TYPED)
|
||||||
|
elseif captureAt ~= 0 and frame > captureAt and verdictAt == 0 then
|
||||||
|
captured = captured + 1
|
||||||
|
if SNAPSHOTS[captured] then
|
||||||
|
local pix, w = scr:pixels()
|
||||||
|
snaps[#snaps + 1] = { pix, w, "f" .. captured }
|
||||||
|
end
|
||||||
|
if captured >= CAPTURE then
|
||||||
|
stopTaps()
|
||||||
|
out(string.format("captured %d frames, %d events", CAPTURE, #evT))
|
||||||
|
local ok = checkTimeline()
|
||||||
|
reportInterrupt()
|
||||||
|
for _, sn in ipairs(snaps) do
|
||||||
|
ok = checkSnapshot(sn[1], sn[2], sn[3]) and ok
|
||||||
|
end
|
||||||
|
if not ok then
|
||||||
|
fail("the SCB palette reloads do not land where they must")
|
||||||
|
end
|
||||||
|
verdictAt = frame
|
||||||
|
exitDeadline = frame + EXIT_FRAMES
|
||||||
|
end
|
||||||
|
elseif verdictAt ~= 0 then
|
||||||
|
if frame % KEY_EVERY == 0 then
|
||||||
|
nat:post_coded("{ESC}")
|
||||||
|
end
|
||||||
|
local vec = mem:read_u32(RASTER_VECTOR)
|
||||||
|
local ch14 = (mem:read_u8(MFP_IERA) | mem:read_u8(MFP_IMRA)) & GPIP6_BIT
|
||||||
|
local tick = mem:read_u32(TICK_VECTOR)
|
||||||
|
local vdsp = mem:read_u32(VDISP_VECTOR)
|
||||||
|
local ch13 = (mem:read_u8(MFP_IERA) | mem:read_u8(MFP_IMRA)) & TIMER_A_BIT
|
||||||
|
if vec ~= isrAddress and ch14 == 0 and tick ~= tickAddress and vdsp ~= vdispAddress and ch13 == 0 then
|
||||||
|
out(string.format("exit ok: \$138 = %08X (was the interrupt at %08X), channel 14 off", vec, isrAddress))
|
||||||
|
out(string.format("exit ok: \$114 = %08X, \$134 = %08X, channel 13 off", tick, vdsp))
|
||||||
|
out(failed and "verdict FAIL" or "verdict PASS")
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
elseif frame >= exitDeadline then
|
||||||
|
fail(string.format("the program did not exit and hand back \$138 (%08X) and channel 14, \$114 (%08X), \$134 (%08X) and channel 13", vec, tick, vdsp))
|
||||||
|
out("verdict FAIL")
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not done and frame > FRAMES then
|
||||||
|
out("verdict FAIL: the stripes never appeared or the capture never finished (f" .. frame .. ")")
|
||||||
|
done = true
|
||||||
|
manager.machine:exit()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
LUA
|
||||||
|
|
||||||
|
# From the work dir, so MAME's cfg/ and nvram/ never persist between runs.
|
||||||
|
# Under timeout so a hung run cannot outlive $wall whatever happens to this
|
||||||
|
# script; mameChild is MAME itself, which cleanup stops first. The Lua ends
|
||||||
|
# the run as soon as the verdict is in and the program has exited.
|
||||||
|
cd "$work" || exit 1
|
||||||
|
timeout -s KILL "$wall" "$MAME" x68000 -bios ipl10 \
|
||||||
|
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
|
||||||
|
-video none -sound none -nothrottle \
|
||||||
|
-autoboot_script "$work/scb.lua" < /dev/null > "$work/mame.log" 2>&1 &
|
||||||
|
mame=$!
|
||||||
|
mameChild=$(joeyChild "$mame")
|
||||||
|
wait "$mame"
|
||||||
|
mame=""
|
||||||
|
mameChild=""
|
||||||
|
|
||||||
|
out=$(grep '^SCB-X68K' "$work/mame.log")
|
||||||
|
echo "${out:-SCB-X68K: no report from the Lua (MAME died, or hit the ${wall}s cap)}"
|
||||||
|
if grep -q '^SCB-X68K verdict PASS' <<< "$out"; then
|
||||||
|
echo "verify-x68000-scb: PASS"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "verify-x68000-scb: FAIL" >&2
|
||||||
|
exit 1
|
||||||
|
|
@ -9,11 +9,13 @@
|
||||||
| four plane buffers at once. Currently:
|
| four plane buffers at once. Currently:
|
||||||
|
|
|
|
||||||
| surface68kFillCircle jlFillCircle: the whole exact-disk walk
|
| surface68kFillCircle jlFillCircle: the whole exact-disk walk
|
||||||
| surface68kFillRect jlFillRect: a rect that does not span whole rows
|
| surface68kFillRect jlFillRect and jlSurfaceClear: any on-surface rect
|
||||||
|
|
|
|
||||||
| A span is a partial left byte, a run of whole middle bytes, and a partial
|
| The circle's span is a partial left byte, a run of whole middle bytes, and a
|
||||||
| right byte, in each of four planes. Three things make this much cheaper
|
| partial right byte, in each of four planes. (The rect works in words and
|
||||||
| than the general case, and the PLANEFILL macro below is built around them:
|
| plane by plane instead; its own section below says why.) Three things make
|
||||||
|
| this much cheaper than the general case, and the PLANEFILL macro below is
|
||||||
|
| built around them:
|
||||||
|
|
|
|
||||||
| * A PLANE'S FILL VALUE IS ONLY EVER ALL-ONES OR ALL-ZEROS. It is one bit
|
| * A PLANE'S FILL VALUE IS ONLY EVER ALL-ONES OR ALL-ZEROS. It is one bit
|
||||||
| of the colour smeared across a byte. So a masked edge is not a read,
|
| of the colour smeared across a byte. So a masked edge is not a read,
|
||||||
|
|
@ -293,72 +295,27 @@ fcDone:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| ---- RECTBODY: every row of the rect, for one colour ----
|
| ---- The rect: word edges, and plane by plane ----
|
||||||
|
|
|
|
||||||
| A rect is one span repeated, so the masks and the middle byte count are
|
| A rect is the same span on every row, so everything about it is decided once
|
||||||
| computed once at entry and stay in d2/d3/d4 for the whole shape; the row
|
| at entry, and the rows then run as tight as the span allows. It works in
|
||||||
| loop only walks the offset. d7 counts up by a row's worth of bytes until it
|
| 16-pixel WORDS rather than bytes: a plane row is 20 words, the 68000 needs
|
||||||
| meets a6, which is one row past the last.
|
| only even addresses for word and long stores, so a span is one masked edge
|
||||||
.macro RECTBODY colour
|
| word, a run of whole words, and a second masked edge word -- never a lone
|
||||||
.Lrect\colour:
|
| middle byte, and never a per-row alignment test.
|
||||||
SPAN \colour
|
|
||||||
addi.w #ROW_BYTES,%d7
|
|
||||||
cmp.w %a6,%d7
|
|
||||||
bne.w .Lrect\colour
|
|
||||||
bra.w frDone
|
|
||||||
.endm
|
|
||||||
|
|
||||||
|
|
||||||
| ---- THINPLANE / THINBODY: a rect only one byte wide ----
|
|
||||||
|
|
|
|
||||||
| Worth its own body rather than folding into SPAN. A vertical line is this
|
| The planes run one after another, each through a body that loops over the
|
||||||
| shape (draw.c turns one into a 1px-wide rect), and it has no middle run and
|
| rows. A plane's fill value is all ones or all zeros, so a set plane ORs its
|
||||||
| only one masked byte per row, so the generic span pays for a middle-run test
|
| edge masks in and a clear plane ANDs their complements; the whole words go
|
||||||
| and a second edge it does not need. Here the plane loop is outside the row
|
| out as `move.l %d6,(%a0)+` stores, entered part-way down an unrolled run by a
|
||||||
| loop, which leaves a dbra over a single read-modify-write: d2 holds the one
|
| jump computed once for the rect. An odd count of whole words is folded into
|
||||||
| combined mask and d4 the row count less one.
|
| the left edge, which then stores a long -- the edge mask in its high word,
|
||||||
.macro THINPLANE areg, op
|
| all ones in its low -- for the same cost as the word it absorbs, so the run
|
||||||
move.w %d4,%d5
|
| is always whole longs. That leaves four wide bodies (OR or AND, word or long
|
||||||
lea (\areg,%d7.w),%a0
|
| left edge) and two thin ones for a rect inside a single word.
|
||||||
1: \op\().b %d2,(%a0)
|
|
|
||||||
lea ROW_BYTES(%a0),%a0
|
| A rect of whole rows (w = 320) is one contiguous run per plane and takes the
|
||||||
dbra %d5,1b
|
| movem.l path at the end instead, a row per instruction.
|
||||||
.endm
|
|
||||||
|
|
||||||
|
|
||||||
.macro THINBODY colour
|
|
||||||
.if (\colour) != 0
|
|
||||||
.if ((\colour) & 1)
|
|
||||||
THINPLANE %a2, or
|
|
||||||
.endif
|
|
||||||
.if ((\colour) & 2)
|
|
||||||
THINPLANE %a3, or
|
|
||||||
.endif
|
|
||||||
.if ((\colour) & 4)
|
|
||||||
THINPLANE %a4, or
|
|
||||||
.endif
|
|
||||||
.if ((\colour) & 8)
|
|
||||||
THINPLANE %a5, or
|
|
||||||
.endif
|
|
||||||
.endif
|
|
||||||
.if (\colour) != 15
|
|
||||||
not.b %d2
|
|
||||||
.if !((\colour) & 1)
|
|
||||||
THINPLANE %a2, and
|
|
||||||
.endif
|
|
||||||
.if !((\colour) & 2)
|
|
||||||
THINPLANE %a3, and
|
|
||||||
.endif
|
|
||||||
.if !((\colour) & 4)
|
|
||||||
THINPLANE %a4, and
|
|
||||||
.endif
|
|
||||||
.if !((\colour) & 8)
|
|
||||||
THINPLANE %a5, and
|
|
||||||
.endif
|
|
||||||
.endif
|
|
||||||
bra.w frDone
|
|
||||||
.endm
|
|
||||||
|
|
||||||
|
|
||||||
| 44 saved registers + 4 return address
|
| 44 saved registers + 4 return address
|
||||||
.equ FR_ARG, 48
|
.equ FR_ARG, 48
|
||||||
|
|
@ -368,130 +325,202 @@ fcDone:
|
||||||
.equ FR_W, FR_ARG + 12 + 2
|
.equ FR_W, FR_ARG + 12 + 2
|
||||||
.equ FR_H, FR_ARG + 16 + 2
|
.equ FR_H, FR_ARG + 16 + 2
|
||||||
.equ FR_COLOR, FR_ARG + 20 + 2
|
.equ FR_COLOR, FR_ARG + 20 + 2
|
||||||
|
.equ FR_COLOR_LOW, FR_COLOR + 1 | the byte btst reads
|
||||||
|
|
||||||
|
.equ ROW_PIXELS, ROW_BYTES * 8
|
||||||
|
.equ ROW_WORDS, ROW_BYTES / 2
|
||||||
|
| Whole words between the two edge words, at most, as longs.
|
||||||
|
.equ MIDDLE_LONGS_MAX, (ROW_WORDS - 2) / 2
|
||||||
|
| The second half of gFillWordLut, the right-edge masks.
|
||||||
|
.equ WORD_LUT_RIGHT, 32
|
||||||
|
.equ PIXEL_IN_WORD, 15 | x & 15: the pixel's bit within its word
|
||||||
|
.equ WORD_SHIFT, 4 | x >> 4: the word it is in
|
||||||
|
| The whole-row path's unrolled group: that many movem.l rows per dbra.
|
||||||
|
.equ FLAT_GROUP_ROWS, 8
|
||||||
|
.equ FLAT_GROUP_SHIFT, 3
|
||||||
|
.equ MOVEM_SHIFT, 2 | a movem.l instruction is 4 bytes
|
||||||
|
| d3-d7/a1-a5: the registers one whole-row movem.l stores, which must be a row.
|
||||||
|
.equ FLAT_ROW_REGS, 10
|
||||||
|
.ifne (FLAT_ROW_REGS * 4) - ROW_BYTES
|
||||||
|
.error "the whole-row movem.l must store exactly one row"
|
||||||
|
.endif
|
||||||
|
|
||||||
|
|
||||||
|
| ---- WIDEBODY: one plane of a rect at least two words wide ----
|
||||||
|
| a0 = the first row's left edge word, d5 = rows - 1, d2 = left mask (word, or
|
||||||
|
| long when it takes the first middle word), d3 = right mask, d4 = row advance
|
||||||
|
| from the right edge word to the next left edge, a2 = bytes of move.l
|
||||||
|
| instructions to run (which is the count of middle words), d6 = fill value.
|
||||||
|
| \op is or (set plane) or and (clear plane, masks complemented by the caller).
|
||||||
|
.macro WIDEBODY op, size
|
||||||
|
lea 9f(%pc),%a1
|
||||||
|
suba.w %a2,%a1 | back up over this rect's middle
|
||||||
|
1: \op\().\size %d2,(%a0)+ | left edge
|
||||||
|
jmp (%a1)
|
||||||
|
.rept MIDDLE_LONGS_MAX
|
||||||
|
move.l %d6,(%a0)+
|
||||||
|
.endr
|
||||||
|
9: \op\().w %d3,(%a0) | right edge
|
||||||
|
adda.w %d4,%a0
|
||||||
|
dbra %d5,1b
|
||||||
|
rts
|
||||||
|
.endm
|
||||||
|
|
||||||
|
|
||||||
|
| ---- THINBODY: one plane of a rect inside a single word ----
|
||||||
|
| d2 = the one combined mask. A vertical line is this shape (draw.c turns one
|
||||||
|
| into a 1px-wide rect), so it is a dbra over a single read-modify-write.
|
||||||
|
.macro THINBODY op
|
||||||
|
1: \op\().w %d2,(%a0)
|
||||||
|
lea ROW_BYTES(%a0),%a0
|
||||||
|
dbra %d5,1b
|
||||||
|
rts
|
||||||
|
.endm
|
||||||
|
|
||||||
|
|
||||||
|
| ---- RECTPLANE: run plane \n's body ----
|
||||||
|
| d0 = the first row's left edge offset, d1 = colour, d7 = rows - 1, a3/a4 =
|
||||||
|
| this rect's OR/AND bodies, a6 = the plane table.
|
||||||
|
.macro RECTPLANE n
|
||||||
|
move.l (\n * 4)(%a6),%a0
|
||||||
|
adda.w %d0,%a0
|
||||||
|
move.w %d7,%d5
|
||||||
|
btst #\n,%d1
|
||||||
|
beq.s 1f
|
||||||
|
moveq #-1,%d6
|
||||||
|
jsr (%a3)
|
||||||
|
bra.s 2f
|
||||||
|
1: moveq #0,%d6
|
||||||
|
not.l %d2
|
||||||
|
not.w %d3
|
||||||
|
jsr (%a4)
|
||||||
|
not.l %d2 | the next plane wants them back
|
||||||
|
not.w %d3
|
||||||
|
2:
|
||||||
|
.endm
|
||||||
|
|
||||||
|
|
||||||
|
| ---- FLATPLANE: plane \n of a whole-row rect ----
|
||||||
|
| Its rows are one run, filled backwards from the end with every fill register
|
||||||
|
| written per movem.l: ten registers are forty bytes, one row. The first pass
|
||||||
|
| enters the unrolled group part-way (d1 = bytes of movem.l to skip) so the
|
||||||
|
| dbra loop only ever runs whole groups. d2 = offset one past the last row.
|
||||||
|
.macro FLATPLANE n
|
||||||
|
moveq #0,%d3
|
||||||
|
btst #\n,FR_COLOR_LOW(%sp)
|
||||||
|
beq.s 1f
|
||||||
|
moveq #-1,%d3
|
||||||
|
1: move.l %d3,%d4
|
||||||
|
move.l %d3,%d5
|
||||||
|
move.l %d3,%d6
|
||||||
|
move.l %d3,%d7
|
||||||
|
move.l %d3,%a1
|
||||||
|
move.l %d3,%a2
|
||||||
|
move.l %d3,%a3
|
||||||
|
move.l %d3,%a4
|
||||||
|
move.l %d3,%a5
|
||||||
|
move.l (\n * 4)(%a6),%a0
|
||||||
|
adda.w %d2,%a0
|
||||||
|
move.w FR_H(%sp),%d0
|
||||||
|
addq.w #FLAT_GROUP_ROWS - 1,%d0
|
||||||
|
lsr.w #FLAT_GROUP_SHIFT,%d0
|
||||||
|
subq.w #1,%d0 | whole groups - 1, counting the first
|
||||||
|
jmp 2f(%pc,%d1.w)
|
||||||
|
2: .rept FLAT_GROUP_ROWS
|
||||||
|
movem.l %d3-%d7/%a1-%a5,-(%a0)
|
||||||
|
.endr
|
||||||
|
dbra %d0,2b
|
||||||
|
.endm
|
||||||
|
|
||||||
|
|
||||||
| void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y,
|
| void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y,
|
||||||
| uint16_t w, uint16_t h, uint16_t color);
|
| uint16_t w, uint16_t h, uint16_t color);
|
||||||
|
|
|
|
||||||
| w and h must both be at least 1 -- draw.c clips before it calls, so an empty
|
| w and h must both be at least 1 and the rect on-surface -- draw.c clips
|
||||||
| rect never arrives. A rect covering whole rows is better served by one flat
|
| before it calls, so an empty or overhanging rect never arrives.
|
||||||
| run of words per plane and halPlanar.c keeps that case to itself; everything
|
|
||||||
| else, partial pixel edges included, lands here.
|
|
||||||
surface68kFillRect:
|
surface68kFillRect:
|
||||||
_surface68kFillRect:
|
_surface68kFillRect:
|
||||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||||
move.l FR_PLANES(%sp),%a0
|
move.l FR_PLANES(%sp),%a6
|
||||||
move.l (%a0),%a2
|
|
||||||
move.l 4(%a0),%a3
|
|
||||||
move.l 8(%a0),%a4
|
|
||||||
move.l 12(%a0),%a5
|
|
||||||
lea gFillMaskLut,%a1
|
|
||||||
|
|
||||||
| span geometry, once for the whole rect
|
|
||||||
move.w FR_X(%sp),%d0 | left
|
move.w FR_X(%sp),%d0 | left
|
||||||
move.w FR_W(%sp),%d1
|
move.w FR_W(%sp),%d1
|
||||||
|
cmp.w #ROW_PIXELS,%d1 | on-surface, so w = 320 means x = 0
|
||||||
|
beq.w .Lflat
|
||||||
add.w %d0,%d1
|
add.w %d0,%d1
|
||||||
subq.w #1,%d1 | right
|
subq.w #1,%d1 | right
|
||||||
move.w %d0,%d5
|
lea gFillWordLut,%a0
|
||||||
lsr.w #3,%d5 | first byte
|
move.w %d0,%d2
|
||||||
and.w #7,%d0
|
and.w #PIXEL_IN_WORD,%d2
|
||||||
move.b (%a1,%d0.w),%d2 | left mask
|
add.w %d2,%d2
|
||||||
move.w %d1,%d0
|
move.w (%a0,%d2.w),%d2 | left mask
|
||||||
lsr.w #3,%d0 | last byte
|
move.w %d1,%d3
|
||||||
and.w #7,%d1
|
and.w #PIXEL_IN_WORD,%d3
|
||||||
move.b 8(%a1,%d1.w),%d3 | right mask
|
add.w %d3,%d3
|
||||||
sub.w %d5,%d0
|
move.w WORD_LUT_RIGHT(%a0,%d3.w),%d3 | right mask
|
||||||
bne.s 1f
|
lsr.w #WORD_SHIFT,%d0 | first word
|
||||||
| One byte wide: one combined mask, and d4 becomes the dbra
|
lsr.w #WORD_SHIFT,%d1 | last word
|
||||||
| row count for THINBODY instead of a middle-run length.
|
sub.w %d0,%d1 | d1 = last - first
|
||||||
and.b %d3,%d2
|
|
||||||
move.w FR_H(%sp),%d4
|
|
||||||
subq.w #1,%d4
|
|
||||||
moveq #1,%d6 | take the thin dispatch
|
|
||||||
bra.s 2f
|
|
||||||
1: subq.w #1,%d0
|
|
||||||
move.w %d0,%d4 | middle byte count
|
|
||||||
moveq #0,%d6
|
|
||||||
2: | d7 = y * ROW_BYTES + firstByte, a6 = one row past the last
|
|
||||||
move.w FR_Y(%sp),%d0
|
|
||||||
move.w %d0,%d1
|
|
||||||
lsl.w #5,%d0
|
|
||||||
lsl.w #3,%d1
|
|
||||||
add.w %d1,%d0 | y * 40, as (y << 5) + (y << 3)
|
|
||||||
add.w %d5,%d0
|
|
||||||
move.w %d0,%d7
|
|
||||||
move.w FR_H(%sp),%d0
|
|
||||||
move.w %d0,%d1
|
|
||||||
lsl.w #5,%d0
|
|
||||||
lsl.w #3,%d1
|
|
||||||
add.w %d1,%d0 | h * 40
|
|
||||||
add.w %d7,%d0
|
|
||||||
move.w %d0,%a6
|
|
||||||
|
|
||||||
move.w FR_COLOR(%sp),%d0
|
|
||||||
and.w #15,%d0
|
|
||||||
add.w %d0,%d0
|
add.w %d0,%d0
|
||||||
| The jump table goes through a1: a PC-relative index has only
|
move.w FR_Y(%sp),%d4
|
||||||
| an 8-bit displacement, and the thin table is 4 KB out.
|
mulu.w #ROW_BYTES,%d4
|
||||||
tst.w %d6
|
add.w %d4,%d0 | d0 = the first row's left edge offset
|
||||||
bne.s .Lthin
|
move.w FR_H(%sp),%d7
|
||||||
lea frJump(%pc),%a1
|
subq.w #1,%d7 | rows - 1
|
||||||
bra.s .Ldispatch
|
tst.w %d1
|
||||||
.Lthin:
|
bne.s .Lwide
|
||||||
lea ftJump(%pc),%a1
|
and.w %d3,%d2 | both edges in one word
|
||||||
.Ldispatch:
|
lea frOrThin(%pc),%a3
|
||||||
move.w (%a1,%d0.w),%d1
|
lea frAndThin(%pc),%a4
|
||||||
jmp (%a1,%d1.w)
|
bra.s .Lplanes
|
||||||
|
.Lwide:
|
||||||
frJump:
|
| After the right edge a0 sits on the last word; the next row's
|
||||||
.word frC0-frJump, frC1-frJump, frC2-frJump, frC3-frJump
|
| first word is a row on from the first, 2 * (last - first) back.
|
||||||
.word frC4-frJump, frC5-frJump, frC6-frJump, frC7-frJump
|
moveq #ROW_BYTES,%d4
|
||||||
.word frC8-frJump, frC9-frJump, frC10-frJump, frC11-frJump
|
sub.w %d1,%d4
|
||||||
.word frC12-frJump, frC13-frJump, frC14-frJump, frC15-frJump
|
sub.w %d1,%d4
|
||||||
|
subq.w #1,%d1 | d1 = whole middle words
|
||||||
frC0: RECTBODY 0
|
lea frOrWord(%pc),%a3
|
||||||
frC1: RECTBODY 1
|
lea frAndWord(%pc),%a4
|
||||||
frC2: RECTBODY 2
|
btst #0,%d1
|
||||||
frC3: RECTBODY 3
|
beq.s 1f
|
||||||
frC4: RECTBODY 4
|
| Odd: the left edge takes the first of them, as a long.
|
||||||
frC5: RECTBODY 5
|
swap %d2
|
||||||
frC6: RECTBODY 6
|
move.w #0xFFFF,%d2
|
||||||
frC7: RECTBODY 7
|
subq.w #1,%d1
|
||||||
frC8: RECTBODY 8
|
lea frOrLong(%pc),%a3
|
||||||
frC9: RECTBODY 9
|
lea frAndLong(%pc),%a4
|
||||||
frC10: RECTBODY 10
|
1: move.w %d1,%a2 | words of middle = bytes of move.l to run
|
||||||
frC11: RECTBODY 11
|
.Lplanes:
|
||||||
frC12: RECTBODY 12
|
move.w FR_COLOR(%sp),%d1
|
||||||
frC13: RECTBODY 13
|
RECTPLANE 0
|
||||||
frC14: RECTBODY 14
|
RECTPLANE 1
|
||||||
frC15: RECTBODY 15
|
RECTPLANE 2
|
||||||
|
RECTPLANE 3
|
||||||
ftJump:
|
|
||||||
.word ftC0-ftJump, ftC1-ftJump, ftC2-ftJump, ftC3-ftJump
|
|
||||||
.word ftC4-ftJump, ftC5-ftJump, ftC6-ftJump, ftC7-ftJump
|
|
||||||
.word ftC8-ftJump, ftC9-ftJump, ftC10-ftJump, ftC11-ftJump
|
|
||||||
.word ftC12-ftJump, ftC13-ftJump, ftC14-ftJump, ftC15-ftJump
|
|
||||||
|
|
||||||
ftC0: THINBODY 0
|
|
||||||
ftC1: THINBODY 1
|
|
||||||
ftC2: THINBODY 2
|
|
||||||
ftC3: THINBODY 3
|
|
||||||
ftC4: THINBODY 4
|
|
||||||
ftC5: THINBODY 5
|
|
||||||
ftC6: THINBODY 6
|
|
||||||
ftC7: THINBODY 7
|
|
||||||
ftC8: THINBODY 8
|
|
||||||
ftC9: THINBODY 9
|
|
||||||
ftC10: THINBODY 10
|
|
||||||
ftC11: THINBODY 11
|
|
||||||
ftC12: THINBODY 12
|
|
||||||
ftC13: THINBODY 13
|
|
||||||
ftC14: THINBODY 14
|
|
||||||
ftC15: THINBODY 15
|
|
||||||
|
|
||||||
frDone:
|
|
||||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||||
rts
|
rts
|
||||||
|
|
||||||
|
.Lflat:
|
||||||
|
move.w FR_Y(%sp),%d2
|
||||||
|
add.w FR_H(%sp),%d2
|
||||||
|
mulu.w #ROW_BYTES,%d2 | d2 = one past the last row
|
||||||
|
move.w FR_H(%sp),%d1
|
||||||
|
neg.w %d1
|
||||||
|
and.w #FLAT_GROUP_ROWS - 1,%d1 | rows the first group does not run
|
||||||
|
lsl.w #MOVEM_SHIFT,%d1 | as bytes of movem.l
|
||||||
|
FLATPLANE 0
|
||||||
|
FLATPLANE 1
|
||||||
|
FLATPLANE 2
|
||||||
|
FLATPLANE 3
|
||||||
|
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||||
|
rts
|
||||||
|
|
||||||
|
frOrWord: WIDEBODY or, w
|
||||||
|
frOrLong: WIDEBODY or, l
|
||||||
|
frAndWord: WIDEBODY and, w
|
||||||
|
frAndLong: WIDEBODY and, l
|
||||||
|
frOrThin: THINBODY or
|
||||||
|
frAndThin: THINBODY and
|
||||||
|
|
||||||
|
|
||||||
.data
|
.data
|
||||||
.even
|
.even
|
||||||
|
|
@ -501,3 +530,13 @@ frDone:
|
||||||
gFillMaskLut:
|
gFillMaskLut:
|
||||||
.byte 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01
|
.byte 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01
|
||||||
.byte 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF
|
.byte 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF
|
||||||
|
|
||||||
|
.even
|
||||||
|
| The rect's word edges, the same way round: sixteen left-edge masks (bits at
|
||||||
|
| and to the right of x&15), then sixteen right-edge masks (bits at and to the
|
||||||
|
| left of it), the second half WORD_LUT_RIGHT bytes in.
|
||||||
|
gFillWordLut:
|
||||||
|
.word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF, 0x0FFF, 0x07FF, 0x03FF, 0x01FF
|
||||||
|
.word 0x00FF, 0x007F, 0x003F, 0x001F, 0x000F, 0x0007, 0x0003, 0x0001
|
||||||
|
.word 0x8000, 0xC000, 0xE000, 0xF000, 0xF800, 0xFC00, 0xFE00, 0xFF00
|
||||||
|
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF
|
||||||
|
|
|
||||||
1370
src/x68000/hal.c
1370
src/x68000/hal.c
File diff suppressed because it is too large
Load diff
|
|
@ -55,42 +55,6 @@ extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0,
|
||||||
// untouched. widthWords counts 16-bit words; modBytes is added to the pointer
|
// untouched. widthWords counts 16-bit words; modBytes is added to the pointer
|
||||||
// after each row (0 for a full-stride operation).
|
// after each row (0 for a full-stride operation).
|
||||||
|
|
||||||
// DEST-only block fill: `height` rows of `widthWords` words set or cleared.
|
|
||||||
// Fill a run of 16-bit words with one value, in longs where it can. Marked
|
|
||||||
// always_inline on purpose: the masked fill calls it once PER ROW, and at
|
|
||||||
// 16x16 a row's middle is a word or two, where a real call costs more than
|
|
||||||
// the stores it saves. Measured: routing that middle through a function made
|
|
||||||
// jlFillRect 16x16 run at 0.45x of the word loop it replaced.
|
|
||||||
static inline __attribute__((always_inline))
|
|
||||||
void x68kFillWordRun(uint8_t *d, uint16_t words, uint32_t val32, uint16_t val16) {
|
|
||||||
uint32_t *lp;
|
|
||||||
uint16_t longs;
|
|
||||||
|
|
||||||
if (words != 0u && ((uintptr_t)d & 2u) != 0u) {
|
|
||||||
*(uint16_t *)d = val16;
|
|
||||||
d += 2;
|
|
||||||
words--;
|
|
||||||
}
|
|
||||||
lp = (uint32_t *)d;
|
|
||||||
longs = (uint16_t)(words >> 1);
|
|
||||||
while (longs >= 4u) {
|
|
||||||
lp[0] = val32;
|
|
||||||
lp[1] = val32;
|
|
||||||
lp[2] = val32;
|
|
||||||
lp[3] = val32;
|
|
||||||
lp += 4;
|
|
||||||
longs = (uint16_t)(longs - 4u);
|
|
||||||
}
|
|
||||||
while (longs != 0u) {
|
|
||||||
*lp++ = val32;
|
|
||||||
longs--;
|
|
||||||
}
|
|
||||||
if ((words & 1u) != 0u) {
|
|
||||||
*(uint16_t *)lp = val16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Copy the same rect out of all four planes.
|
// Copy the same rect out of all four planes.
|
||||||
static void x68kCpuCopyPlanes(uint8_t *dst[X68K_BITPLANES], uint8_t *src[X68K_BITPLANES],
|
static void x68kCpuCopyPlanes(uint8_t *dst[X68K_BITPLANES], uint8_t *src[X68K_BITPLANES],
|
||||||
uint16_t offsetBytes, uint16_t widthWords, uint16_t height,
|
uint16_t offsetBytes, uint16_t widthWords, uint16_t height,
|
||||||
|
|
@ -115,51 +79,22 @@ static void x68kCpuCopyPlanes(uint8_t *dst[X68K_BITPLANES], uint8_t *src[X68K_BI
|
||||||
|
|
||||||
|
|
||||||
void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
||||||
X68kPlanarT *pd;
|
|
||||||
uint8_t colorIndex;
|
|
||||||
uint16_t i;
|
|
||||||
bool setBits;
|
|
||||||
|
|
||||||
if (s->portData == NULL) {
|
if (s->portData == NULL) {
|
||||||
jlpGenericSurfaceClear(s, doubled);
|
jlpGenericSurfaceClear(s, doubled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
colorIndex = (uint8_t)(doubled & 0x0Fu);
|
// A whole surface is the rect fill's whole-row case: each plane is one
|
||||||
pd = (X68kPlanarT *)s->portData;
|
// contiguous run, stored a row per movem.l.
|
||||||
|
surface68kFillRect(((X68kPlanarT *)s->portData)->planes, 0u, 0u, SURFACE_WIDTH, SURFACE_HEIGHT, (uint16_t)(doubled & 0x0Fu));
|
||||||
// Blitter DEST-only block fill of every whole plane to $00 (clear)
|
|
||||||
// or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
|
|
||||||
// BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now
|
|
||||||
// CHIP RAM (see jlpSurfaceAllocPortData) so the blitter can reach
|
|
||||||
// them. Own the blitter once and fill all four planes; the final
|
|
||||||
// CPU fill: no blitter to own or wait on, so the planes are complete
|
|
||||||
// written before any CPU read (a subsequent draw or the present
|
|
||||||
// copy) touches them.
|
|
||||||
|
|
||||||
for (i = 0; i < X68K_BITPLANES; i++) {
|
|
||||||
setBits = ((colorIndex >> i) & 1u) != 0u;
|
|
||||||
// A whole plane is 200 contiguous rows of 20 words: one flat run.
|
|
||||||
x68kFillWordRun(pd->planes[i], (uint16_t)((X68K_BYTES_PER_ROW / 2u) * SURFACE_HEIGHT),
|
|
||||||
setBits ? 0xFFFFFFFFul : 0x00000000ul,
|
|
||||||
setBits ? 0xFFFFu : 0x0000u);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Planar jlFillRect: fills the four off-screen shadow plane buffers via
|
// Planar jlFillRect into the four off-screen shadow plane buffers, invisible
|
||||||
// the blitter. Caller (cross-platform jlFillRect) has already clipped
|
// until jlStagePresent. The caller (cross-platform jlFillRect) has already
|
||||||
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
// clipped the rect to the surface. src/m68k/planarFill.s does all of it: a
|
||||||
// are off-screen, so this is invisible until jlStagePresent.
|
// rect of whole rows as one movem.l run per plane, anything else as masked
|
||||||
//
|
// edge words and long stores between them, plane by plane -- one-pixel-wide
|
||||||
// Layout reminder (see claudeDocs/amiga_planar.md): each plane word covers 16
|
// rects included, which is what draw.c turns a vertical line into.
|
||||||
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
|
|
||||||
// fills whole words and cuts the partial-pixel left/right edges with its
|
|
||||||
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
|
|
||||||
// plane handles any width -- no per-pixel-edge CPU read-modify-write.
|
|
||||||
// For each plane the bit value at every rect pixel is constant:
|
|
||||||
// (colorIndex >> plane) & 1; set bits OR the mask into the plane, clear
|
|
||||||
// bits AND its complement (see the two masked-fill minterms).
|
|
||||||
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
||||||
X68kPlanarT *pd;
|
X68kPlanarT *pd;
|
||||||
|
|
||||||
|
|
@ -170,30 +105,6 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
|
||||||
jlpGenericFillRect(s, x, y, w, h, colorIndex);
|
jlpGenericFillRect(s, x, y, w, h, colorIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-width rows are contiguous within a plane, so a full-width rect is
|
|
||||||
// ONE flat run of words per plane instead of h separately masked ones --
|
|
||||||
// about three cycles a byte against the general path's six. Every
|
|
||||||
// full-screen wipe is this shape, so it keeps its own case.
|
|
||||||
if (x == 0 && w == (int16_t)SURFACE_WIDTH) {
|
|
||||||
uint16_t plane;
|
|
||||||
|
|
||||||
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
|
||||||
uint8_t bitVal = (uint8_t)((colorIndex >> plane) & 1u);
|
|
||||||
uint8_t *rowBase = pd->planes[plane] + (uint16_t)y * X68K_BYTES_PER_ROW;
|
|
||||||
|
|
||||||
x68kFillWordRun(rowBase, (uint16_t)((X68K_BYTES_PER_ROW / 2u) * (uint16_t)h),
|
|
||||||
bitVal ? 0xFFFFFFFFul : 0x00000000ul,
|
|
||||||
bitVal ? 0xFFFFu : 0x0000u);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything else goes to planarFill.s, which shares its span body with
|
|
||||||
// the filled circle: masked end bytes, long stores between them, and the
|
|
||||||
// set-or-clear decision made once per colour instead of per plane per row.
|
|
||||||
// That includes one-pixel-wide rects, which is what draw.c turns a
|
|
||||||
// vertical line into -- the asm's single-byte case covers them.
|
|
||||||
surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex);
|
surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,154 @@
|
||||||
| It does NOT raise a CPU interrupt of its own: it drives the MFP's GPIP6 line,
|
| It does NOT raise a CPU interrupt of its own: it drives the MFP's GPIP6 line,
|
||||||
| which is MFP channel 14, vector $40|14 = $4E, exception address $138. That is
|
| which is MFP channel 14, vector $40|14 = $4E, exception address $138. That is
|
||||||
| exactly the vector _CRTCRAS installs into, and IOCS does NOT wrap the
|
| exactly the vector _CRTCRAS installs into, and IOCS does NOT wrap the
|
||||||
| handler -- the pointer goes straight into the vector. So this must be a RAW
|
| handler -- the pointer goes straight into the vector. So this is a RAW
|
||||||
| interrupt routine. A C function (RTS, and free to clobber d0/d1/a0/a1) can
|
| interrupt routine: it saves what it uses and returns with RTE.
|
||||||
| never be valid there; the port already proved that by hanging the machine
|
|
||||||
| with one on $134 (see the _VDISPST note in hal.c).
|
|
||||||
|
|
|
|
||||||
| This shim saves EVERY register, calls the C service, restores, drops the
|
| TIMING. A band's sixteen colours are stored with one movem.l -- eight long
|
||||||
| MFP's in-service bit for channel 14, and returns with RTE. Saving everything
|
| writes, 72 cycles -- in the horizontal blanking between the last stage pixel
|
||||||
| is exactly what lets the service side be an ordinary C function.
|
| of the line above the band and the first stage pixel of the band. Nothing is
|
||||||
|
| counted in cycles; every step waits on a signal edge, so the landing point
|
||||||
|
| does not depend on how late the interrupt was taken or on the CPU's clock:
|
||||||
|
|
|
||||||
|
| 1. The compare is armed two rasters ahead: for a band starting on display
|
||||||
|
| row D it fires as the raster of row D-2 begins.
|
||||||
|
| 2. The routine at once re-points the compare at the raster of row D-1
|
||||||
|
| (X68kBandT.raster) and waits for GPIP6 to go low, which it does as that
|
||||||
|
| raster begins -- a fixed point however late the interrupt was taken.
|
||||||
|
| 3. It waits for H-SYNC (GPIP7) to go high, then low: the blanking ahead of
|
||||||
|
| row D-1 is over and the row is being drawn. The colours are loaded into
|
||||||
|
| d0-d7 while it is.
|
||||||
|
| 4. It waits for H-SYNC to go high again -- row D-1's display is over -- and
|
||||||
|
| stores.
|
||||||
|
| Step 3 waits for high then low because the two references disagree on what
|
||||||
|
| GPIP7 is: MAME holds it high for the whole horizontal blanking, XEiJ
|
||||||
|
| documents it as the sync pulse alone (raster numbers stepping at the front
|
||||||
|
| porch, before the pulse). The sequence finds the same edge under either.
|
||||||
|
|
|
||||||
|
| A band that starts one or two lines after the previous one
|
||||||
|
| (X68kBandT.stay) cannot be reached by re-arming -- its compare would already
|
||||||
|
| be past by the time the stores finish -- so the routine stays and waits out
|
||||||
|
| the lines in between. Otherwise it drops the request its own step-2 edge left
|
||||||
|
| pending and arms the compare for the next band. After the last band it
|
||||||
|
| rewinds to band 0 for the next frame, taking up the table hal.c has queued
|
||||||
|
| in gX68kRasterPending if there is one: a new screen's bands always start on
|
||||||
|
| a frame boundary.
|
||||||
|
|
|
||||||
|
| LATENESS. The routine must be running before step 2's raster begins, about
|
||||||
|
| one raster after the compare fires. Human68k's Timer C tick used to hold IPL
|
||||||
|
| 6 for up to ~2000 cycles and made it miss (x68kTickIsr.s now lets this
|
||||||
|
| interrupt through). If it is ever that late anyway, step 2's raster has
|
||||||
|
| already begun and GPIP6 would not fall again until the next frame: rather
|
||||||
|
| than spin a whole frame at IPL 6, the wait gives up after REF_WAIT_LINES
|
||||||
|
| H-SYNC periods and the routine rewinds to band 0 for the next frame. The
|
||||||
|
| rest of that one frame keeps the palette it has.
|
||||||
|
|
|
|
||||||
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
||||||
|
|
||||||
|
.equ MFP_GPIP, 0xE88001 | bit 7 = H-SYNC, bit 6 = raster match (low)
|
||||||
|
.equ GPIP_RINT_BIT, 6
|
||||||
|
.equ MFP_IPRA, 0xE8800B | MFP reg 5, interrupt pending A
|
||||||
.equ MFP_ISRA, 0xE8800F | MFP reg 7, in-service A
|
.equ MFP_ISRA, 0xE8800F | MFP reg 7, in-service A
|
||||||
.equ ISRA_NOT_G6, 0xBF | clear bit 6 (channel 14), keep the rest
|
.equ CH14_CLEAR, 0xBF | clear bit 6 (channel 14), keep the rest
|
||||||
|
.equ CRTC_R09, 0xE80012 | raster compare
|
||||||
|
.equ TEXT_PALETTE, 0xE82200 | the text plane's 16 colours
|
||||||
|
.equ ARM_LEAD, 1 | arm one raster before X68kBandT.raster
|
||||||
|
.equ REF_WAIT_LINES, 3 | H-SYNC periods to wait for step 2's edge
|
||||||
|
|
||||||
|
| X68kBandT and X68kBandTableT (src/x68000/hal.c, which holds
|
||||||
|
| these offsets with compile-time size checks).
|
||||||
|
.equ BAND_PALETTE, 0
|
||||||
|
.equ BAND_RASTER, 4
|
||||||
|
.equ BAND_STAY, 6
|
||||||
|
.equ BAND_SIZE, 8
|
||||||
|
.equ TABLE_END, 0
|
||||||
|
.equ TABLE_BANDS, 4
|
||||||
|
|
||||||
.text
|
.text
|
||||||
.even
|
.even
|
||||||
.globl x68kRasterIsr
|
.globl x68kRasterIsr
|
||||||
|
|
||||||
x68kRasterIsr:
|
x68kRasterIsr:
|
||||||
movem.l %d0-%d7/%a0-%a6,%sp@-
|
| Step 2's compare write comes first and only what the edge
|
||||||
jsr x68kRasterService
|
| waits use is saved before them, so the interrupt can be taken
|
||||||
movem.l %sp@+,%d0-%d7/%a0-%a6
|
| late and still make it; the rest is saved while the line
|
||||||
| Ack LAST: the in-service bit holds off another channel-14
|
| above the band is drawn.
|
||||||
| interrupt while the service reprograms the compare register.
|
move.l %a2,%sp@-
|
||||||
move.b #ISRA_NOT_G6,MFP_ISRA
|
movea.l gX68kRasterCursor,%a2
|
||||||
|
move.w %a2@(BAND_RASTER),CRTC_R09
|
||||||
|
movem.l %d1/%a1,%sp@-
|
||||||
|
lea MFP_GPIP,%a1
|
||||||
|
moveq #REF_WAIT_LINES,%d1
|
||||||
|
rasterAbove: | the raster of the line above begins...
|
||||||
|
btst #GPIP_RINT_BIT,%a1@
|
||||||
|
beq.s rasterPulse
|
||||||
|
tst.b %a1@
|
||||||
|
bpl.s rasterAbove
|
||||||
|
rasterAboveSync: | ...or an H-SYNC period goes by
|
||||||
|
btst #GPIP_RINT_BIT,%a1@
|
||||||
|
beq.s rasterPulse
|
||||||
|
tst.b %a1@
|
||||||
|
bmi.s rasterAboveSync
|
||||||
|
subq.w #1,%d1
|
||||||
|
bne.s rasterAbove
|
||||||
|
movem.l %d0/%d2-%d7/%a0/%a3,%sp@-
|
||||||
|
movea.l gX68kRasterTable,%a0 | too late for this frame: resync at the next
|
||||||
|
movea.l %a0@(TABLE_END),%a2
|
||||||
|
bra.s rasterNext
|
||||||
|
rasterPulse: | its blanking (MAME) or sync pulse
|
||||||
|
tst.b %a1@
|
||||||
|
bpl.s rasterPulse
|
||||||
|
rasterDisplay: | the line above the band is being drawn
|
||||||
|
tst.b %a1@
|
||||||
|
bmi.s rasterDisplay
|
||||||
|
movem.l %d0/%d2-%d7/%a0/%a3,%sp@-
|
||||||
|
lea TEXT_PALETTE,%a3
|
||||||
|
rasterLoad:
|
||||||
|
movea.l %a2@(BAND_PALETTE),%a0
|
||||||
|
movem.l %a0@,%d0-%d7 | the colours, ready...
|
||||||
|
rasterBlank:
|
||||||
|
tst.b %a1@
|
||||||
|
bpl.s rasterBlank
|
||||||
|
movem.l %d0-%d7,%a3@ | ...stored in the line's blanking
|
||||||
|
move.w %a2@(BAND_STAY),%d1
|
||||||
|
lea %a2@(BAND_SIZE),%a2
|
||||||
|
subq.w #1,%d1
|
||||||
|
bmi.s rasterNext | 0: far enough away to re-arm for
|
||||||
|
bra.s rasterSkipCount
|
||||||
|
rasterSkipLine: | a line between this band and the next
|
||||||
|
tst.b %a1@
|
||||||
|
bmi.s rasterSkipLine
|
||||||
|
rasterSkipBlank:
|
||||||
|
tst.b %a1@
|
||||||
|
bpl.s rasterSkipBlank
|
||||||
|
rasterSkipCount:
|
||||||
|
dbra %d1,rasterSkipLine
|
||||||
|
rasterStay: | the line above the next band
|
||||||
|
tst.b %a1@
|
||||||
|
bmi.s rasterStay
|
||||||
|
bra.s rasterLoad
|
||||||
|
rasterNext:
|
||||||
|
movea.l gX68kRasterTable,%a0
|
||||||
|
cmpa.l %a0@(TABLE_END),%a2
|
||||||
|
bne.s rasterArm
|
||||||
|
move.l gX68kRasterPending,%d0 | frame done: a newer table waiting?
|
||||||
|
beq.s rasterRewind
|
||||||
|
movea.l %d0,%a0
|
||||||
|
move.l %a0,gX68kRasterTable
|
||||||
|
clr.l gX68kRasterPending
|
||||||
|
rasterRewind:
|
||||||
|
lea %a0@(TABLE_BANDS),%a2
|
||||||
|
rasterArm:
|
||||||
|
move.b #CH14_CLEAR,MFP_IPRA | step 2's edge, not a new request
|
||||||
|
move.w %a2@(BAND_RASTER),%d0
|
||||||
|
subq.w #ARM_LEAD,%d0
|
||||||
|
move.w %d0,CRTC_R09
|
||||||
|
move.l %a2,gX68kRasterCursor
|
||||||
|
movem.l %sp@+,%d0/%d2-%d7/%a0/%a3
|
||||||
|
movem.l %sp@+,%d1/%a1
|
||||||
|
movea.l %sp@+,%a2
|
||||||
|
| Human68k runs the MFP in automatic end-of-interrupt mode (VR
|
||||||
|
| = $40), where this does nothing; in software end-of-interrupt
|
||||||
|
| mode it is what releases channel 14.
|
||||||
|
move.b #CH14_CLEAR,MFP_ISRA
|
||||||
rte
|
rte
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,62 @@
|
||||||
| Sharp X68000 millisecond tick.
|
| Sharp X68000 millisecond tick.
|
||||||
|
|
|
|
||||||
| Human68k already runs MFP Timer C as its ~100 Hz system tick, so rather than
|
| Human68k already runs MFP Timer C as its system tick, so rather than program
|
||||||
| program a timer of our own we CHAIN its interrupt: bump a counter, then jump
|
| a timer of our own we CHAIN its interrupt: bump a counter, then run whatever
|
||||||
| to whatever handler was there before. That keeps the OS clock working and
|
| handler was there before. That keeps the OS clock working and costs one add
|
||||||
| costs one add per tick.
|
| per tick.
|
||||||
|
|
|
|
||||||
| Entered DIRECTLY from 68000 exception vector $45 (MFP channel 5 = Timer C,
|
| Entered DIRECTLY from 68000 exception vector $45 (MFP channel 5 = Timer C,
|
||||||
| vector base $40 | 5), so this is a raw interrupt handler -- it must not
|
| vector base $40 | 5), so this is a raw interrupt handler -- it must not
|
||||||
| disturb any register and must not return with RTS. It does neither: addq.l on
|
| disturb any register and must not return with RTS.
|
||||||
| an absolute address touches no register, and control leaves through a JMP to
|
|
||||||
| the previous handler, which owns the RTE.
|
|
||||||
|
|
|
|
||||||
| The JMP's operand is patched at install time by installTickIsr() in hal.c via
|
| The previous handler is IOCS's software-timer dispatcher ($FF1344 in IPL
|
||||||
| the x68kTickChainAddr symbol below. Patching the operand rather than doing an
|
| 1.0), which runs whichever of its four timer callbacks are due and holds IPL
|
||||||
| indirect jump through a pointer is what keeps this register-free -- a 68000
|
| 6 for all of it: 700-2000 cycles, 100 times a second, measured under MAME.
|
||||||
| has no jmp ([abs]) addressing mode.
|
| The SCB raster interrupt (x68kRasterIsr.s) is also an MFP interrupt at IPL 6
|
||||||
|
| and has about one raster (~310 cycles) to be taken, so a raster match that
|
||||||
|
| fell inside that window was taken late and the band's palette reload missed
|
||||||
|
| its line (scripts/verify-x68000-scb.sh). So the dispatcher is CALLED rather
|
||||||
|
| than jumped to: through an exception frame whose RTE comes back here, at IPL
|
||||||
|
| 5, with every MFP channel but the raster compare's masked in IMR. Nothing
|
||||||
|
| can interrupt it that could not before except the raster interrupt, which
|
||||||
|
| touches no OS state -- only its own tables, the text palette, CRTC R09 and
|
||||||
|
| its own MFP pending bit. The MFP runs in automatic end-of-interrupt mode
|
||||||
|
| (VR = $40), so IMR is the only gate: a request masked here stays pending in
|
||||||
|
| IPR and is taken when the masks come back.
|
||||||
|
|
|
||||||
|
| The call's target is patched at install time by installTickIsr() in hal.c
|
||||||
|
| via the x68kTickChainAddr symbol below. Patching the operand rather than
|
||||||
|
| going through a pointer is what keeps this register-free -- a 68000 has no
|
||||||
|
| jmp ([abs]) addressing mode.
|
||||||
|
|
|
|
||||||
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
||||||
|
|
||||||
|
.equ MFP_IMRA, 0xE88013 | MFP reg 9, interrupt mask A
|
||||||
|
.equ MFP_IMRB, 0xE88015 | MFP reg 10, interrupt mask B
|
||||||
|
.equ IMRA_RASTER, 0x40 | channel 14: the CRTC raster compare
|
||||||
|
.equ SR_CALL, 0x2500 | supervisor, IPL 5: level 6 (the MFP) gets in
|
||||||
|
.equ SR_RETURN, 0x2700 | supervisor, IPL 7: the masks go back undisturbed
|
||||||
|
|
||||||
.text
|
.text
|
||||||
.even
|
.even
|
||||||
.globl x68kTickIsr
|
.globl x68kTickIsr
|
||||||
.globl x68kTickChainAddr
|
.globl x68kTickChainAddr
|
||||||
|
|
||||||
x68kTickIsr:
|
x68kTickIsr:
|
||||||
|
| The masks first and the IPL down at once: every cycle spent
|
||||||
|
| here at IPL 6 is a cycle a raster match waits.
|
||||||
|
move.b MFP_IMRA,%sp@- | a byte push keeps the stack even
|
||||||
|
move.b MFP_IMRB,%sp@-
|
||||||
|
andi.b #IMRA_RASTER,MFP_IMRA
|
||||||
|
clr.b MFP_IMRB
|
||||||
|
move.w #SR_CALL,%sr | the rest, and the dispatcher, at IPL 5
|
||||||
addq.l #1,gX68kTicks
|
addq.l #1,gX68kTicks
|
||||||
|
pea x68kTickReturn | the frame the dispatcher's RTE uses
|
||||||
|
move.w #SR_RETURN,%sp@-
|
||||||
.word 0x4EF9 | jmp <abs.l>
|
.word 0x4EF9 | jmp <abs.l>
|
||||||
x68kTickChainAddr:
|
x68kTickChainAddr:
|
||||||
.long 0 | patched with the previous handler
|
.long 0 | patched with the previous handler
|
||||||
|
x68kTickReturn:
|
||||||
|
move.b %sp@+,MFP_IMRB
|
||||||
|
move.b %sp@+,MFP_IMRA
|
||||||
|
rte
|
||||||
|
|
|
||||||
23
src/x68000/x68kVdispIsr.s
Normal file
23
src/x68000/x68kVdispIsr.s
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
| Sharp X68000 frame counter.
|
||||||
|
|
|
||||||
|
| Entered DIRECTLY from 68000 exception vector $134 (MFP channel 13, Timer A),
|
||||||
|
| where IOCS _VDISPST writes it with no wrapper (installVdispIsr in hal.c).
|
||||||
|
| Timer A counts V-DISP edges -- the IPL ROM's MFP table leaves it in event
|
||||||
|
| count mode -- and _VDISPST loads it with 1 and picks the rising edge, so this
|
||||||
|
| runs once a frame, as the display period starts. A raw interrupt handler: it
|
||||||
|
| changes no register and returns with RTE. The MFP runs in automatic
|
||||||
|
| end-of-interrupt mode (VR = $40), so there is nothing to acknowledge.
|
||||||
|
|
|
||||||
|
| MFP channel 13 is below the SCB raster interrupt (channel 14) and above
|
||||||
|
| Timer C (channel 5); x68kTickIsr.s masks it while the IOCS dispatcher runs,
|
||||||
|
| and an edge in that window is counted as the mask lifts.
|
||||||
|
|
|
||||||
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
||||||
|
|
||||||
|
.text
|
||||||
|
.even
|
||||||
|
.globl x68kVdispIsr
|
||||||
|
|
||||||
|
x68kVdispIsr:
|
||||||
|
addq.w #1,gX68kFrames
|
||||||
|
rte
|
||||||
Loading…
Add table
Reference in a new issue