More X68k fixes.

This commit is contained in:
Scott Duensing 2026-09-30 16:01:47 -05:00
parent c520b16179
commit 10232e73bb
20 changed files with 2515 additions and 802 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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_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 ISRA_NOT_G6, 0xBF | clear bit 6 (channel 14), keep the rest
.equ CH14_CLEAR, 0xBF | clear bit 6 (channel 14), keep the rest
.equ CRTC_R09, 0xE80012 | raster compare
.equ TEXT_PALETTE, 0xE82200 | the text plane's 16 colours
.equ ARM_LEAD, 1 | arm one raster before X68kBandT.raster
.equ REF_WAIT_LINES, 3 | H-SYNC periods to wait for step 2's edge
| X68kBandT and X68kBandTableT (src/x68000/hal.c, which holds
| these offsets with compile-time size checks).
.equ BAND_PALETTE, 0
.equ BAND_RASTER, 4
.equ BAND_STAY, 6
.equ BAND_SIZE, 8
.equ TABLE_END, 0
.equ TABLE_BANDS, 4
.text
.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

View file

@ -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
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
x68kTickReturn:
move.b %sp@+,MFP_IMRB
move.b %sp@+,MFP_IMRA
rte

23
src/x68000/x68kVdispIsr.s Normal file
View 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