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`):
|
||||
|
||||
```
|
||||
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-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.
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
the DOS port does: it renders through the 256-colour graphics plane,
|
||||
so all 16 palettes are resident at once and each pixel byte is
|
||||
`(scb[y] << 4) | nibble` -- the pixel value selects the band, with no
|
||||
raster interrupt involved.
|
||||
via the SCB. Every port honours it. The X68000 presents to its text
|
||||
plane, which is planar like the stage (so a present is a copy) but has
|
||||
one 16-colour palette: a raster interrupt reloads that palette wherever
|
||||
the SCB changes, which for a single-palette screen is never. A build
|
||||
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
|
||||
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||
|
|
|
|||
|
|
@ -192,11 +192,13 @@ uint16_t jlFrameHz(void);
|
|||
|
||||
*Parameters:* none.
|
||||
|
||||
*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
|
||||
70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the same
|
||||
frame count). The real cadence may drift slightly; this is the figure to
|
||||
divide by when converting frames to seconds.
|
||||
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs
|
||||
and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has
|
||||
a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the
|
||||
same frame count). The X68000 times its display at `jlInit` and rounds to the
|
||||
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
|
||||
|
||||
|
|
@ -205,9 +207,9 @@ divide by when converting frames to seconds.
|
|||
uint32_t jlMillisElapsed(void);
|
||||
----
|
||||
|
||||
Monotonic millisecond counter since `jlInit`. DOS, ST and DVX read a real
|
||||
hardware timer; IIgs and Amiga derive it from the frame counter, so the value steps at
|
||||
frame granularity there. Two calls separated by a long render report the true
|
||||
Monotonic millisecond counter since `jlInit`. DOS, ST, X68000 and DVX read a
|
||||
real hardware timer; IIgs and Amiga derive it from the frame counter, so the
|
||||
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.
|
||||
|
||||
*Parameters:* none.
|
||||
|
|
|
|||
|
|
@ -186,7 +186,9 @@ bool jlJoystickConnected(jlJoystickE js);
|
|||
|`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
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|50 Hz (PAL)
|
||||
|60 Hz (NTSC)
|
||||
|70 Hz
|
||||
|60 Hz
|
||||
|55 Hz (56 under MAME)
|
||||
|70 Hz (virtual; the real retrace on the whole screen)
|
||||
|
||||
|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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs,
|
||||
ST NTSC, X68000) and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop
|
||||
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz
|
||||
(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
|
||||
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_INPUT_INIT
|
||||
#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_WAIT_VBL // _iocs_vsync
|
||||
#define JL_HAS_FRAME_COUNT
|
||||
#define JL_HAS_FRAME_HZ // ~55 Hz in the 31 kHz modes
|
||||
// Chains Human68k's MFP Timer C (~100 Hz). Without this the port fell back to
|
||||
// the generic clock, which derives ms from the POLLED frame counter and
|
||||
// undercounts exactly in proportion to how slow the op is.
|
||||
#define JL_HAS_WAIT_VBL // the V-DISP frame count moving
|
||||
#define JL_HAS_FRAME_COUNT // V-DISP edges, counted on MFP Timer A via _VDISPST
|
||||
#define JL_HAS_FRAME_HZ // measured at init: 55 on hardware, 56 under MAME
|
||||
// Human68k's MFP Timer C: 100 Hz off the crystal, read to the millisecond
|
||||
// through its live count (src/x68000/hal.c, "Clocks").
|
||||
#define JL_HAS_MILLIS_ELAPSED
|
||||
// Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL.
|
||||
#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.
|
||||
# TILEBANK the tile-bank gate's program: this port's .tbk bake loaded both
|
||||
# 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
|
||||
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
||||
DRAW_SRC := $(EXAMPLES)/draw/draw.c
|
||||
|
|
@ -106,6 +109,7 @@ SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
|
|||
SERMOUSE_SRC := $(EXAMPLES)/sermouse/sermouse.c
|
||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||
TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c
|
||||
SCB_SRC := $(EXAMPLES)/scb/scb.c
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
|
|
@ -113,7 +117,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c
|
|||
ADV_SRC := $(EXAMPLES)/adventure/adventure.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
|
||||
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)
|
||||
|
||||
# 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_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
|
||||
|
||||
|
|
@ -290,13 +294,24 @@ x68000-verify-mouse: $(BINDIR)/KEYS.X
|
|||
x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X
|
||||
$(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
|
||||
# the host gate's, and every other target's bake must be refused (~1 min).
|
||||
x68000-verify-tilebank: $(BINDIR)/TILEBANK.X $(TILEBANK_DATA)
|
||||
$(REPO_DIR)/scripts/verify-x68000-tilebank.sh
|
||||
|
||||
# Golden-hash gate against the Apple IIgs reference (~70 min: UBER on the
|
||||
# generic renderer is slow and must reach jlLogFlush before hashes exist).
|
||||
# SCB gate: every text-palette reload of a 147-change screen, timed on the
|
||||
# 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)
|
||||
$(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
|
||||
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||
-- 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.
|
||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||
local presentPlane = "$PRESENT_PLANE"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@
|
|||
# dvx: full screen -- UBER's hashes through a suspend and a
|
||||
# resume, and KEYS's row read off the 320x200 VGA screen, then
|
||||
# in its window, then on the whole screen again
|
||||
# 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
|
||||
# of the host fixture both ways, pixel-exact and with the host
|
||||
# gate's surface hash, and refuses every other target's bake (the
|
||||
# host half is check-tilebank.sh under `host`)
|
||||
# input each port: a typed string reaches jlInputGetChar byte-exact
|
||||
# (dvx: and the present shows it, decoded from the desktop)
|
||||
# x68000: also both joysticks -- every direction, both triggers
|
||||
# and every release, read back from examples/joy's display
|
||||
# audio six gates, two kinds:
|
||||
# iigs DOC wavetable + sound IRQ vector (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
|
||||
visual iigs examples scripts/verify-iigs-all.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 x68000 tilebank scripts/verify-x68000-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
|
||||
input iigs input scripts/verify-iigs-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 amiga input scripts/verify-amiga-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
|
||||
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||
-- 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.
|
||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||
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
|
||||
# one clamped 127-unit packet).
|
||||
#
|
||||
# GVRAM geometry (crtmod 13, 256-colour graphics plane): 16-bit word
|
||||
# per pixel at $C00000, 512 words per row; the 320x200 stage is centred
|
||||
# at origin (96, 156). Mouse verification row = stage y 199 -> GVRAM
|
||||
# y 355.
|
||||
# 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). Mouse verification row = stage y 199 -> screen y 355.
|
||||
#
|
||||
# bash scripts/verify-x68000-mouse.sh
|
||||
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:
|
||||
|
|
||||
| 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
|
||||
| right byte, in each of four planes. Three things make this much cheaper
|
||||
| than the general case, and the PLANEFILL macro below is built around them:
|
||||
| The circle's span is a partial left byte, a run of whole middle bytes, and a
|
||||
| partial right byte, in each of four planes. (The rect works in words and
|
||||
| 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
|
||||
| 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
|
||||
| computed once at entry and stay in d2/d3/d4 for the whole shape; the row
|
||||
| loop only walks the offset. d7 counts up by a row's worth of bytes until it
|
||||
| meets a6, which is one row past the last.
|
||||
.macro RECTBODY colour
|
||||
.Lrect\colour:
|
||||
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 ----
|
||||
| A rect is the same span on every row, so everything about it is decided once
|
||||
| at entry, and the rows then run as tight as the span allows. It works in
|
||||
| 16-pixel WORDS rather than bytes: a plane row is 20 words, the 68000 needs
|
||||
| only even addresses for word and long stores, so a span is one masked edge
|
||||
| word, a run of whole words, and a second masked edge word -- never a lone
|
||||
| middle byte, and never a per-row alignment test.
|
||||
|
|
||||
| Worth its own body rather than folding into SPAN. A vertical line is this
|
||||
| shape (draw.c turns one into a 1px-wide rect), and it has no middle run and
|
||||
| only one masked byte per row, so the generic span pays for a middle-run test
|
||||
| and a second edge it does not need. Here the plane loop is outside the row
|
||||
| loop, which leaves a dbra over a single read-modify-write: d2 holds the one
|
||||
| combined mask and d4 the row count less one.
|
||||
.macro THINPLANE areg, op
|
||||
move.w %d4,%d5
|
||||
lea (\areg,%d7.w),%a0
|
||||
1: \op\().b %d2,(%a0)
|
||||
lea ROW_BYTES(%a0),%a0
|
||||
dbra %d5,1b
|
||||
.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
|
||||
|
||||
| The planes run one after another, each through a body that loops over the
|
||||
| rows. A plane's fill value is all ones or all zeros, so a set plane ORs its
|
||||
| edge masks in and a clear plane ANDs their complements; the whole words go
|
||||
| out as `move.l %d6,(%a0)+` stores, entered part-way down an unrolled run by a
|
||||
| jump computed once for the rect. An odd count of whole words is folded into
|
||||
| the left edge, which then stores a long -- the edge mask in its high word,
|
||||
| all ones in its low -- for the same cost as the word it absorbs, so the run
|
||||
| is always whole longs. That leaves four wide bodies (OR or AND, word or long
|
||||
| left edge) and two thin ones for a rect inside a single word.
|
||||
|
|
||||
| A rect of whole rows (w = 320) is one contiguous run per plane and takes the
|
||||
| movem.l path at the end instead, a row per instruction.
|
||||
|
||||
| 44 saved registers + 4 return address
|
||||
.equ FR_ARG, 48
|
||||
|
|
@ -368,130 +325,202 @@ fcDone:
|
|||
.equ FR_W, FR_ARG + 12 + 2
|
||||
.equ FR_H, FR_ARG + 16 + 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,
|
||||
| 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
|
||||
| rect never arrives. A rect covering whole rows is better served by one flat
|
||||
| run of words per plane and halPlanar.c keeps that case to itself; everything
|
||||
| else, partial pixel edges included, lands here.
|
||||
| w and h must both be at least 1 and the rect on-surface -- draw.c clips
|
||||
| before it calls, so an empty or overhanging rect never arrives.
|
||||
surface68kFillRect:
|
||||
_surface68kFillRect:
|
||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||
move.l FR_PLANES(%sp),%a0
|
||||
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.l FR_PLANES(%sp),%a6
|
||||
move.w FR_X(%sp),%d0 | left
|
||||
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
|
||||
subq.w #1,%d1 | right
|
||||
move.w %d0,%d5
|
||||
lsr.w #3,%d5 | first byte
|
||||
and.w #7,%d0
|
||||
move.b (%a1,%d0.w),%d2 | left mask
|
||||
move.w %d1,%d0
|
||||
lsr.w #3,%d0 | last byte
|
||||
and.w #7,%d1
|
||||
move.b 8(%a1,%d1.w),%d3 | right mask
|
||||
sub.w %d5,%d0
|
||||
bne.s 1f
|
||||
| One byte wide: one combined mask, and d4 becomes the dbra
|
||||
| row count for THINBODY instead of a middle-run length.
|
||||
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
|
||||
lea gFillWordLut,%a0
|
||||
move.w %d0,%d2
|
||||
and.w #PIXEL_IN_WORD,%d2
|
||||
add.w %d2,%d2
|
||||
move.w (%a0,%d2.w),%d2 | left mask
|
||||
move.w %d1,%d3
|
||||
and.w #PIXEL_IN_WORD,%d3
|
||||
add.w %d3,%d3
|
||||
move.w WORD_LUT_RIGHT(%a0,%d3.w),%d3 | right mask
|
||||
lsr.w #WORD_SHIFT,%d0 | first word
|
||||
lsr.w #WORD_SHIFT,%d1 | last word
|
||||
sub.w %d0,%d1 | d1 = last - first
|
||||
add.w %d0,%d0
|
||||
| The jump table goes through a1: a PC-relative index has only
|
||||
| an 8-bit displacement, and the thin table is 4 KB out.
|
||||
tst.w %d6
|
||||
bne.s .Lthin
|
||||
lea frJump(%pc),%a1
|
||||
bra.s .Ldispatch
|
||||
.Lthin:
|
||||
lea ftJump(%pc),%a1
|
||||
.Ldispatch:
|
||||
move.w (%a1,%d0.w),%d1
|
||||
jmp (%a1,%d1.w)
|
||||
|
||||
frJump:
|
||||
.word frC0-frJump, frC1-frJump, frC2-frJump, frC3-frJump
|
||||
.word frC4-frJump, frC5-frJump, frC6-frJump, frC7-frJump
|
||||
.word frC8-frJump, frC9-frJump, frC10-frJump, frC11-frJump
|
||||
.word frC12-frJump, frC13-frJump, frC14-frJump, frC15-frJump
|
||||
|
||||
frC0: RECTBODY 0
|
||||
frC1: RECTBODY 1
|
||||
frC2: RECTBODY 2
|
||||
frC3: RECTBODY 3
|
||||
frC4: RECTBODY 4
|
||||
frC5: RECTBODY 5
|
||||
frC6: RECTBODY 6
|
||||
frC7: RECTBODY 7
|
||||
frC8: RECTBODY 8
|
||||
frC9: RECTBODY 9
|
||||
frC10: RECTBODY 10
|
||||
frC11: RECTBODY 11
|
||||
frC12: RECTBODY 12
|
||||
frC13: RECTBODY 13
|
||||
frC14: RECTBODY 14
|
||||
frC15: RECTBODY 15
|
||||
|
||||
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:
|
||||
move.w FR_Y(%sp),%d4
|
||||
mulu.w #ROW_BYTES,%d4
|
||||
add.w %d4,%d0 | d0 = the first row's left edge offset
|
||||
move.w FR_H(%sp),%d7
|
||||
subq.w #1,%d7 | rows - 1
|
||||
tst.w %d1
|
||||
bne.s .Lwide
|
||||
and.w %d3,%d2 | both edges in one word
|
||||
lea frOrThin(%pc),%a3
|
||||
lea frAndThin(%pc),%a4
|
||||
bra.s .Lplanes
|
||||
.Lwide:
|
||||
| After the right edge a0 sits on the last word; the next row's
|
||||
| first word is a row on from the first, 2 * (last - first) back.
|
||||
moveq #ROW_BYTES,%d4
|
||||
sub.w %d1,%d4
|
||||
sub.w %d1,%d4
|
||||
subq.w #1,%d1 | d1 = whole middle words
|
||||
lea frOrWord(%pc),%a3
|
||||
lea frAndWord(%pc),%a4
|
||||
btst #0,%d1
|
||||
beq.s 1f
|
||||
| Odd: the left edge takes the first of them, as a long.
|
||||
swap %d2
|
||||
move.w #0xFFFF,%d2
|
||||
subq.w #1,%d1
|
||||
lea frOrLong(%pc),%a3
|
||||
lea frAndLong(%pc),%a4
|
||||
1: move.w %d1,%a2 | words of middle = bytes of move.l to run
|
||||
.Lplanes:
|
||||
move.w FR_COLOR(%sp),%d1
|
||||
RECTPLANE 0
|
||||
RECTPLANE 1
|
||||
RECTPLANE 2
|
||||
RECTPLANE 3
|
||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||
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
|
||||
.even
|
||||
|
|
@ -501,3 +530,13 @@ frDone:
|
|||
gFillMaskLut:
|
||||
.byte 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01
|
||||
.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
|
||||
|
|
|
|||
1440
src/x68000/hal.c
1440
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
|
||||
// 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.
|
||||
static void x68kCpuCopyPlanes(uint8_t *dst[X68K_BITPLANES], uint8_t *src[X68K_BITPLANES],
|
||||
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) {
|
||||
X68kPlanarT *pd;
|
||||
uint8_t colorIndex;
|
||||
uint16_t i;
|
||||
bool setBits;
|
||||
|
||||
if (s->portData == NULL) {
|
||||
jlpGenericSurfaceClear(s, doubled);
|
||||
return;
|
||||
}
|
||||
colorIndex = (uint8_t)(doubled & 0x0Fu);
|
||||
pd = (X68kPlanarT *)s->portData;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// A whole surface is the rect fill's whole-row case: each plane is one
|
||||
// contiguous run, stored a row per movem.l.
|
||||
surface68kFillRect(((X68kPlanarT *)s->portData)->planes, 0u, 0u, SURFACE_WIDTH, SURFACE_HEIGHT, (uint16_t)(doubled & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
// Planar jlFillRect: fills the four off-screen shadow plane buffers via
|
||||
// the blitter. Caller (cross-platform jlFillRect) has already clipped
|
||||
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
||||
// are off-screen, so this is invisible until jlStagePresent.
|
||||
//
|
||||
// Layout reminder (see claudeDocs/amiga_planar.md): each plane word covers 16
|
||||
// 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).
|
||||
// Planar jlFillRect into the four off-screen shadow plane buffers, invisible
|
||||
// until jlStagePresent. The caller (cross-platform jlFillRect) has already
|
||||
// clipped the rect to the surface. src/m68k/planarFill.s does all of it: a
|
||||
// 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
|
||||
// rects included, which is what draw.c turns a vertical line into.
|
||||
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,29 +8,154 @@
|
|||
| 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
|
||||
| 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
|
||||
| interrupt routine. A C function (RTS, and free to clobber d0/d1/a0/a1) can
|
||||
| never be valid there; the port already proved that by hanging the machine
|
||||
| with one on $134 (see the _VDISPST note in hal.c).
|
||||
| handler -- the pointer goes straight into the vector. So this is a RAW
|
||||
| interrupt routine: it saves what it uses and returns with RTE.
|
||||
|
|
||||
| This shim saves EVERY register, calls the C service, restores, drops the
|
||||
| MFP's in-service bit for channel 14, and returns with RTE. Saving everything
|
||||
| is exactly what lets the service side be an ordinary C function.
|
||||
| TIMING. A band's sixteen colours are stored with one movem.l -- eight long
|
||||
| writes, 72 cycles -- in the horizontal blanking between the last stage pixel
|
||||
| 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.
|
||||
|
||||
.equ MFP_ISRA, 0xE8800F | MFP reg 7, in-service A
|
||||
.equ ISRA_NOT_G6, 0xBF | clear bit 6 (channel 14), keep the rest
|
||||
.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 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
|
||||
.even
|
||||
.globl x68kRasterIsr
|
||||
|
||||
x68kRasterIsr:
|
||||
movem.l %d0-%d7/%a0-%a6,%sp@-
|
||||
jsr x68kRasterService
|
||||
movem.l %sp@+,%d0-%d7/%a0-%a6
|
||||
| Ack LAST: the in-service bit holds off another channel-14
|
||||
| interrupt while the service reprograms the compare register.
|
||||
move.b #ISRA_NOT_G6,MFP_ISRA
|
||||
| Step 2's compare write comes first and only what the edge
|
||||
| waits use is saved before them, so the interrupt can be taken
|
||||
| late and still make it; the rest is saved while the line
|
||||
| above the band is drawn.
|
||||
move.l %a2,%sp@-
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,30 +1,62 @@
|
|||
| Sharp X68000 millisecond tick.
|
||||
|
|
||||
| Human68k already runs MFP Timer C as its ~100 Hz system tick, so rather than
|
||||
| program a timer of our own we CHAIN its interrupt: bump a counter, then jump
|
||||
| to whatever handler was there before. That keeps the OS clock working and
|
||||
| costs one add per tick.
|
||||
| Human68k already runs MFP Timer C as its system tick, so rather than program
|
||||
| a timer of our own we CHAIN its interrupt: bump a counter, then run whatever
|
||||
| handler was there before. That keeps the OS clock working and costs one add
|
||||
| per tick.
|
||||
|
|
||||
| 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
|
||||
| disturb any register and must not return with RTS. It does neither: addq.l on
|
||||
| an absolute address touches no register, and control leaves through a JMP to
|
||||
| the previous handler, which owns the RTE.
|
||||
| disturb any register and must not return with RTS.
|
||||
|
|
||||
| The JMP's operand is patched at install time by installTickIsr() in hal.c via
|
||||
| the x68kTickChainAddr symbol below. Patching the operand rather than doing an
|
||||
| indirect jump through a pointer is what keeps this register-free -- a 68000
|
||||
| has no jmp ([abs]) addressing mode.
|
||||
| The previous handler is IOCS's software-timer dispatcher ($FF1344 in IPL
|
||||
| 1.0), which runs whichever of its four timer callbacks are due and holds IPL
|
||||
| 6 for all of it: 700-2000 cycles, 100 times a second, measured under MAME.
|
||||
| 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.
|
||||
|
||||
.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
|
||||
.even
|
||||
.globl x68kTickIsr
|
||||
.globl x68kTickChainAddr
|
||||
|
||||
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
|
||||
.word 0x4EF9 | jmp <abs.l>
|
||||
pea x68kTickReturn | the frame the dispatcher's RTE uses
|
||||
move.w #SR_RETURN,%sp@-
|
||||
.word 0x4EF9 | jmp <abs.l>
|
||||
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