diff --git a/README.md b/README.md index 6a5f50f..801726c 100644 --- a/README.md +++ b/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); diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index b26c35a..a0677eb 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -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. diff --git a/docs/ApiInput.adoc b/docs/ApiInput.adoc index 7ec8994..ba182e7 100644 --- a/docs/ApiInput.adoc +++ b/docs/ApiInput.adoc @@ -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 diff --git a/docs/Portability.adoc b/docs/Portability.adoc index 2e422d5..a32ef7f 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -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: diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc index d615fa0..9c8a861 100644 --- a/docs/ProgramLayout.adoc +++ b/docs/ProgramLayout.adoc @@ -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. diff --git a/examples/scb/scb.c b/examples/scb/scb.c new file mode 100644 index 0000000..5cb2068 --- /dev/null +++ b/examples/scb/scb.c @@ -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 +#include + +#include + +// ----- 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; +} diff --git a/include/joey/platform.h b/include/joey/platform.h index 388b4cc..9d0a331 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -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 diff --git a/make/x68000.mk b/make/x68000.mk index 14721e0..1bffaf0 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -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 diff --git a/scripts/bench-x68000.sh b/scripts/bench-x68000.sh index 8ccc5a5..ac9ba81 100755 --- a/scripts/bench-x68000.sh +++ b/scripts/bench-x68000.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" diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index e2675f1..abe82e5 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -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 diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh index 65fa98b..570e364 100755 --- a/scripts/verify-x68000-golden.sh +++ b/scripts/verify-x68000-golden.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" diff --git a/scripts/verify-x68000-joy.sh b/scripts/verify-x68000-joy.sh new file mode 100755 index 0000000..8689fdd --- /dev/null +++ b/scripts/verify-x68000-joy.sh @@ -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" < 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 diff --git a/scripts/verify-x68000-mouse.sh b/scripts/verify-x68000-mouse.sh index 07f7824..24e69eb 100755 --- a/scripts/verify-x68000-mouse.sh +++ b/scripts/verify-x68000-mouse.sh @@ -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 diff --git a/scripts/verify-x68000-scb.sh b/scripts/verify-x68000-scb.sh new file mode 100755 index 0000000..35c6926 --- /dev/null +++ b/scripts/verify-x68000-scb.sh @@ -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" <> 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(" 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 diff --git a/src/m68k/planarFill.s b/src/m68k/planarFill.s index 427fc02..873c0f9 100644 --- a/src/m68k/planarFill.s +++ b/src/m68k/planarFill.s @@ -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 diff --git a/src/x68000/hal.c b/src/x68000/hal.c index 3941c6e..f1d1d90 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -37,20 +37,84 @@ // width, so the row stride is a constant, not a function of SURFACE_WIDTH. #define X68K_GVRAM_STRIDE 512u -// _iocs_crtmod screen mode. 13 = 512x512, 256 colours, 31 kHz -- same timing as -// mode 12 (512x512/16) one slot below it, but with the 256-entry graphics -// palette this port needs for SCB per-band palettes. There is no 320x200 mode on +// _iocs_crtmod screen mode 12: 512x512, 31 kHz, PROGRESSIVE, ~55.5 Hz. It is +// the 65536-colour graphic mode, which matters to neither present path: the +// text plane has its own 16 colours whatever the graphic mode, and the graphics +// path forces 256 colours (X68K_VIDCTRL0 below). There is no 320x200 mode on // this machine, so the stage is centred and letterboxed. -#define X68K_CRTMOD_512_256 13 -#define X68K_CRTMOD_512_16 12 // 512x512, 16 colours, 31 kHz -- text plane +// +// NOT mode 13, its 15 kHz sibling, which this port used from 2026-09-26: that +// mode is INTERLACED (the CRTC reads back R20 = $0305 and R04 = 259, so a field +// is 260 rasters and each shows every other line of the 512). A real machine +// shows it as two 62 Hz fields that flicker; MAME shows it as one 520-line +// frame at 31 Hz. Either way V-DISP no longer ran at the 55 Hz jlpFrameHz +// then assumed, so the millisecond clock of the time, calibrated on V-DISP, +// was scaled wrongly -- under MAME every elapsed time read 1.78x short and +// every UBER rate 1.78x high -- +// and the SCB raster interrupt got one raster per TWO display lines, so its +// band list pointed at rasters that never come (verify-x68000-scb.sh). +#define X68K_CRTMOD_512_31K 12 // _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about. -#define X68K_KEYGROUP_COUNT 15 +// A scancode's group is scancode >> SHIFT and its bit in that group's byte +// is scancode & BIT_MASK. +#define X68K_KEYGROUP_COUNT 15 +#define X68K_KEYGROUP_SHIFT 3u +#define X68K_KEYGROUP_BIT_MASK 7u // Cap on the per-poll IOCS type-ahead drain for the typed-character // queue -- purely defensive, so a wedged _B_KEYSNS can't spin forever. #define X68K_KEY_DRAIN_GUARD 32u +// IOCS calls jlpInputPoll can answer by reading the IOCS work area instead, +// and the table at $400 that holds each call's handler address. +#define X68K_IOCS_TABLE 0x00000400ul +#define X68K_IOCS_B_KEYSNS 0x01u +#define X68K_IOCS_BITSNS 0x04u +#define X68K_IOCS_MS_GETDT 0x74u +#define X68K_IOCS_MS_CURGT 0x75u + +// The IOCS work area those calls read (IPL 1.0, from the ROM code quoted in +// kRomBitsns and friends below, which is what proves each address). +#define X68K_IOCS_KEY_MATRIX 0x00000800ul // _BITSNS: a byte per group, bit set = down +#define X68K_IOCS_KEY_COUNT 0x00000812ul // word: keys waiting in the type-ahead buffer +#define X68K_IOCS_KEY_INJECTED 0x00000BCAul // word: keys the ROM queued itself, taken first +#define X68K_IOCS_MOUSE_WORK 0x00000A7Aul // the block every _MS_* call points a4 at +// Offsets into the mouse block. The button words are $FFFF held / 0 up and +// _MS_GETDT keeps only each word's low byte (right << 8 | left); the position +// long is the cursor tracker's (x << 16) | y, which is all _MS_CURGT returns. +#define X68K_MOUSE_RIGHT_WORD 0x004Cu +#define X68K_MOUSE_LEFT_WORD 0x004Eu +#define X68K_MOUSE_DX_WORD 0x0050u +#define X68K_MOUSE_DY_WORD 0x0052u +#define X68K_MOUSE_POS 0x0054u +#define X68K_MOUSE_LOW_BYTE 1u // big-endian: a word's low byte is its second + +#define X68K_HI16(_v) ((uint16_t)((uint32_t)(_v) >> 16)) +#define X68K_LO16(_v) ((uint16_t)((uint32_t)(_v) & 0xFFFFu)) + +// The two joystick ports are 8255 PPI ports A and B, one byte each on the odd +// half of consecutive words; the IPL ROM makes both inputs (control word $92, +// written at $FF0BDA). _JOYGET is nothing but a read of that byte (kRomJoyget). +// Bit layout, every line active LOW (MAME x68k.cpp ppi_port_a_r/ppi_port_b_r): +// 0 up, 1 down, 2 left, 3 right, 5 trigger A, 6 trigger B; bits 4 and 7 read +// 1. Stick 1's triggers also read LOW while PPI port C bit 6 (A) or 7 (B) is +// set; the ROM clears port C's upper nibble at boot ($0B at $FF0BE2) and +// src/x68000/audioPcm.c writes only its lower one. +#define X68K_IOCS_JOYGET 0x3Bu +#define X68K_PPI_JOY_PORTS 0x00E9A001ul // port A: stick 1; port B follows +#define X68K_PPI_PORT_STRIDE 2u // bytes between PPI ports +#define X68K_JOY_PORT_COUNT 2u // _JOYGET's own bound on d1 +#define X68K_JOY_RELEASED 0xFFu // every line high: nothing held +#define X68K_JOY_PORT_BITS 8u // a port's byte in joyPortsRead's word +#define X68K_JOY_BOTH_RELEASED ((uint16_t)((X68K_JOY_RELEASED << X68K_JOY_PORT_BITS) | X68K_JOY_RELEASED)) +#define X68K_JOY_Y_SHIFT 0u // up, down: a two-bit pair +#define X68K_JOY_X_SHIFT 2u // left, right: the next pair +#define X68K_JOY_PAIR_MASK 0x03u +#define X68K_JOY_TRIGGER_A 0x20u +#define X68K_JOY_TRIGGER_B 0x40u +typedef char x68kJoyPortsCoverSticks[(X68K_JOY_PORT_COUNT == JOYSTICK_COUNT) ? 1 : -1]; + // _iocs_skey_mod modes (IOCS $7D). The IPL ROM powers up in AUTO -- its boot // copies $FF into the mode byte -- and in AUTO every press of the LEFT mouse // button steps the ROM's own display: its mouse pointer on, then its software @@ -85,28 +149,44 @@ // CRTC register 9: the raster the compare interrupt fires on. It does not // reach the CPU directly -- it drives MFP GPIP6 (channel 14, vector $4E, // exception address $138), which is the vector _CRTCRAS installs into. -#define X68K_CRTC_RASTER ((volatile uint16_t *)0xE80012L) #define X68K_MFP_IERA ((volatile uint8_t *)0xE88007L) // reg 3 #define X68K_MFP_IMRA ((volatile uint8_t *)0xE88013L) // reg 9 #define X68K_MFP_GPIP6 0x40u // channel 14 #define X68K_RASTER_VEC_ADDR 0x00000138ul // vector $4E +// Rasters count from 0 at the vertical sync pulse and step at the start of +// each horizontal front porch; the first displayed line is raster R06 + 1 +// (XEiJ's CRTC documentation). R06 is the last raster of the vertical back +// porch, as _CRTMOD 12 programs it (read back from the CRTC after the call: +// R04 567, R06 40, R07 552). +#define X68K_CRTC_VBACK_END 40u +#define X68K_RASTER_FIRST (X68K_CRTC_VBACK_END + 1u) +// A band's reload is keyed to the raster of the line ABOVE it: the interrupt +// waits out that line's display and stores in the blanking after it (see +// x68kRasterIsr.s), and arms the compare ARM_LEAD rasters before that to get +// there in time. A band starting within X68K_RASTER_STAY_MAX lines of the one +// before is reached by staying in the interrupt instead: re-arming for it +// would set the compare for a raster already under way. +#define X68K_RASTER_ABOVE 1u +#define X68K_RASTER_ARM_LEAD 1u // == ARM_LEAD in x68kRasterIsr.s +#define X68K_RASTER_STAY_MAX 2u +#define X68K_SR_MASK_ALL 0x0700u #define X68K_VBL_SPIN_LIMIT 2000000ul #define X68K_TEE_SPIN_LIMIT 200000ul -// ----- SCB per-band palette (256-colour graphics plane) ---------------------- +// ----- SCB per-band palette on the graphics plane (X68K_PRESENT_TEXT 0) ------- // -// JoeyLib surfaces carry an SCB: one palette index per scanline. This port does -// it the way the DOS port does (src/dos/hal.c:7), not the way the Atari ST does. -// DOS writes `pixel byte = (scb[y] << 4) | nibble` into a 256-entry DAC, so all -// 16 palettes are resident at once and the PIXEL VALUE selects the band. No -// interrupt, no timing window, correct by construction. +// JoeyLib surfaces carry an SCB: one palette index per scanline. The graphics +// path does it the way the DOS port does (src/dos/hal.c:7): `pixel byte = +// (scb[y] << 4) | nibble` into a 256-entry palette, so all 16 palettes are +// resident at once and the PIXEL VALUE selects the band. No interrupt, no +// timing window, correct by construction. The 256-colour graphics plane has +// exactly that: a 256-entry palette at $E82000, 16 palettes x 16 colours. // -// The ST needs a raster interrupt only because the STF shifter really has 16 -// hardware colours. This machine does not: the graphics plane in 256-colour mode -// has a 256-entry palette at $E82000 -- exactly 16 palettes x 16 colours. The -// TEXT plane cannot do it (four 1bpp planes cap it at 16, and MAME's -// get_text_pixel resolves 0-15 only), which is why this port moved off it. +// The default text-plane present cannot (four 1bpp planes cap it at 16 +// colours, one 16-entry palette) and honours the SCB with the raster interrupt +// instead -- see "SCB per-band palette" further down. The text plane wins on +// speed because it is planar like the stage, so presenting is a copy. // // GVRAM layout in 256-colour mode, from MAME x68k_v.cpp:339-340: // colour = gvram[lineoffset0 + x] & 0x000F; // low nibble @@ -138,69 +218,189 @@ #define X68K_VIDCTRL0_256 0x0001u -// ----- Millisecond clock ----------------------------------------------------- +// ----- Clocks ---------------------------------------------------------------- // -// Without this the port fell back to jlpGenericMillisElapsed(), which derives -// milliseconds from the FRAME COUNTER -- and this port's counter is polled -// (vdispPoll), not interrupt-driven. An op spanning more than one frame lets -// V-DISP edges pass unobserved, so the measured time shrank in proportion to how -// slow the op was and the two errors cancelled: every UBER row landed near -// 10,000 ops/sec regardless of workload. Games pacing on jlMillisElapsed were -// equally wrong. +// Two clocks, each counted from a source whose rate is known, neither +// calibrated against the other. // -// Fix: chain Human68k's existing MFP Timer C tick (~100 Hz) and count it. MFP -// channel 5, so vector ($40 | 5) = $45 at address $45 * 4 = $114. -#define X68K_TICK_VEC_ADDR 0x00000114ul -// Timer C's rate is NOT assumed -- it is derived at init from the MFP's own -// registers, because Human68k's programming is not what you would guess. A -// measured run showed ~178 Hz, not the 100 Hz a first guess suggested: -// 4 MHz / (prescale 200 * TCDR 112) = 178.6 Hz. -#define X68K_MFP_TCDCR ((volatile uint8_t *)0xE8801DL) // reg 14 -#define X68K_MFP_TCDR ((volatile uint8_t *)0xE88023L) // reg 17 -#define X68K_MFP_CLOCK_HZ 4000000ul -#define X68K_TCDCR_C_SHIFT 4u -#define X68K_TCDCR_C_MASK 0x07u -// Display frames to count the tick against at init. Sixteen is ~290 ms, long -// enough that one frame of slop is under a percent. -#define X68K_TICK_CAL_FRAMES 16u -// Spin bound for that calibration: comfortably more polls than sixteen frames -// need, so only a machine that is not going to toggle V-DISP ever hits it. -#define X68K_TICK_CAL_GUARD 4000000ul +// MILLISECONDS come from MFP Timer C, Human68k's system tick. The IPL ROM's MFP +// table at $FF0EAA programs it with TCDCR $70 (prescale 200) and TCDR $C8 +// (200), so on the MFP's 4 MHz clock it ticks at exactly 100 Hz, and between +// ticks its live count steps once every 50 us. x68kTickIsr.s counts the ticks +// and the count gives the position inside the current one, so the clock +// resolves to the millisecond rather than to the 10 ms tick. +// +// That live count is also what sank this port's first clock: reading an +// MC68901 timer data register returns the counter as it runs down (MAME +// mc68901.cpp returns m_tmc, as the datasheet says), not the value the timer +// reloads, so dividing the MFP clock by it gave a different wrong rate on +// every run -- 178 Hz once, a 2.8x spread across runs. The clock that replaced +// it counted ticks against display frames at init and truncated to whole Hz: +// 92, 96 or 99 depending on where init fell, every rate in a run off by up to +// 8% and moving with any change to init timing. Now the prescaler is read +// (TCDCR reads back what was written), the reload value is caught in the +// instant it is loaded (tickReloadCapture), and the clock is used only if both +// are Human68k's. +// +// FRAMES come from the V-DISP line through IOCS _VDISPST: MFP Timer A, which +// the ROM's table leaves counting V-DISP edges (TACR $08), interrupts on every +// one and x68kVdispIsr.s counts them. jlpWaitVBL waits for that count to +// move, so it and jlpFrameCount agree on every edge, and no edge is lost however +// long the program goes between calls. +#define X68K_TICK_VEC_ADDR 0x00000114ul // MFP channel 5: vector $45 +#define X68K_MFP_IPRB ((volatile uint8_t *)0xE8800DL) // reg 6 +#define X68K_MFP_TIMER_C 0x20u // channel 5's bit in the B registers +#define X68K_MFP_TCDCR ((volatile uint8_t *)0xE8801DL) // reg 14 +#define X68K_MFP_TCDR ((volatile uint8_t *)0xE88023L) // reg 17 +#define X68K_TCDCR_C_SHIFT 4u +#define X68K_TCDCR_C_MASK 0x07u +#define X68K_TCDCR_C_DIV200 7u // the prescaler code for 200 +#define X68K_MFP_CLOCK_HZ 4000000ul +#define X68K_TICK_PRESCALE 200ul +#define X68K_TICK_RELOAD 200u // the count runs 200..1, then reloads +#define X68K_TICK_STEPS_PER_SEC (X68K_MFP_CLOCK_HZ / X68K_TICK_PRESCALE) +#define X68K_TICK_STEPS_PER_MS (X68K_TICK_STEPS_PER_SEC / 1000u) +#define X68K_TICK_MS (X68K_TICK_RELOAD / X68K_TICK_STEPS_PER_MS) +typedef char x68kTickIsWholeMs[((X68K_TICK_RELOAD % X68K_TICK_STEPS_PER_MS) == 0u) ? 1 : -1]; +// tickReloadCapture: a count at or under RELOAD_SOON is at most that many +// steps from reloading, so the wait for the reload with every interrupt masked +// is that short (~100 us). A try the count outruns is retried. +#define X68K_TICK_RELOAD_SOON 2u +#define X68K_TICK_CAPTURE_TRIES 4u +#define X68K_TICK_RELOAD_SPIN 1000u // polls for the reload: far more than two steps take +// Spin bound for waiting on the count: comfortably more polls than a whole +// tick needs, so only a stopped timer ever hits it. +#define X68K_TICK_SPIN_GUARD 100000ul + +// _VDISPST's d1: bit 8 set counts the rising edge of V-DISP (the start of the +// display period, the edge this port has always counted); the low byte is how +// many edges per interrupt. +#define X68K_VDISP_DISPLAY_EDGE 1 +#define X68K_VDISP_EVERY_EDGE 1 + +// jlpFrameHz: the refresh, timed on the Timer C clock across this many frames +// at init (~290 ms) and rounded to the nearest whole Hz. That is 55 on a real +// machine: crtmod 12 is 23.184 MHz dots, 736 to a line and 568 lines, 55.46 Hz. +// Under MAME 0.264 it is 56: its CRTC times a line as 8 dots short +// (x68k_crtc.cpp refresh_mode uses htotal - 8), 56.07 Hz, measured. Timed to +// +-50 us at each end, the estimate is good to about 0.02 Hz, so the real +// machine's 0.04 Hz margin from rounding up holds. FRAME_HZ_NOMINAL stands +// when there is no Timer C clock to time it on. +#define X68K_REFRESH_CAL_FRAMES 16u +#define X68K_FRAME_HZ_NOMINAL 55u +// An edge seen fewer than this many steps (300 us) after a tick may have been +// seen late: the tick's handler runs the IOCS timer dispatcher for up to +// ~200 us with the V-DISP interrupt masked (x68kTickIsr.s). Such an edge is +// not timed; the next one is, and EDGE_TRIES bounds how many are passed over. +#define X68K_EDGE_LATE_STEPS 6u +#define X68K_EDGE_TRIES 8u // ----- Module state --------------------------------------------------------- static int gPrevCrtMode = -1; static bool gModeSet = false; -static uint16_t gFrameCount = 0; // Saved USP from the _dos_super(0) switch, or -1 if we were already in // supervisor mode and must not switch back. static int gPrevSsp = -1; -// Last sampled V-DISP level, for the frame-counter edge detect. +// Last sampled V-DISP level, for vdispPoll's edge detect. static uint8_t gLastVdisp = 0; -// True once the V-DISP interrupt handler is live; the poll then stands down. -static bool gVdispInstalled = false; -// Bumped by x68kTickIsr.s on every Human68k Timer C interrupt. Written from -// interrupt context, so volatile; read with a single long load, which is atomic -// against the handler's addq.l on a 68000. -volatile uint32_t gX68kTicks = 0ul; +// Input. gInputDirect is set by jlpInputInit when every IOCS call the poll +// stands in for still lands on the ROM code it was proven against. The key +// groups invert kScanForKey: group g's keys are gKeyGroupKey[first[g] .. +// first[g + 1]), each tested with its gKeyGroupMask bit. gKeyGroupBits is +// each group's byte as of the last poll, which gKeyState already reflects. +static bool gInputDirect = false; +static uint8_t gKeyGroupFirst[X68K_KEYGROUP_COUNT + 1]; +static uint8_t gKeyGroupKey [KEY_COUNT]; +static uint8_t gKeyGroupMask [KEY_COUNT]; +static uint8_t gKeyGroupBits [X68K_KEYGROUP_COUNT]; +// Joysticks. gJoyDirect is set when _JOYGET is still the ROM's port read, so +// the poll reads the PPI itself; a resident driver that hooked _JOYGET for +// another kind of pad keeps the trap. gJoyPorts is both ports as of the last +// poll (joyPortsRead's word), which the gJoy* state already reflects. +static bool gJoyDirect = false; +static uint16_t gJoyPorts = X68K_JOY_BOTH_RELEASED; + +// Clocks. gX68kTicks is bumped by x68kTickIsr.s on every Timer C interrupt, +// gX68kFrames by x68kVdispIsr.s on every V-DISP edge -- or by vdispPoll when +// _VDISPST would not take the handler. Written from interrupt context, so +// volatile; each is read with a single load, which is atomic against its +// handler's single add on a 68000. +volatile uint32_t gX68kTicks = 0ul; +volatile uint16_t gX68kFrames = 0u; extern void x68kTickIsr(void); +extern void x68kVdispIsr(void); extern uint32_t x68kTickChainAddr; // the JMP operand inside the ISR -static uint32_t gTickBase = 0ul; -static uint32_t gTickHz = 0ul; -static bool gTickBaseSet = false; -static bool gTickHooked = false; +static bool gTickHooked = false; // Timer C is Human68k's 100 Hz and chained +static bool gVdispHooked = false; // _VDISPST took x68kVdispIsr +static uint32_t gMillisBase = 0ul; // tickMillisNow() as jlpInit finished +static uint16_t gFrameHz = X68K_FRAME_HZ_NOMINAL; + +#if X68K_PRESENT_TEXT +// ----- SCB band tables (text-plane present) ----------------------------------- +// +// The text plane has ONE 16-entry palette, so honouring the SCB means reloading +// it as the beam scans: one reload per TRANSITION between lines with different +// SCB values, worst case one per line. src/x68000/x68kRasterIsr.s does the +// reloads from a table built here -- see "SCB per-band palette" below. +#define X68K_MAX_BANDS SURFACE_HEIGHT +#define X68K_NO_PALETTE 0xFFu // no line's SCB value; starts the first band + +// One band as the raster interrupt reads it. x68kRasterIsr.s addresses the +// fields by offset; the size checks below hold both sides to one layout. +typedef struct { + const uint16_t *palette; // its 16 colours, already in hardware format + uint16_t raster; // the raster of the line above its first line + uint16_t stay; // lines to the next band if the interrupt stays for it, else 0 +} X68kBandT; + +// Everything one frame of reloads reads. There are two: the interrupt walks +// one while the main thread builds the other, and takes the new one up at the +// frame wrap, so rebuilding never disturbs the frame being scanned. +typedef struct { + const X68kBandT *end; // one past the last band + X68kBandT bands[X68K_MAX_BANDS]; + uint16_t palettes[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +} X68kBandTableT; + +typedef char x68kBandPaletteAt0[(offsetof(X68kBandT, palette) == 0u) ? 1 : -1]; +typedef char x68kBandRasterAt4[(offsetof(X68kBandT, raster) == 4u) ? 1 : -1]; +typedef char x68kBandStayAt6[(offsetof(X68kBandT, stay) == 6u) ? 1 : -1]; +typedef char x68kBandSizeIs8[(sizeof(X68kBandT) == 8u) ? 1 : -1]; +typedef char x68kTableEndAt0[(offsetof(X68kBandTableT, end) == 0u) ? 1 : -1]; +typedef char x68kTableBandsAt4[(offsetof(X68kBandTableT, bands) == 4u) ? 1 : -1]; + +extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s + +// Shared with x68kRasterIsr.s, hence not static. Written by the main thread +// only with every interrupt masked; the interrupt runs to completion. +X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked +X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap +const X68kBandT *volatile gX68kRasterCursor = NULL; // the next band to reload +static X68kBandTableT gBandTables[2]; +static bool gRasterArmed = false; +static uint32_t gRasterOldVec = 0ul; // $138 before we took it +#endif // ----- Prototypes ----- -static uint32_t tickHzFromMfp(void); -uint32_t jlpMillisElapsed(void); -uint16_t jlpFrameHz(void); -static void calibrateTickHz(void); -static void installTickIsr(void); +static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps); +static void installTickIsr(void); +static void installVdispIsr(void); +static void measureFrameHz(void); +static uint32_t tickMillisNow(void); +static uint32_t tickRead(uint16_t *steps); +static uint16_t tickReloadCapture(void); +static void vdispPoll(void); +static inline uint16_t x68kInterruptsOff(void); +static inline void x68kInterruptsRestore(uint16_t sr); #if X68K_PRESENT_TEXT -static void clearTextPlane(void); -static void disarmRaster(void); +static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src); +static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src); +static void clearTextPlane(void); +static void disarmRaster(void); +static void installScb(const jlSurfaceT *src); #endif #if !X68K_PRESENT_TEXT static void buildSpreadTables(void); @@ -208,10 +408,32 @@ static void spreadSetPalBase(uint8_t palBase); static void uploadGfxPalette(const jlSurfaceT *src); #endif -static void vdispHandler(void); -static void vdispPoll(void); +static bool inputDirectUsable(void); +static uint32_t iocsHandler(uint8_t call); +static uint16_t joyPortsRead(void); +static void joyUpdate(jlJoystickE stick, uint8_t bits); +static void keyGroupsBuild(void); +static void keyGroupsUpdate(const volatile uint8_t *matrix); +static bool keysQueued(void); +static bool romMatches(uint32_t addr, const uint16_t *words, uint16_t bytes); +static bool romMouseCallMatches(uint8_t call, const uint16_t *tail, uint16_t tailBytes); +static inline const volatile void *x68kLowMem(uint32_t addr); -extern void *const gX68kVdispHandlerRef; + +// ----- Interrupt mask --------------------------------------------------------- + +// Mask every interrupt; returns the SR to hand back to x68kInterruptsRestore. +static inline uint16_t x68kInterruptsOff(void) { + uint16_t sr; + + __asm__ volatile ("move.w %%sr,%0\n\tori.w %1,%%sr" : "=d" (sr) : "i" (X68K_SR_MASK_ALL) : "memory"); + return sr; +} + + +static inline void x68kInterruptsRestore(uint16_t sr) { + __asm__ volatile ("move.w %0,%%sr" :: "d" (sr) : "memory"); +} // ----- Lifecycle ------------------------------------------------------------ @@ -232,23 +454,27 @@ bool jlpInit(const jlConfigT *config) { // rid of Human68k's desktop -- which the graphics-plane path could only // leave bleeding over the frame (a reg[2] &= ~0x20 write to hide it did NOT // take on MAME). Presenting INTO the text plane makes that bleed go away by - // construction: we own every pixel of it. - // Mode 13, NOT the 16-colour mode 12 you might expect for a 4bpp text - // plane. MEASURED: crtmod 12 costs about 45% of ALL CPU throughput -- - // jlKeyDown fell 63728 -> 34624 ops/sec and jlMouseX 108679 -> 60071, and - // those touch nothing but RAM, so it is the mode and not the drawing. Mode - // 13 keeps full speed and displays the text plane anyway: the desktop - // "bleeding over our frame" that this port used to document as a cosmetic - // wart was the text plane being composited ON TOP, which is exactly what - // we now want. _G_CLR_ON blacks the graphics plane underneath and the wipe - // takes the desktop away. - _iocs_crtmod(X68K_CRTMOD_512_256); + // construction: we own every pixel of it. The desktop "bleeding over our + // frame" that this port used to document as a cosmetic wart was the text + // plane being composited ON TOP, which is exactly what we now want. + // _G_CLR_ON blacks the graphics plane underneath and the wipe takes the + // desktop away. + // + // This port once measured mode 12 as costing "about 45% of ALL CPU + // throughput" against mode 13 (jlKeyDown 63728 -> 34624 ops/sec). That was + // the clock, not the CPU: mode 13 is interlaced, MAME runs its V-DISP at + // 31 Hz, and the clock's calibration assumed 55, so mode 13's rates read 1.78x high + // on every op alike. Counted against emulated time instead, a loop that + // redraws and presents the whole stage ran at the same rate in both modes + // (4.3 passes a second in 13, 4.0 in 12, where the SCB raster interrupt + // was also running). + _iocs_crtmod(X68K_CRTMOD_512_31K); _iocs_g_clr_on(); clearTextPlane(); #else - _iocs_crtmod(X68K_CRTMOD_512_256); + _iocs_crtmod(X68K_CRTMOD_512_31K); _iocs_g_clr_on(); // clear graphics + enable the plane - // crtmod 13 leaves the video controller in 65536-colour DIRECT mode (reg[0] & 3 == 3). This port + // The 65536-colour modes (12 and 13) leave the video controller in DIRECT mode (reg[0] & 3 == 3). This port // draws 8-bit palette indices, so force 256-colour palette-indexed mode or every colour renders // near-black. Root-caused + proven live on MAME (reg0 0x0003 -> 0x0001 turned a black frame into a // correct red/yellow/blue test pattern). @@ -258,17 +484,20 @@ bool jlpInit(const jlConfigT *config) { // reg[2] &= ~0x20 write to hide it did NOT take on MAME (the desktop stayed visible), so that cosmetic // bleed is left as a follow-up; it does not affect the graphic layers we draw into. #endif - // NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine -- - // tested, no serial output at all, so it wedges before main() gets going. - // IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into - // exception vector $134 (iplrom.dat $FF9DC8), exactly as _CRTCRAS does for - // $138. So the handler must be a raw interrupt routine -- preserve every - // register, return with RTE -- and a C function (RTS, free to clobber - // d0/d1/a0/a1) can never be valid. A raw asm handler would be required; - // the polled fallback below owns the frame counter until then. - // Until then the polled fallback below owns the counter. - gVdispInstalled = false; + // Human68k's console cursor blinks by XORing an 8x16 block into text + // planes 0 and 1 from IOCS's Timer C callback ($FF137A), twice a second + // -- into this port's display, wherever the console cursor was left, which + // for a program launched from a prompt low on the screen is inside the + // stage. Off while JoeyLib owns the screen, and after the crtmod call, + // which resets the console. jlpShutdown turns it back on. + _iocs_b_curoff(); + // The clocks, then the refresh timed on them (see "Clocks"). installTickIsr(); + installVdispIsr(); + measureFrameHz(); + if (gTickHooked) { + gMillisBase = tickMillisNow(); + } gModeSet = true; return true; } @@ -287,11 +516,17 @@ void jlpShutdown(void) { volatile uint32_t *vec = (volatile uint32_t *)addr; uint16_t sr; - __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr)); + sr = x68kInterruptsOff(); *vec = x68kTickChainAddr; - __asm__ volatile ("move.w %0,%%sr" :: "d" (sr)); + x68kInterruptsRestore(sr); gTickHooked = false; } + // The same for $134: _VDISPST with no handler masks Timer A's interrupt + // and puts back the ROM's unused-vector marker. + if (gVdispHooked) { + (void)_iocs_vdispst(NULL, 0, 0); + gVdispHooked = false; + } #if X68K_PRESENT_TEXT disarmRaster(); #endif @@ -307,9 +542,11 @@ void jlpShutdown(void) { // above so those defaults match the screen the user is going back to. // The software keyboard's AUTO mode goes back too (see X68K_SKEY_OFF): IOCS // has no call that reports the mode, so the ROM's power-on default is - // what is restored. + // what is restored. So does the console cursor jlpInit turned off, for + // the same reason: Human68k's console runs with it on. _iocs_ms_init(); (void)_iocs_skey_mod(X68K_SKEY_AUTO, 0, 0); + _iocs_b_curon(); gModeSet = false; if (gPrevSsp >= 0) { (void)_dos_super(gPrevSsp); // back to user mode for Human68k @@ -318,33 +555,41 @@ void jlpShutdown(void) { } -// Timer C's interrupt rate, read from the MFP rather than assumed: -// MFP clock / (prescaler * data register). The prescaler is a 3-bit code in the -// high nibble of TCDCR; code 0 means the timer is stopped. -static uint32_t tickHzFromMfp(void) { - static const uint16_t kPrescale[8] = { 0u, 4u, 10u, 16u, 50u, 64u, 100u, 200u }; - uint16_t code; - uint16_t divisor; - uint16_t data; +// The Timer C steps at which the next frame edge was seen, and the frame it +// began. An edge seen within X68K_EDGE_LATE_STEPS of a tick may have been seen +// late and is passed over for the next. False if no edge came (a CRTC that is +// not running) or every edge tried was a late one. +static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps) { + uint16_t tries; + uint16_t before; + uint16_t into; + uint32_t ticks; - code = (uint16_t)((*X68K_MFP_TCDCR >> X68K_TCDCR_C_SHIFT) & X68K_TCDCR_C_MASK); - divisor = kPrescale[code]; - if (divisor == 0u) { - return 0ul; + for (tries = 0u; tries < X68K_EDGE_TRIES; tries++) { + before = gX68kFrames; + jlpWaitVBL(); + ticks = tickRead(&into); + if (gX68kFrames == before) { + return false; + } + if (into >= X68K_EDGE_LATE_STEPS) { + *frame = gX68kFrames; + *steps = ticks * X68K_TICK_RELOAD + into; + return true; + } } - data = *X68K_MFP_TCDR; - if (data == 0u) { - data = 256u; // 0 in the data register means 256 - } - return X68K_MFP_CLOCK_HZ / ((uint32_t)divisor * (uint32_t)data); + return false; } -// Chain our counter onto Human68k's Timer C interrupt. The previous handler is -// patched into the ISR's JMP operand, so it still runs and still owns the RTE -- -// the OS clock keeps working. Supervisor already (jlpInit's _dos_super), so the -// vector write is legal. Refuses if the vector looks unset, because chaining to -// nothing would fault on the first tick. +// Chain our counter onto Human68k's Timer C interrupt, if Timer C is the +// 100 Hz tick the clock is built on: prescaler code 7 in TCDCR and a reload of +// 200 caught as it happens. The previous handler is patched into the ISR's +// call operand, so it still runs -- the OS clock keeps working -- with only +// the SCB raster interrupt able to cut in (see x68kTickIsr.s for why). +// Supervisor already (jlpInit's _dos_super), so the vector write is legal. +// Refuses if the vector looks unset, because chaining to nothing would fault +// on the first tick. static void installTickIsr(void) { // Laundered through a volatile: gcc treats a constant pointer this close to // zero as a null dereference and -Werror=array-bounds rejects it, though @@ -356,70 +601,148 @@ static void installTickIsr(void) { if (*vec == 0ul) { return; } - gTickHz = tickHzFromMfp(); - if (gTickHz == 0ul) { - return; // timer stopped: no usable clock + if (((*X68K_MFP_TCDCR >> X68K_TCDCR_C_SHIFT) & X68K_TCDCR_C_MASK) != X68K_TCDCR_C_DIV200) { + return; // stopped, or a rate this clock does not know + } + if (tickReloadCapture() != X68K_TICK_RELOAD) { + return; } x68kTickChainAddr = *vec; *vec = (uint32_t)x68kTickIsr; gTickHooked = true; - calibrateTickHz(); } -// Count real ticks across a known number of display frames and believe THAT -// rather than the MFP registers. -// -// Reading the prescale and data registers looks exact and is not: two runs of -// the same benchmark reported 50 s and 116 s of elapsed time for runs that -// really took 118 s and 100 s, a 2.7x disagreement between them, because the -// rate Human68k leaves Timer C at is not the rate those registers describe by -// the time a program reads them. Everything derived from the clock was wrong -// by that factor, which is how a performance change came back looking three -// times slower than the code it replaced. -// -// The display frame is the one rate on this machine that cannot drift: it -// comes from the CRT mode. Sixteen frames is about 290 ms of init. -static void calibrateTickHz(void) { - uint8_t last; - uint16_t frames = 0u; - uint32_t start; - uint32_t ticks; - uint32_t guard; +// Count frames in x68kVdispIsr.s. _VDISPST writes the handler straight into +// vector $134 (iplrom.dat $FF9DC8) with no IOCS wrapper -- which is why the C +// function (RTS, free to clobber d0/d1/a0/a1) this port first handed it hung +// the machine -- loads Timer A with the edges per interrupt, picks the edge in +// the MFP's AER and enables the channel. It refuses, returning the vector, +// when another program already holds $134; vdispPoll counts instead. +static void installVdispIsr(void) { + gX68kFrames = 0u; + gLastVdisp = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); + gVdispHooked = (_iocs_vdispst(x68kVdispIsr, X68K_VDISP_DISPLAY_EDGE, X68K_VDISP_EVERY_EDGE) == 0); +} - // Every spin is bounded. A machine where V-DISP never toggles would - // otherwise hang here in jlInit, which is far worse than a clock that is - // merely wrong: the guard gives up and keeps the register-derived rate. - last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); - guard = X68K_TICK_CAL_GUARD; - while ((uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u) == last) { - if (--guard == 0ul) { + +// Time X68K_REFRESH_CAL_FRAMES frames on the Timer C clock and round to the +// nearest whole Hz. Without the clock, or without frame edges to time, +// jlpFrameHz stays nominal. +static void measureFrameHz(void) { + uint16_t startFrame; + uint16_t frame; + uint16_t frames; + uint32_t startSteps; + uint32_t steps; + + gFrameHz = X68K_FRAME_HZ_NOMINAL; + if (!gTickHooked || !frameEdgeSteps(&startFrame, &startSteps)) { + return; + } + do { + if (!frameEdgeSteps(&frame, &steps)) { return; } - } - last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); - start = gX68kTicks; - guard = X68K_TICK_CAL_GUARD; - while (frames < X68K_TICK_CAL_FRAMES) { - uint8_t now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); + frames = (uint16_t)(frame - startFrame); + } while (frames < X68K_REFRESH_CAL_FRAMES); + steps -= startSteps; + gFrameHz = (uint16_t)(((uint32_t)frames * X68K_TICK_STEPS_PER_SEC + steps / 2u) / steps); +} - if (now != last) { - last = now; - if (now != 0u) { - frames++; + +// Milliseconds on the Timer C clock: X68K_TICK_MS a tick, plus the whole +// milliseconds the live count has run since the last one. +static uint32_t tickMillisNow(void) { + uint16_t into; + uint32_t ticks; + + ticks = tickRead(&into); + return ticks * X68K_TICK_MS + (uint32_t)(into / X68K_TICK_STEPS_PER_MS); +} + + +// One consistent reading of the Timer C clock: the ticks counted, and in +// *steps how far the live count has run down since the last reload +// (0 .. X68K_TICK_RELOAD - 1). Read again if a tick is counted between the +// reads. A reload the interrupt has not yet counted -- its pending bit still +// up in IPRB -- belongs to this reading only if the count read is the +// reloaded one, still near the top; a count near the bottom was read before +// the reload. +static uint32_t tickRead(uint16_t *steps) { + uint32_t ticks; + uint8_t count; + bool pending; + + do { + ticks = gX68kTicks; + count = *X68K_MFP_TCDR; + pending = (*X68K_MFP_IPRB & X68K_MFP_TIMER_C) != 0u; + } while (ticks != gX68kTicks); + if (pending && count > X68K_TICK_RELOAD / 2u) { + ticks++; + } + *steps = (uint16_t)(X68K_TICK_RELOAD - count); + return ticks; +} + + +// The value Timer C reloads, caught as it is loaded: a read of TCDR returns +// the live count, never the reload value (see "Clocks"). Wait until the count +// is within X68K_TICK_RELOAD_SOON steps of running out, then mask every +// interrupt, so nothing can hold a read past the one step (50 us) the fresh +// value lasts, and watch it jump. The mask lasts those few steps; the tick it +// holds back is taken as it lifts. A try that starts too late (the count has +// already jumped) is retried. 0 if no try saw the reload. +static uint16_t tickReloadCapture(void) { + uint32_t guard; + uint16_t tries; + uint16_t spin; + uint16_t sr; + uint8_t last; + uint8_t now; + + for (tries = 0u; tries < X68K_TICK_CAPTURE_TRIES; tries++) { + for (guard = 0ul; *X68K_MFP_TCDR > X68K_TICK_RELOAD_SOON; guard++) { + if (guard == X68K_TICK_SPIN_GUARD) { + return 0u; // the count never runs out: stopped } } - if (--guard == 0ul) { - return; + sr = x68kInterruptsOff(); + last = *X68K_MFP_TCDR; + now = last; + for (spin = 0u; spin < X68K_TICK_RELOAD_SPIN && last <= X68K_TICK_RELOAD_SOON; spin++) { + now = *X68K_MFP_TCDR; + if (now > last) { + break; + } + last = now; + } + x68kInterruptsRestore(sr); + if (now > last) { + return now; } } - ticks = gX68kTicks - start; - if (ticks == 0ul) { - return; // ISR never fired: keep the guess + return 0u; +} + + +// Count V-DISP's rising edges by polling, for when _VDISPST refused the +// handler. It sees an edge only while the program is inside a call that polls +// -- jlpPresent, jlpFrameCount, jlpWaitVBL -- so a program that goes longer +// than a frame between them loses frames, which the interrupt never does. +static void vdispPoll(void) { + uint8_t now; + + if (gVdispHooked) { + return; } - gTickHz = (ticks * (uint32_t)jlpFrameHz()) / (uint32_t)X68K_TICK_CAL_FRAMES; - if (gTickHz == 0ul) { - gTickHz = tickHzFromMfp(); + now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); + if (now != gLastVdisp) { + gLastVdisp = now; + if (now != 0u) { + gX68kFrames++; + } } } @@ -427,101 +750,74 @@ static void calibrateTickHz(void) { // ----- SCB per-band palette -------------------------------------------------- #if X68K_PRESENT_TEXT -// The text plane has one 16-entry palette, so honouring the SCB means changing -// it as the beam scans. A band is a RUN of scanlines sharing an SCB value, and -// only transitions matter: one CRTC raster interrupt per transition, which for -// a single-palette screen (Space Taxi reports one band) is none at all. +// A band is a RUN of lines sharing an SCB value, and only transitions cost +// anything: one palette reload each, which for a single-palette screen (Space +// Taxi reports one band) is none at all -- the palette is written once and no +// interrupt is armed. SCB values are freely reusable, and reuse does NOT reduce +// the reload count: an A-B-A-B screen uses two palettes but still needs a +// reload at every change, because the hardware palette was overwritten in +// between. The cost is the number of TRANSITIONS, worst case one per line. // -// SCB values are freely reusable, and reuse does NOT reduce the interrupt -// count: an A-B-A-B screen uses two palettes but still needs a reload at every -// change, because the hardware palette was overwritten in between. The cost is -// the number of TRANSITIONS, worst case one per scanline. -#define X68K_MAX_BANDS SURFACE_HEIGHT - -extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s - -// Every palette pre-converted to the hardware's word format, so the interrupt -// service copies words and does no arithmetic. Rebuilt only when a palette -// changes. -static uint16_t gScbPal[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; -static uint16_t gBandLine[X68K_MAX_BANDS]; // display line the band starts on -static uint8_t gBandPal[X68K_MAX_BANDS]; // which palette it selects -static uint16_t gBandCount; -static volatile uint16_t gBandNext; // service cursor -static bool gRasterArmed; -static uint32_t gRasterOldVec; // $138 before we took it +// Timing (scripts/verify-x68000-scb.sh measures it on every transition of a +// screen with 147 of them): x68kRasterIsr.s waits out the display of the line +// ABOVE a band and stores the band's sixteen colours with one movem in the +// horizontal blanking after it, between the last stage pixel of one line and +// the first of the next. A band that starts within X68K_RASTER_STAY_MAX lines +// of the previous one is reached by staying in the interrupt, so a run of +// per-line changes holds the interrupt, at IPL 6, for the whole run: about +// one raster (31.8 us) per transition, and about two for a band it re-arms for. -// Interrupt service, called from the raw shim in x68kRasterIsr.s with every -// register already saved -- which is what lets this be ordinary C. Loads the -// current band's palette and re-points the compare register at the next band, -// wrapping to band 0 for the next frame. -void x68kRasterService(void) { - const uint16_t *pal; - uint16_t band; - uint16_t col; +// One band per RUN of equal SCB values, in the raster units the compare wants. +// Returns the band count, never 0: the first line always starts a band. +static uint16_t buildScbBands(X68kBandTableT *table, const jlSurfaceT *src) { + X68kBandT *band; + uint16_t count; + uint16_t line; + uint16_t prevLine; + uint8_t idx; + uint8_t prev; - band = gBandNext; - if (band >= gBandCount) { - band = 0u; + count = 0u; + prevLine = 0u; + prev = X68K_NO_PALETTE; + for (line = 0u; line < SURFACE_HEIGHT; line++) { + idx = src->scb[line]; + if (idx >= SURFACE_PALETTE_COUNT) { + idx = 0u; // a corrupt SCB read from a file + } + if (idx == prev) { + continue; + } + if (count != 0u && (uint16_t)(line - prevLine) <= X68K_RASTER_STAY_MAX) { + table->bands[count - 1u].stay = (uint16_t)(line - prevLine); + } + band = &table->bands[count]; + band->palette = table->palettes[idx]; + band->raster = (uint16_t)(X68K_RASTER_FIRST + X68K_ORIGIN_Y + line - X68K_RASTER_ABOVE); + band->stay = 0u; + count++; + prevLine = line; + prev = idx; } - pal = gScbPal[gBandPal[band]]; - for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { - X68K_TEXT_PALETTE[col] = pal[col]; - } - band = (uint16_t)((band + 1u < gBandCount) ? (band + 1u) : 0u); - gBandNext = band; - *X68K_CRTC_RASTER = gBandLine[band]; + table->end = &table->bands[count]; + return count; } -// Quantise every palette once into the form the service copies. -static void buildScbPalettes(const jlSurfaceT *src) { +// Quantise every palette once into the form the interrupt stores. +static void buildScbPalettes(X68kBandTableT *table, const jlSurfaceT *src) { uint16_t pal; uint16_t col; for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { - gScbPal[pal][col] = x68kColorFromRgb12(src->palette[pal][col]); + table->palettes[pal][col] = x68kColorFromRgb12(src->palette[pal][col]); } } } -// One band per RUN of equal SCB values. Lines are stored already offset into -// display space, because that is what the compare register wants. -static void buildScbBands(const jlSurfaceT *src) { - uint16_t line; - uint8_t idx; - uint8_t prev; - - gBandCount = 0u; - prev = 0xFFu; - for (line = 0u; line < SURFACE_HEIGHT; line++) { - idx = src->scb[line]; - if (idx >= SURFACE_PALETTE_COUNT) { - idx = 0u; - } - if (idx != prev) { - if (gBandCount < X68K_MAX_BANDS) { - gBandLine[gBandCount] = (uint16_t)(line + X68K_ORIGIN_Y); - gBandPal[gBandCount] = idx; - gBandCount++; - } - prev = idx; - } - } - if (gBandCount == 0u) { // no line claimed a palette - gBandLine[0] = X68K_ORIGIN_Y; - gBandPal[0] = 0u; - gBandCount = 1u; - } -} - - -// A single-palette screen needs no interrupt: load it once and leave the -// hardware alone. Anything else arms the compare on band 0 and lets the -// service walk the list. // Mask GPIP6 and hand $138 back. This MUST run before the program exits: the // handler lives in this program's RAM, so leaving the vector pointing at it // sends the next raster interrupt into freed memory -- the same dangling-ISR @@ -532,42 +828,60 @@ static void disarmRaster(void) { if (!gRasterArmed) { return; } - __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr)); + sr = x68kInterruptsOff(); *X68K_MFP_IMRA = (uint8_t)(*X68K_MFP_IMRA & (uint8_t)~X68K_MFP_GPIP6); *X68K_MFP_IERA = (uint8_t)(*X68K_MFP_IERA & (uint8_t)~X68K_MFP_GPIP6); if (gRasterOldVec != 0ul) { static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; *(volatile uint32_t *)addr = gRasterOldVec; } - __asm__ volatile ("move.w %0,%%sr" :: "d" (sr)); + x68kInterruptsRestore(sr); gRasterArmed = false; } -static void applyScbBands(void) { - uint16_t col; +// Hand a new screen's bands to the interrupt. They are built into the table +// the interrupt is not walking and queued for the next frame wrap, so the frame +// being scanned finishes with the bands it started with: a fade or an SCB +// change can lag the pixels by up to a frame but never tears one. A +// single-band screen needs no interrupt: its palette is written here, once. +static void installScb(const jlSurfaceT *src) { + X68kBandTableT *table; + uint16_t sr; + uint16_t col; - if (gBandCount <= 1u) { + // Withdraw any queued table first, so the interrupt cannot take up the one + // about to be rewritten; after that the table it walks cannot change. + sr = x68kInterruptsOff(); + gX68kRasterPending = NULL; + table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0]; + x68kInterruptsRestore(sr); + buildScbPalettes(table, src); + if (buildScbBands(table, src) == 1u) { disarmRaster(); for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { - X68K_TEXT_PALETTE[col] = gScbPal[gBandPal[0]][col]; + X68K_TEXT_PALETTE[col] = table->bands[0].palette[col]; } return; } - gBandNext = 0u; - if (!gRasterArmed) { - // _CRTCRAS takes the handler and the line, and owns enabling GPIP6 in - // the MFP. The handler must be the raw shim, never a C function. - // Remember the old vector first so shutdown can put it back. - { - static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; - gRasterOldVec = *(volatile uint32_t *)addr; - } - (void)_iocs_crtcras(x68kRasterIsr, (int)gBandLine[0]); - gRasterArmed = true; - } else { - *X68K_CRTC_RASTER = gBandLine[0]; + sr = x68kInterruptsOff(); + if (gRasterArmed) { + gX68kRasterPending = table; + x68kInterruptsRestore(sr); + return; } + gX68kRasterTable = table; + gX68kRasterCursor = table->bands; + x68kInterruptsRestore(sr); + // _CRTCRAS takes the handler and the first band's raster, and owns + // enabling GPIP6 in the MFP. The handler must be the raw shim, never a C + // function. Remember the old vector first so shutdown can put it back. + { + static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; + gRasterOldVec = *(volatile uint32_t *)addr; + } + (void)_iocs_crtcras(x68kRasterIsr, (int)(table->bands[0].raster - X68K_RASTER_ARM_LEAD)); + gRasterArmed = true; } #endif // X68K_PRESENT_TEXT @@ -765,17 +1079,12 @@ void jlpPresent(const jlSurfaceT *src) { return; } // gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume - // both here the way src/iigs/hal.c does. A palette change alters what the - // bands load, so it forces the band apply too. - if (gStagePaletteDirty) { - buildScbPalettes(src); + // both here the way src/iigs/hal.c does. Either one rebuilds the bands, + // because every band table carries its own copy of the palettes. + if (gStagePaletteDirty || gStageScbDirty) { + installScb(src); gStagePaletteDirty = false; - gStageScbDirty = true; - } - if (gStageScbDirty) { - buildScbBands(src); - applyScbBands(); - gStageScbDirty = false; + gStageScbDirty = false; } for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) { if (minGroups[g] == 0xFFFFFFFFul) { @@ -904,9 +1213,297 @@ void jlpPresent(const jlSurfaceT *src) { // ----- Input ---------------------------------------------------------------- -// _iocs_bitsns hands back a raw per-group key-DOWN bitmap, so there is no ISR -// to install, no vector to take over and no packet state machine -- unlike the +// The IOCS keeps a raw per-group key-DOWN bitmap, so there is no ISR to +// install, no vector to take over and no packet state machine -- unlike the // ST, which has to replace the TOS ikbdsys vector and decode IKBD packets. +// +// The poll reads the IOCS work area directly rather than trapping. _BITSNS, +// _B_KEYSNS (with nothing typed), _MS_CURGT and _MS_GETDT are nothing but +// reads of that RAM, yet a trap costs 40-90 us: the exception, the ROM's +// dispatcher and, for the mouse calls, a fifteen-register save and restore. +// The eighteen a poll used to make were ~870 us of its ~2 ms under MAME. The +// arrays below are the ROM code each read stands in for, word for word, with +// the work-area addresses spelled into them -- so matching them is what +// proves those addresses. jlpInputInit compares every handler the IOCS table +// names against them and keeps the traps on any difference: another ROM +// revision, or a resident program that hooked one of the calls. + +// _BITSNS ($FF6172 in IPL 1.0): the group's byte of the key matrix. +static const uint16_t kRomBitsns[] = { + 0x4280, // clr.l d0 + 0x41F9, X68K_HI16(X68K_IOCS_KEY_MATRIX), X68K_LO16(X68K_IOCS_KEY_MATRIX), // lea KEY_MATRIX,a0 + 0x1030, 0x1000, // move.b (a0,d1.w),d0 + 0x4E75 // rts +}; + +// _JOYGET ($FF8186): port d1's byte, or 0 for a port past the second. +static const uint16_t kRomJoyget[] = { + 0x4280, // clr.l d0 + 0xB27C, X68K_JOY_PORT_COUNT, // cmp.w #PORT_COUNT,d1 + 0x640E, // bcc.s (return) + 0x3001, // move.w d1,d0 + 0xD040, // add.w d0,d0 + 0x41F9, X68K_HI16(X68K_PPI_JOY_PORTS), X68K_LO16(X68K_PPI_JOY_PORTS), // lea PPI_JOY_PORTS,a0 + 0x1030, 0x0000, // move.b 0(a0,d0.w),d0 + 0x4E75 // rts +}; + +// _B_KEYSNS ($FF3324) up to the call of its buffer peek, whose displacement +// word follows. Keys the ROM injected come first; with none it peeks the +// type-ahead buffer, and with that empty as well every later branch only tests +// the zero it got back, so the call returns 0 and changes nothing. That is +// what lets the poll skip it while both counts are zero. +static const uint16_t kRomBKeysns[] = { + 0x3039, X68K_HI16(X68K_IOCS_KEY_INJECTED), X68K_LO16(X68K_IOCS_KEY_INJECTED), // move.w KEY_INJECTED,d0 + 0x6638, // bne.s (take the injected key) + 0x6100 // bsr.w peek +}; + +// The peek ($FF604A): an empty type-ahead buffer returns d0 = 0. +static const uint16_t kRomKeyPeek[] = { + 0x4280, // clr.l d0 + 0x4A79, X68K_HI16(X68K_IOCS_KEY_COUNT), X68K_LO16(X68K_IOCS_KEY_COUNT), // tst.w KEY_COUNT + 0x671C // beq.s (return) +}; + +// Every _MS_* call opens alike: save d0-a6, point a4 at the mouse block and +// park the caller's d1/d2/a1 there. Only the calls that take arguments read +// those slots back, inside the same call, so skipping a trap loses nothing. +static const uint16_t kRomMouseOpen[] = { + 0x48E7, 0xFFFE, // movem.l d0-a6,-(sp) + 0x49F9, X68K_HI16(X68K_IOCS_MOUSE_WORK), X68K_LO16(X68K_IOCS_MOUSE_WORK), // lea MOUSE_WORK,a4 + 0x2941, 0x006A, // move.l d1,106(a4) + 0x2942, 0x006E, // move.l d2,110(a4) + 0x2949, 0x0072 // move.l a1,114(a4) +}; + +// _MS_CURGT ($FFA08C) after the opening: the position long, through the saved d0. +static const uint16_t kRomMsCurgtTail[] = { + 0x2EAC, X68K_MOUSE_POS, // move.l POS(a4),(sp) + 0x4CDF, 0x7FFF, // movem.l (sp)+,d0-a6 + 0x4E75 // rts +}; + +// _MS_GETDT ($FFA04C) after the opening: motion in the high word, then the +// low bytes of the two button words as (right << 8) | left. +static const uint16_t kRomMsGetdtTail[] = { + 0x302C, X68K_MOUSE_DX_WORD, // move.w DX(a4),d0 + 0xE148, // lsl.w #8,d0 + 0x322C, X68K_MOUSE_DY_WORD, // move.w DY(a4),d1 + 0xC27C, 0x00FF, // and.w #$FF,d1 + 0x8041, // or.w d1,d0 + 0x4840, // swap d0 + 0x322C, X68K_MOUSE_RIGHT_WORD, // move.w RIGHT(a4),d1 + 0xE149, // lsl.w #8,d1 + 0x302C, X68K_MOUSE_LEFT_WORD, // move.w LEFT(a4),d0 + 0xC07C, 0x00FF, // and.w #$FF,d0 + 0x8041, // or.w d1,d0 + 0x2E80, // move.l d0,(sp) + 0x4CDF, 0x7FFF, // movem.l (sp)+,d0-a6 + 0x4E75 // rts +}; + + +// jlKeyE -> X68000 scancode. Values taken from XEiJ's keyboard table +// (Keyboard.java), which is the authoritative mapping, not guessed. +// 0 means "this key has no equivalent on an X68000 keyboard". +// +// Notes on the keys that do not map one-to-one: +// - The X68000 has a SINGLE shift key (0x70), so KEY_LSHIFT and KEY_RSHIFT +// both read it. A game testing either sees the same physical key. +// - There is no ALT; OPT.1 (0x72) is the closest equivalent and is what +// KEY_LALT reads. OPT.2 (0x73) is left unmapped. +static const uint8_t kScanForKey[KEY_COUNT] = { + [KEY_NONE] = 0x00, + [KEY_A] = 0x1e, [KEY_B] = 0x2e, [KEY_C] = 0x2c, [KEY_D] = 0x20, + [KEY_E] = 0x13, [KEY_F] = 0x21, [KEY_G] = 0x22, [KEY_H] = 0x23, + [KEY_I] = 0x18, [KEY_J] = 0x24, [KEY_K] = 0x25, [KEY_L] = 0x26, + [KEY_M] = 0x30, [KEY_N] = 0x2f, [KEY_O] = 0x19, [KEY_P] = 0x1a, + [KEY_Q] = 0x11, [KEY_R] = 0x14, [KEY_S] = 0x1f, [KEY_T] = 0x15, + [KEY_U] = 0x17, [KEY_V] = 0x2d, [KEY_W] = 0x12, [KEY_X] = 0x2b, + [KEY_Y] = 0x16, [KEY_Z] = 0x2a, + [KEY_0] = 0x0b, [KEY_1] = 0x02, [KEY_2] = 0x03, [KEY_3] = 0x04, + [KEY_4] = 0x05, [KEY_5] = 0x06, [KEY_6] = 0x07, [KEY_7] = 0x08, + [KEY_8] = 0x09, [KEY_9] = 0x0a, + [KEY_SPACE] = 0x35, + [KEY_ESCAPE] = 0x01, + [KEY_RETURN] = 0x1d, + [KEY_TAB] = 0x10, + [KEY_BACKSPACE] = 0x0f, + [KEY_UP] = 0x3c, [KEY_DOWN] = 0x3e, + [KEY_LEFT] = 0x3b, [KEY_RIGHT] = 0x3d, + [KEY_LSHIFT] = 0x70, [KEY_RSHIFT] = 0x70, // one physical SHIFT + [KEY_LCTRL] = 0x71, + [KEY_LALT] = 0x72, // OPT.1 + [KEY_F1] = 0x63, [KEY_F2] = 0x64, [KEY_F3] = 0x65, [KEY_F4] = 0x66, + [KEY_F5] = 0x67, [KEY_F6] = 0x68, [KEY_F7] = 0x69, [KEY_F8] = 0x6a, + [KEY_F9] = 0x6b, [KEY_F10] = 0x6c, +}; + + +// True when every IOCS call the poll stands in for still lands on the ROM +// code quoted above. The peek is found through _B_KEYSNS's own bsr.w, whose +// displacement is relative to the displacement word itself. +static bool inputDirectUsable(void) { + uint32_t keysns; + uint32_t peek; + + keysns = iocsHandler(X68K_IOCS_B_KEYSNS); + if (!romMatches(keysns, kRomBKeysns, sizeof(kRomBKeysns))) { + return false; + } + peek = keysns + sizeof(kRomBKeysns); + peek += (uint32_t)(int32_t)*(const volatile int16_t *)peek; + if (!romMatches(peek, kRomKeyPeek, sizeof(kRomKeyPeek))) { + return false; + } + if (!romMatches(iocsHandler(X68K_IOCS_BITSNS), kRomBitsns, sizeof(kRomBitsns))) { + return false; + } + if (!romMouseCallMatches(X68K_IOCS_MS_CURGT, kRomMsCurgtTail, sizeof(kRomMsCurgtTail))) { + return false; + } + return romMouseCallMatches(X68K_IOCS_MS_GETDT, kRomMsGetdtTail, sizeof(kRomMsGetdtTail)); +} + + +// The handler the IOCS table currently names for a call. +static uint32_t iocsHandler(uint8_t call) { + return ((const volatile uint32_t *)x68kLowMem(X68K_IOCS_TABLE))[call]; +} + + +// Both ports' bytes as one word, stick 1's high: with nothing moved, the +// poll's whole joystick cost is two port reads and one compare. +static uint16_t joyPortsRead(void) { + const volatile uint8_t *ports; + + if (!gJoyDirect) { + return (uint16_t)(((uint16_t)(uint8_t)_iocs_joyget(JOYSTICK_0) << X68K_JOY_PORT_BITS) | (uint8_t)_iocs_joyget(JOYSTICK_1)); + } + ports = (const volatile uint8_t *)X68K_PPI_JOY_PORTS; + return (uint16_t)(((uint16_t)ports[JOYSTICK_0 * X68K_PPI_PORT_STRIDE] << X68K_JOY_PORT_BITS) | ports[JOYSTICK_1 * X68K_PPI_PORT_STRIDE]); +} + + +// Decode one port's byte into its stick's state; only this writes gJoy*. The +// sticks are digital, so each axis is a pair of lines and snaps to an extreme. +// With both lines of a pair held the positive direction wins, as it does on +// the Amiga and ST ports. +static void joyUpdate(jlJoystickE stick, uint8_t bits) { + static const int8_t kAxisForPair[X68K_JOY_PAIR_MASK + 1u] = { 0, JOYSTICK_AXIS_MIN, JOYSTICK_AXIS_MAX, JOYSTICK_AXIS_MAX }; + uint8_t held; + + held = (uint8_t)~bits; + gJoyAxisX[stick] = kAxisForPair[(held >> X68K_JOY_X_SHIFT) & X68K_JOY_PAIR_MASK]; + gJoyAxisY[stick] = kAxisForPair[(held >> X68K_JOY_Y_SHIFT) & X68K_JOY_PAIR_MASK]; + gJoyButtonState[stick][JOY_BUTTON_0] = (uint8_t)((held & X68K_JOY_TRIGGER_A) != 0u); + gJoyButtonState[stick][JOY_BUTTON_1] = (uint8_t)((held & X68K_JOY_TRIGGER_B) != 0u); +} + + +// Invert kScanForKey into per-group key lists, so a poll touches only the keys +// of a group whose byte changed. kScanForKey stays the one source of truth; +// a scancode past the last polled group is never read, as before. +static void keyGroupsBuild(void) { + uint16_t group; + uint16_t key; + uint8_t scan; + uint8_t slot; + + slot = 0u; + for (group = 0u; group < X68K_KEYGROUP_COUNT; group++) { + gKeyGroupFirst[group] = slot; + for (key = KEY_NONE + 1u; key < KEY_COUNT; key++) { + scan = kScanForKey[key]; + if (scan != 0u && (uint16_t)(scan >> X68K_KEYGROUP_SHIFT) == group) { + gKeyGroupKey[slot] = (uint8_t)key; + gKeyGroupMask[slot] = (uint8_t)(1u << (scan & X68K_KEYGROUP_BIT_MASK)); + slot++; + } + } + } + gKeyGroupFirst[X68K_KEYGROUP_COUNT] = slot; +} + + +// Rewrite the keys of every group whose byte differs from the last poll's. +// Both states come from the bitmap, so a released key drops back to 0 (the +// old set-only write latched keys down forever, hanging any second +// jlWaitForAnyKey); a group whose byte did not change already matches +// gKeyState, which is what makes skipping it exact. Nothing else writes +// gKeyState. With no key moving, a poll is fifteen byte compares instead of a +// shift and mask for every mapped key. +static void keyGroupsUpdate(const volatile uint8_t *matrix) { + uint16_t group; + uint16_t slot; + uint16_t end; + uint8_t bits; + + for (group = 0u; group < X68K_KEYGROUP_COUNT; group++) { + bits = matrix[group]; + if (bits == gKeyGroupBits[group]) { + continue; + } + gKeyGroupBits[group] = bits; + end = gKeyGroupFirst[group + 1u]; + for (slot = gKeyGroupFirst[group]; slot < end; slot++) { + gKeyState[gKeyGroupKey[slot]] = (uint8_t)((bits & gKeyGroupMask[slot]) != 0u); + } + } +} + + +// Whether _B_KEYSNS could return a key: the ROM's injected keys or the +// type-ahead buffer. With both counts zero it returns 0 at once (kRomBKeysns). +static bool keysQueued(void) { + return *(const volatile uint16_t *)x68kLowMem(X68K_IOCS_KEY_COUNT) != 0u || *(const volatile uint16_t *)x68kLowMem(X68K_IOCS_KEY_INJECTED) != 0u; +} + + +// Whether the words at addr are exactly `words`. A handler at an odd address +// is not 68000 code, and reading a word there would be an address error. +static bool romMatches(uint32_t addr, const uint16_t *words, uint16_t bytes) { + const volatile uint16_t *code; + uint16_t i; + + if ((addr & 1u) != 0u) { + return false; + } + code = (const volatile uint16_t *)addr; + for (i = 0u; i < bytes / sizeof(uint16_t); i++) { + if (code[i] != words[i]) { + return false; + } + } + return true; +} + + +// A mouse call is its shared opening followed by its own tail. +static bool romMouseCallMatches(uint8_t call, const uint16_t *tail, uint16_t tailBytes) { + uint32_t handler; + + handler = iocsHandler(call); + if (!romMatches(handler, kRomMouseOpen, sizeof(kRomMouseOpen))) { + return false; + } + return romMatches(handler + sizeof(kRomMouseOpen), tail, tailBytes); +} + + +// A pointer to a fixed low-memory address. gcc takes a constant pointer this +// close to zero for a null dereference (-Werror=array-bounds); an empty asm +// hides the constant from it without the memory load per use that laundering +// through a static volatile costs, which matters on the poll's path. +static inline const volatile void *x68kLowMem(uint32_t addr) { + const volatile void *p; + + __asm__ ("" : "=a" (p) : "0" (addr)); + return p; +} + + void jlpInputInit(void) { uint16_t drain; @@ -947,6 +1544,25 @@ void jlpInputInit(void) { gMouseAttached = true; gMouseX = SURFACE_WIDTH / 2; gMouseY = SURFACE_HEIGHT / 2; + + // Key state: the group lists, then every key up and every group byte 0, + // which agree, so the first poll rewrites exactly the groups that have a + // key held. Core never clears gKeyState, and a shutdown/re-init cycle + // must not leave a key reading down that the delta poll would not revisit. + keyGroupsBuild(); + memset(gKeyGroupBits, 0, sizeof(gKeyGroupBits)); + memset(gKeyState, 0, sizeof(gKeyState)); + gInputDirect = inputDirectUsable(); + + // Joysticks, on the same terms: every stick released and the cached ports + // the released ones, which agree. Both ports are always wired -- an empty + // port reads released -- so both report connected, as on the ST. + gJoyPorts = X68K_JOY_BOTH_RELEASED; + memset(gJoyAxisX, 0, sizeof(gJoyAxisX)); + memset(gJoyAxisY, 0, sizeof(gJoyAxisY)); + memset(gJoyButtonState, 0, sizeof(gJoyButtonState)); + memset(gJoyConnected, true, sizeof(gJoyConnected)); + gJoyDirect = romMatches(iocsHandler(X68K_IOCS_JOYGET), kRomJoyget, sizeof(kRomJoyget)); } @@ -958,106 +1574,85 @@ void jlpInputShutdown(void) { } -// jlKeyE -> X68000 scancode. Values taken from XEiJ's keyboard table -// (Keyboard.java), which is the authoritative mapping, not guessed. -// 0 means "this key has no equivalent on an X68000 keyboard". -// -// Notes on the keys that do not map one-to-one: -// - The X68000 has a SINGLE shift key (0x70), so KEY_LSHIFT and KEY_RSHIFT -// both read it. A game testing either sees the same physical key. -// - There is no ALT; OPT.1 (0x72) is the closest equivalent and is what -// KEY_LALT reads. OPT.2 (0x73) is left unmapped. -static const uint8_t kScanForKey[KEY_COUNT] = { - [KEY_NONE] = 0x00, - [KEY_A] = 0x1e, [KEY_B] = 0x2e, [KEY_C] = 0x2c, [KEY_D] = 0x20, - [KEY_E] = 0x13, [KEY_F] = 0x21, [KEY_G] = 0x22, [KEY_H] = 0x23, - [KEY_I] = 0x18, [KEY_J] = 0x24, [KEY_K] = 0x25, [KEY_L] = 0x26, - [KEY_M] = 0x30, [KEY_N] = 0x2f, [KEY_O] = 0x19, [KEY_P] = 0x1a, - [KEY_Q] = 0x11, [KEY_R] = 0x14, [KEY_S] = 0x1f, [KEY_T] = 0x15, - [KEY_U] = 0x17, [KEY_V] = 0x2d, [KEY_W] = 0x12, [KEY_X] = 0x2b, - [KEY_Y] = 0x16, [KEY_Z] = 0x2a, - [KEY_0] = 0x0b, [KEY_1] = 0x02, [KEY_2] = 0x03, [KEY_3] = 0x04, - [KEY_4] = 0x05, [KEY_5] = 0x06, [KEY_6] = 0x07, [KEY_7] = 0x08, - [KEY_8] = 0x09, [KEY_9] = 0x0a, - [KEY_SPACE] = 0x35, - [KEY_ESCAPE] = 0x01, - [KEY_RETURN] = 0x1d, - [KEY_TAB] = 0x10, - [KEY_BACKSPACE] = 0x0f, - [KEY_UP] = 0x3c, [KEY_DOWN] = 0x3e, - [KEY_LEFT] = 0x3b, [KEY_RIGHT] = 0x3d, - [KEY_LSHIFT] = 0x70, [KEY_RSHIFT] = 0x70, // one physical SHIFT - [KEY_LCTRL] = 0x71, - [KEY_LALT] = 0x72, // OPT.1 - [KEY_F1] = 0x63, [KEY_F2] = 0x64, [KEY_F3] = 0x65, [KEY_F4] = 0x66, - [KEY_F5] = 0x67, [KEY_F6] = 0x68, [KEY_F7] = 0x69, [KEY_F8] = 0x6a, - [KEY_F9] = 0x6b, [KEY_F10] = 0x6c, -}; - - -// _iocs_bitsns(group) returns a bitmap of the eight keys in that group, bit n -// set meaning "down": group = scancode >> 3, bit = scancode & 7. Every group -// is fetched once per poll rather than per key, so the whole keyboard costs -// X68K_KEYGROUP_COUNT IOCS calls regardless of how many keys are tested. -// -// No ISR, no vector takeover, no packet decoding -- unlike the ST, which has -// to replace the TOS ikbdsys vector and run an IKBD packet state machine. +// Keys: the fifteen group bytes, from the key matrix itself or from +// X68K_KEYGROUP_COUNT _BITSNS traps, then only the changed groups' keys. +// Typed characters: the IOCS type-ahead drain, skipped while both counts are +// zero on the direct path. Mouse: the cursor tracker's position and the two +// button bytes, from the mouse block or through _MS_CURGT / _MS_GETDT. +// Joysticks: each port's byte, from the PPI or through _JOYGET. void jlpInputPoll(void) { - uint8_t groups[X68K_KEYGROUP_COUNT]; - uint16_t group; - uint16_t key; - uint16_t drain; - uint8_t scan; - uint32_t mousePos; - uint32_t mouseButtons; + uint8_t groups[X68K_KEYGROUP_COUNT]; + const volatile uint8_t *matrix; + const volatile uint8_t *mouse; + uint16_t group; + uint16_t drain; + uint16_t joy; + uint32_t mousePos; + uint32_t mouseButtons; + bool leftDown; + bool rightDown; - for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { - groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); - } - // Write both states each poll: the bitmap is live key-DOWN data, - // so a released key must drop back to 0 here (set-only left keys - // latched down forever, hanging any second jlWaitForAnyKey). - for (key = 1; key < KEY_COUNT; key++) { - scan = kScanForKey[key]; - if (scan == 0u) { - continue; - } - group = (uint16_t)(scan >> 3); - if (group < X68K_KEYGROUP_COUNT) { - gKeyState[key] = (uint8_t)((groups[group] >> (scan & 7u)) & 1u); + if (gInputDirect) { + matrix = (const volatile uint8_t *)x68kLowMem(X68K_IOCS_KEY_MATRIX); + } else { + for (group = 0u; group < X68K_KEYGROUP_COUNT; group++) { + groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); } + matrix = groups; } + keyGroupsUpdate(matrix); // Typed-character path: the IOCS keyboard interrupt queues // translated characters (shift/caps/layout applied) independently // of the bitmap above; _B_KEYINP pops (scancode << 8) | char. // Non-character keys carry 0 in the low byte and are dropped by - // jlInputCharPush's filter. - for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) { - if (_iocs_b_keysns() == 0) { - break; + // jlInputCharPush's filter. The direct path skips the traps while + // nothing is queued; anything queued still goes through them, unchanged. + if (!gInputDirect || keysQueued()) { + for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) { + if (_iocs_b_keysns() == 0) { + break; + } + jlInputCharPush((uint8_t)(_iocs_b_keyinp() & 0xFFu)); } - jlInputCharPush((uint8_t)(_iocs_b_keyinp() & 0xFFu)); } // Mouse position: the IOCS cursor tracker already integrated and // clamped every SCC channel B packet into the _MS_LIMIT box set at - // init (stage coordinates), so _MS_CURGT's packed (x << 16) | y IS - // the pointer position -- no delta math and no second clamp here. - // Buttons ride _MS_GETDT's low word, which holds live levels: - // right in bits 15-8, left in bits 7-0, 0xFF held / 0x00 up. That - // byte order is the one the real IPL 1.0 ROM produces (verified - // under MAME by verify-x68000-mouse.sh); published summaries that - // list left first are wrong. Neither trap blocks on the 4800-baud - // wire -- both just read the IOCS work area. - mousePos = (uint32_t)_iocs_ms_curgt(); - gMouseX = (int16_t)((mousePos >> 16) & 0xFFFFu); - gMouseY = (int16_t)(mousePos & 0xFFFFu); + // init (stage coordinates), so its packed (x << 16) | y IS the + // pointer position -- no delta math and no second clamp here. + // Buttons are live levels, right then left: that byte order is the + // one the real IPL 1.0 ROM produces (verified under MAME by + // verify-x68000-mouse.sh); published summaries that list left first + // are wrong. Neither read touches the 4800-baud wire or the SCC -- + // the receive interrupt fills the mouse block, and it writes each + // word whole, so no read here can see half an update. + if (gInputDirect) { + mouse = (const volatile uint8_t *)x68kLowMem(X68K_IOCS_MOUSE_WORK); + mousePos = *(const volatile uint32_t *)(mouse + X68K_MOUSE_POS); + rightDown = mouse[X68K_MOUSE_RIGHT_WORD + X68K_MOUSE_LOW_BYTE] != 0u; + leftDown = mouse[X68K_MOUSE_LEFT_WORD + X68K_MOUSE_LOW_BYTE] != 0u; + } else { + mousePos = (uint32_t)_iocs_ms_curgt(); + mouseButtons = (uint32_t)_iocs_ms_getdt(); + rightDown = ((mouseButtons >> 8) & 0xFFu) != 0u; + leftDown = (mouseButtons & 0xFFu) != 0u; + } + gMouseX = (int16_t)((mousePos >> 16) & 0xFFFFu); + gMouseY = (int16_t)(mousePos & 0xFFFFu); - mouseButtons = (uint32_t)_iocs_ms_getdt(); - gMouseButtonState[MOUSE_BUTTON_LEFT] = (mouseButtons & 0xFFu) != 0u; - gMouseButtonState[MOUSE_BUTTON_RIGHT] = ((mouseButtons >> 8) & 0xFFu) != 0u; + gMouseButtonState[MOUSE_BUTTON_LEFT] = leftDown; + gMouseButtonState[MOUSE_BUTTON_RIGHT] = rightDown; gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; + + // The ports are live levels: nothing latches, so a released line reads + // released on the very next poll. + joy = joyPortsRead(); + if (joy != gJoyPorts) { + gJoyPorts = joy; + joyUpdate(JOYSTICK_0, (uint8_t)(joy >> X68K_JOY_PORT_BITS)); + joyUpdate(JOYSTICK_1, (uint8_t)joy); + } } @@ -1068,103 +1663,27 @@ void jlpJoystickReset(jlJoystickE js) { // ----- Timing --------------------------------------------------------------- - -// There is no plain "wait for vblank" IOCS call -- _iocs_vdispst INSTALLS a -// vertical-display handler rather than blocking -- so poll the MFP's V-DISP -// input directly and edge-detect it. Both spins are bounded so a wedged or -// mis-programmed CRTC degrades to a dropped frame instead of hanging the game. // -// ASSUMES SUPERVISOR MODE for the $E88001 read. Human68k normally leaves user -// programs in supervisor, unlike TOS on the ST (which needs Super(0L) -- see -// the ST HAL). If that turns out not to hold, this becomes an _iocs_vdispst -// handler bumping gFrameCount instead. -// Vertical-display interrupt handler: the AUTHORITATIVE frame counter. -// -// A polled edge-detector loses ticks, and it loses them exactly when it hurts: -// any frame doing real work between jlFrameCount() calls misses the V-DISP -// transitions that happened meanwhile. Measured: 400 audio refills across ~19 -// frames reported ZERO elapsed frames, because nothing polled in between. -// Animation and music tempo would silently run slow under load. -// -// Registered with _iocs_vdispst, which calls this once per vertical display -// period. Plain C (RTS) is the IOCS convention for a _VDISPST handler; IOCS -// owns the interrupt frame and the RTE. -static void vdispHandler(void) { - gFrameCount++; -} -// Referenced only to keep the handler compiled and honest about its intent -// until the asm thunk exists; see the note in jlpInit. -void *const gX68kVdispHandlerRef = (void *)vdispHandler; +// Both clocks are described under "Clocks" near the top of this file. -// V-DISP edge detect. This is the PRIMARY frame clock (the interrupt route -// hangs -- see jlpInit), so it is called from every per-frame entry point the -// library owns -- jlpPresent, jlpInputPoll, jlpFrameCount, jlpWaitVBL -- not -// just when the app asks the time. A game that presents or polls input once a -// frame therefore keeps an accurate count even while doing heavy work. -// -// The residual limitation is real and worth knowing: a frame that does NONE of -// those for longer than one V-DISP period still loses ticks. An asm thunk for -// _iocs_vdispst removes it for good. +// Frames since init: V-DISP edges, counted by x68kVdispIsr.s, or by +// vdispPoll when _VDISPST refused the handler. // // CRITICAL CONTRACT: jlpFrameCount must be monotonic ON ITS OWN, without // jlpWaitVBL being called. UBER's timing model (and any game that paces by // polling rather than blocking) sits in a tight loop reading jlFrameCount and // never calls jlWaitVBL -- so a counter bumped only inside the wait never -// advances and the caller spins forever. That is exactly what happened here: -// UBER ran for 38,000 frames with a live, moving PC and a frozen screen, -// because it was stuck on its first timed op waiting for a tick that could -// not arrive. -static void vdispPoll(void) { - uint8_t now; - - if (gVdispInstalled) { - return; // the interrupt owns the counter - } - now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); - if (now != gLastVdisp) { - gLastVdisp = now; - if (now != 0u) { - gFrameCount++; - } - } -} - - -void jlpWaitVBL(void) { - uint16_t start; - uint32_t guard; - - // Block until the counter actually moves, so the wait and the counter can - // never disagree. Bounded, so a wedged CRTC drops a frame rather than - // hanging the game. - start = gFrameCount; - guard = 0ul; - while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) { - vdispPoll(); - guard++; - } -} - - -// Frames since init, derived from the honest millisecond tick rather than from -// counting V-DISP edges. -// -// The edge counter (vdispPoll) can only see a transition while the CPU happens -// to be inside a JoeyLib call, so any op that spans more than one frame loses -// the edges in between. That undercount is what made UBER's "16 frame" windows -// run for 88 seconds and what made every op report the same throughput. The -// tick is interrupt-driven and cannot miss, so time -> frames is monotonic and -// correct regardless of what the caller is doing. -// -// vdispPoll still runs: jlpWaitVBL needs the real V-DISP edge to synchronise -// to, and gFrameCount remains the fallback when the tick could not be hooked. +// advances and the caller spins forever. That is exactly what happened here +// once: UBER ran for 38,000 frames with a live, moving PC and a frozen screen. uint16_t jlpFrameCount(void) { vdispPoll(); - if (!gTickHooked) { - return gFrameCount; - } - return (uint16_t)((jlpMillisElapsed() * (uint32_t)jlpFrameHz()) / 1000ul); + return gX68kFrames; +} + + +uint16_t jlpFrameHz(void) { + return gFrameHz; } @@ -1194,31 +1713,26 @@ void jlpLogTee(const char *text) { #endif -// Milliseconds since the first call. Latches its base explicitly rather than -// treating 0 as a sentinel, because 0 is a legal tick value at power-on and at -// every wrap. Falls back to the frame-derived generic clock if the tick could -// not be hooked, which is wrong in the ways described above but better than -// returning a constant. +// Milliseconds since jlInit on the Timer C clock. Without it, the generic +// clock, derived from the frame count and jlpFrameHz. uint32_t jlpMillisElapsed(void) { - uint32_t ticks; - if (!gTickHooked) { return jlpGenericMillisElapsed(); } - ticks = gX68kTicks; - if (!gTickBaseSet) { - gTickBase = ticks; - gTickBaseSet = true; + return tickMillisNow() - gMillisBase; +} + + +// Block until the frame count moves: the next V-DISP edge, the very edge the +// count steps on, so the wait and the count can never disagree. Bounded, so a +// wedged CRTC drops a frame rather than hanging the game. ASSUMES SUPERVISOR +// MODE (jlpInit's _dos_super) for vdispPoll's MFP read. +void jlpWaitVBL(void) { + uint16_t start; + uint32_t guard; + + start = gX68kFrames; + for (guard = 0ul; gX68kFrames == start && guard < X68K_VBL_SPIN_LIMIT; guard++) { + vdispPoll(); } - // Scale by the measured rate. Multiply first (ticks fit comfortably in 32 - // bits for any realistic session) so the divide does not quantize away the - // sub-tick remainder. - return (uint32_t)(((ticks - gTickBase) * 1000ul) / gTickHz); -} - - -uint16_t jlpFrameHz(void) { - // 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported - // only -- the generic millisElapsed divides by it. - return 55u; } diff --git a/src/x68000/halPlanar.c b/src/x68000/halPlanar.c index 8838d0c..71e0da6 100644 --- a/src/x68000/halPlanar.c +++ b/src/x68000/halPlanar.c @@ -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); } diff --git a/src/x68000/x68kRasterIsr.s b/src/x68000/x68kRasterIsr.s index 3d58dbc..a75812f 100644 --- a/src/x68000/x68kRasterIsr.s +++ b/src/x68000/x68kRasterIsr.s @@ -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 diff --git a/src/x68000/x68kTickIsr.s b/src/x68000/x68kTickIsr.s index 57ca489..08b9299 100644 --- a/src/x68000/x68kTickIsr.s +++ b/src/x68000/x68kTickIsr.s @@ -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 + pea x68kTickReturn | the frame the dispatcher's RTE uses + move.w #SR_RETURN,%sp@- + .word 0x4EF9 | jmp 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 diff --git a/src/x68000/x68kVdispIsr.s b/src/x68000/x68kVdispIsr.s new file mode 100644 index 0000000..c2ebc1f --- /dev/null +++ b/src/x68000/x68kVdispIsr.s @@ -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