diff --git a/cfg/default.cfg b/cfg/default.cfg new file mode 100644 index 0000000..02f0986 --- /dev/null +++ b/cfg/default.cfg @@ -0,0 +1,5 @@ + + + + + diff --git a/cfg/x68000.cfg b/cfg/x68000.cfg new file mode 100644 index 0000000..4d58885 --- /dev/null +++ b/cfg/x68000.cfg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/keys/keys.c b/examples/keys/keys.c index fc059dd..b07047f 100644 --- a/examples/keys/keys.c +++ b/examples/keys/keys.c @@ -7,10 +7,12 @@ // whatever you type -- letters, digits, shifted punctuation -- appears // there, Backspace deletes, Return clears. It doubles as the // acceptance test for the typed-character queue ("192.168.1.10:6510" -// must be typeable on every port). The bottom scanline additionally -// encodes every received character as raw pixel nibbles (2 pixels = -// 1 byte) behind an A5A5 sentinel so emulator harnesses can verify -// the exact bytes from a framebuffer dump. +// must be typeable on every port). The bottom two scanlines +// additionally encode verification data as raw pixel nibbles behind +// sentinels: row 198 carries every received character (A5A5 sentinel, +// 2 pixels = 1 byte) and row 199 the live mouse state (5A5A sentinel, +// present/button flags plus position), so emulator harnesses can +// verify the exact bytes from a framebuffer dump. // // The render loop only redraws cells whose target lit state changed // since last frame -- so on idle frames, the only work is the cursor @@ -58,26 +60,52 @@ // nibble) pixel pairs. An emulator harness reads this row back from // the framebuffer to prove each typed byte arrived. #define VERIFY_Y 198 -#define VERIFY_SENTINEL_0 0xA -#define VERIFY_SENTINEL_1 0x5 +#define VERIFY_SENTINEL 0xA5A5 #define VERIFY_COUNT_X 4 #define VERIFY_CHARS_X 8 #define MAX_RECEIVED 150 +// Every field on both verification rows is stamped high nibble first, +// one nibble per pixel: a byte is two pixels wide, a 16-bit value four. +#define VERIFY_BYTE_NIBBLES 2 +#define VERIFY_WORD_NIBBLES 4 + +// Mouse verification scanline (the row below the typed-character one): +// its own 5A5A sentinel, then present/left/right/middle as one nibble +// each, then jlMouseX and jlMouseY as four nibbles each. +// Re-stamped only when the reported state changes. +#define VERIFY_MOUSE_Y 199 +#define VERIFY_MOUSE_SENTINEL 0x5A5A +#define VERIFY_MOUSE_FLAGS_X 4 +#define VERIFY_MOUSE_XPOS_X 8 +#define VERIFY_MOUSE_YPOS_X 12 + +// The flags field packed as one nibble per predicate, in the order the +// row stamps them. This packing is the ONLY definition of that layout: +// it is both what gets drawn and what the change check compares. +#define MOUSE_FLAG_PRESENT 0x1000 +#define MOUSE_FLAG_LEFT 0x0100 +#define MOUSE_FLAG_RIGHT 0x0010 +#define MOUSE_FLAG_MIDDLE 0x0001 + static void buildPalette(jlSurfaceT *screen); static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow); static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow); static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit); static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y); +static void drawMouseVerifyRow(jlSurfaceT *screen); static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *text, uint8_t color); static void drawVerifyRow(jlSurfaceT *screen); static int glyphIdx(char c); static void initialPaint(jlSurfaceT *screen); static void logTypedHistory(void); +static uint16_t mouseFlags(void); static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow); static void processTypedChars(jlSurfaceT *screen); static void redrawTextStrip(jlSurfaceT *screen); +static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles); static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow); +static void updateMouseVerifyRow(jlSurfaceT *screen); // Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly // resembles a real keyboard (top number row, then QWERTY rows, then a @@ -97,6 +125,12 @@ static int16_t gLastCursorY = -100; static int16_t gLastCursorCol = CELL_NONE; static int16_t gLastCursorRow = CELL_NONE; +// Last state stamped into the mouse verification row; updateMouseVerifyRow +// only redraws when this tuple changes, so idle frames stay draw-free. +static int16_t gLastMouseRowX = -1; +static int16_t gLastMouseRowY = -1; +static uint16_t gLastMouseRowFlags = 0xFFFF; + // Typed-text line state plus the full received-character history for // the verification row. static char gLine[MAX_LINE + 1]; @@ -258,8 +292,33 @@ static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit) { } +// The block is clipped against the verification scanlines rather than +// drawn over them: emulator harnesses sample those rows between +// frames, so even a stamp-then-repair within one loop iteration is a +// visible corruption window to them. static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) { - jlFillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR); + int16_t h; + + h = CURSOR_H; + if ((int16_t)(y + h) > VERIFY_Y) { + h = (int16_t)(VERIFY_Y - y); + } + if (h <= 0) { + return; + } + jlFillRect(screen, x, y, CURSOR_W, h, COLOR_CURSOR); +} + + +// Stamp the mouse verification scanline: sentinel, present/button +// flags, then the pointer position as nibble pixels. An emulator +// harness reads this row back from the framebuffer to prove mouse +// motion and button state crossed the HAL (see verify-x68000-mouse.sh). +static void drawMouseVerifyRow(jlSurfaceT *screen) { + stampNibbles(screen, 0, VERIFY_MOUSE_Y, VERIFY_MOUSE_SENTINEL, VERIFY_WORD_NIBBLES); + stampNibbles(screen, VERIFY_MOUSE_FLAGS_X, VERIFY_MOUSE_Y, mouseFlags(), VERIFY_WORD_NIBBLES); + stampNibbles(screen, VERIFY_MOUSE_XPOS_X, VERIFY_MOUSE_Y, (uint16_t)jlMouseX(), VERIFY_WORD_NIBBLES); + stampNibbles(screen, VERIFY_MOUSE_YPOS_X, VERIFY_MOUSE_Y, (uint16_t)jlMouseY(), VERIFY_WORD_NIBBLES); } @@ -296,19 +355,15 @@ static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *t // to redraw whole. static void drawVerifyRow(jlSurfaceT *screen) { int16_t i; - uint8_t ch; - jlDrawPixel(screen, 0, VERIFY_Y, VERIFY_SENTINEL_0); - jlDrawPixel(screen, 1, VERIFY_Y, VERIFY_SENTINEL_1); - jlDrawPixel(screen, 2, VERIFY_Y, VERIFY_SENTINEL_0); - jlDrawPixel(screen, 3, VERIFY_Y, VERIFY_SENTINEL_1); - jlDrawPixel(screen, VERIFY_COUNT_X, VERIFY_Y, (uint8_t)((gReceivedCount >> 4) & 0x0F)); - jlDrawPixel(screen, VERIFY_COUNT_X + 1, VERIFY_Y, (uint8_t)(gReceivedCount & 0x0F)); + stampNibbles(screen, 0, VERIFY_Y, VERIFY_SENTINEL, VERIFY_WORD_NIBBLES); + stampNibbles(screen, VERIFY_COUNT_X, VERIFY_Y, (uint16_t)gReceivedCount, VERIFY_BYTE_NIBBLES); for (i = 0; i < gReceivedCount; i++) { - ch = gReceived[i]; - jlDrawPixel(screen, (int16_t)(VERIFY_CHARS_X + 2 * i), VERIFY_Y, (uint8_t)((ch >> 4) & 0x0F)); - jlDrawPixel(screen, (int16_t)(VERIFY_CHARS_X + 2 * i + 1), VERIFY_Y, (uint8_t)(ch & 0x0F)); + stampNibbles(screen, (int16_t)(VERIFY_CHARS_X + VERIFY_BYTE_NIBBLES * i), VERIFY_Y, gReceived[i], VERIFY_BYTE_NIBBLES); } + // The strip repaint that called us blanked the mouse row too, so + // restore it in the same pass. + drawMouseVerifyRow(screen); } @@ -376,6 +431,17 @@ static void logTypedHistory(void) { } +// The mouse predicates packed one nibble per flag, in the order +// drawMouseVerifyRow stamps them. Sampled once per use so the drawn +// row and updateMouseVerifyRow's change check can never disagree. +static uint16_t mouseFlags(void) { + return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) | + (jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) | + (jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) | + (jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0)); +} + + static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) { int16_t col; int16_t row; @@ -450,6 +516,20 @@ static void redrawTextStrip(jlSurfaceT *screen) { } +// Stamp a value into consecutive pixels as nibbles, high nibble +// first -- the encoding both verification rows and every emulator +// harness that decodes them share. +static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles) { + int16_t i; + int16_t shift; + + for (i = 0; i < nibbles; i++) { + shift = (int16_t)(4 * (nibbles - 1 - i)); + jlDrawPixel(screen, (int16_t)(x + i), y, (uint8_t)((value >> shift) & 0x0F)); + } +} + + // Erase the previous cursor (by redrawing the cell that held it) and // stamp the new cursor at the current mouse position. Both rects are // presented; if the cursor stayed inside the same cell only one rect @@ -487,6 +567,27 @@ static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRo } +// Re-stamp the mouse verification row only when the reported state +// changed since the last stamp, so idle frames stay draw-free. +static void updateMouseVerifyRow(jlSurfaceT *screen) { + int16_t x; + int16_t y; + uint16_t flags; + + x = jlMouseX(); + y = jlMouseY(); + flags = mouseFlags(); + if (x == gLastMouseRowX && y == gLastMouseRowY && flags == gLastMouseRowFlags) { + return; + } + drawMouseVerifyRow(screen); + jlStagePresent(); + gLastMouseRowX = x; + gLastMouseRowY = y; + gLastMouseRowFlags = flags; +} + + int main(void) { jlConfigT config; jlSurfaceT *screen; @@ -521,6 +622,7 @@ int main(void) { cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow); presentChangedCells(screen, cursorCol, cursorRow); updateCursor(screen, cursorCol, cursorRow); + updateMouseVerifyRow(screen); processTypedChars(screen); } diff --git a/examples/sccprobe/sccprobe.c b/examples/sccprobe/sccprobe.c new file mode 100644 index 0000000..4b511ad --- /dev/null +++ b/examples/sccprobe/sccprobe.c @@ -0,0 +1,118 @@ +// Does opening one built-in SCC port survive the OTHER one? +// +// The Zilog 8530 in a IIgs carries both built-in ports: channel A is the +// printer port (where AppleTalk lives), channel B is the modem port. WR9's +// reset command is chip-wide, so an open that issues the force-hardware-reset +// (0xC0) takes the other channel down with it -- registers, FIFOs, baud +// generator and, decisively for this probe, the transmit enable in WR5. +// +// So: bring channel A up through the real HAL, hand it back (the IIgs close +// deliberately leaves the chip configured), then open channel B -- the +// operation under test -- and finally poke a byte STRAIGHT into channel A's +// data register, bypassing the library. If channel A survived, its transmitter +// is still enabled and the byte reaches the host peer. If the open reset the +// whole chip, WR5 went to zero, the transmitter is off, and nothing arrives. +// +// Run headless by scripts/verify-iigs-serial.sh, which reads the bytes off a +// socket and the progress flags out of the SHR framebuffer. + +#include + +// SHR framebuffer, used purely as a mailbox the emulator harness can read: the +// probe never calls jlInit, so nothing else is drawing here. +#define SHR_BASE ((volatile uint8_t *)0x00E12000L) +#define MB_SIGNATURE_0 0 +#define MB_SIGNATURE_1 1 +#define MB_PRINTER_OPEN 2 +#define MB_PRINTER_WROTE 3 +#define MB_MODEM_OPEN 4 +#define MB_DIRECT_WROTE 5 +#define MB_SIG_VALUE_0 0xA5u +#define MB_SIG_VALUE_1 0x5Au + +// Channel A (printer port) registers -- the channel this probe checks for +// survival. Deliberately spelled out here rather than shared with the HAL: the +// point is to look at the hardware independently of the code under test. +#define SCC_A_CTRL ((volatile uint8_t *)0x00C039L) +#define SCC_A_DATA ((volatile uint8_t *)0x00C03BL) +#define SCC_RR0_TX_EMPTY 0x04u + +// Bounded so a dead transmitter reports "nothing arrived" instead of hanging +// the machine and stalling the gate. +#define TX_SPIN_LIMIT 200000ul + +static void mailbox(uint16_t slot, uint8_t value); +static bool pokeChannelA(uint8_t byte); + + +static void mailbox(uint16_t slot, uint8_t value) { + SHR_BASE[slot] = value; +} + + +// Write one byte to channel A without going through the HAL. Returns false if +// the transmitter never reports empty, which is exactly what a wiped channel +// looks like. +static bool pokeChannelA(uint8_t byte) { + uint32_t spin; + + for (spin = 0; spin < TX_SPIN_LIMIT; spin++) { + if ((*SCC_A_CTRL & SCC_RR0_TX_EMPTY) != 0u) { + *SCC_A_DATA = byte; + return true; + } + } + return false; +} + + +int main(void) { + jlSerialConfigT cfg; + uint16_t i; + + mailbox(MB_SIGNATURE_0, MB_SIG_VALUE_0); + mailbox(MB_SIGNATURE_1, MB_SIG_VALUE_1); + + cfg.baud = 9600u; + cfg.dataBits = 8u; + cfg.stopBits = 1u; + cfg.parity = JL_SERIAL_PARITY_NONE; + cfg.flow = JL_SERIAL_FLOW_NONE; + cfg.unit = 0u; + + // 1. Bring channel A (printer) up through the HAL and prove it transmits. + if (!jlSerialOpen(JL_SERIAL_PRINTER, &cfg)) { + return 1; + } + mailbox(MB_PRINTER_OPEN, 1u); + if (jlSerialWrite((const uint8_t *)"A1\r\n", 4u) == 4u) { + mailbox(MB_PRINTER_WROTE, 1u); + } + jlSerialFlush(); + jlSerialClose(); + + // 2. Open channel B (modem). THIS is the operation under test: with the + // old chip-wide 0xC0 reset it also wipes channel A. + if (!jlSerialOpen(JL_SERIAL_MODEM, &cfg)) { + return 1; + } + mailbox(MB_MODEM_OPEN, 1u); + + // 3. Channel A is untouched by the library from here on. Poke it directly: + // the byte only leaves the chip if channel A's transmitter is still on. + if (pokeChannelA('A')) { + mailbox(MB_DIRECT_WROTE, 1u); + } + (void)pokeChannelA('2'); + (void)pokeChannelA('\r'); + (void)pokeChannelA('\n'); + + // Hold the machine still so the harness can read the mailbox, and give the + // last byte time to clock out at 9600 baud. + for (i = 0; i < 60000u; i++) { + (void)*SCC_A_CTRL; + } + jlSerialClose(); + for (;;) { + } +} diff --git a/include/joey/input.h b/include/joey/input.h index 223a5b2..55df6f2 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -144,6 +144,9 @@ bool jlKeyDown(jlKeyE key); bool jlKeyPressed(jlKeyE key); bool jlKeyReleased(jlKeyE key); +// True when the platform confirmed an attached mouse at init (DOS: the INT 33h driver reset; +// Amiga/ST/IIgs/X68000: the OS mouse is always wired). +bool jlMousePresent(void); int16_t jlMouseX(void); int16_t jlMouseY(void); bool jlMouseDown(jlMouseButtonE button); diff --git a/make/iigs.mk b/make/iigs.mk index 481b8ad..a078f76 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -26,7 +26,7 @@ LLVM_MC := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc AR := $(LLVM816_ROOT)/tools/llvm-mos-sdk/bin/llvm-ar CCWRAP := $(LLVM816_ROOT)/scripts/ccRegallocFallback.sh RT_INC := $(LLVM816_ROOT)/runtime/include -CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections +CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections $(EXTRA_CFLAGS) INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen LIBDIR := $(BUILD)/lib LIB := $(LIBDIR)/libjoey.a @@ -58,6 +58,7 @@ DRAW_SRC := $(EXAMPLES)/draw/draw.c KEYS_SRC := $(EXAMPLES)/keys/keys.c SERIAL_SRC := $(EXAMPLES)/serial/serial.c SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c SAVE_SRC := $(EXAMPLES)/save/save.c JOY_SRC := $(EXAMPLES)/joy/joy.c SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c @@ -78,7 +79,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_ASM := $(BUILD)/audio/ntpdata.s IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32 -.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-shrtail clean-iigs clean +.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean # Default: compile-check the library + run the end-to-end smoke test. all iigs: iigs-lib iigs-clang-smoke @@ -125,6 +126,11 @@ iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) iigs-verify: iigs-disk $(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw) +# Cross-channel SCC gate: opening the modem port must leave the printer channel +# (AppleTalk's) transmitting. Builds its own one-example disk; ~2 min. +iigs-verify-serial: $(BINDIR)/SCCPROBE + $(REPO_DIR)/scripts/verify-iigs-serial.sh + # Boot-verify every launchable example on joey.2mg (per-example thresholds). iigs-verify-all: iigs-disk $(REPO_DIR)/scripts/verify-iigs-all.sh @@ -231,6 +237,12 @@ $(BINDIR)/SERTEST: $(SERTEST_SRC) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERTEST.d $(INCLUDES) -o $@ $(SERTEST_SRC) $(LIB) +# Cross-channel SCC check: does opening the modem port leave the printer port +# (AppleTalk's channel) alive? Driven by scripts/verify-iigs-serial.sh. +$(BINDIR)/SCCPROBE: $(SCCPROBE_SRC) $(LIB) $(IIGS_CLANG_BUILD) + @mkdir -p $(dir $@) $(DEP_DIR) + $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SCCPROBE.d $(INCLUDES) -o $@ $(SCCPROBE_SRC) $(LIB) + $(BINDIR)/SAVE: $(SAVE_SRC) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB) diff --git a/make/x68000.mk b/make/x68000.mk index c9cda47..4e6ba26 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -96,7 +96,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c KEYS_SRC := $(EXAMPLES)/keys/keys.c -.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-golden clean-x68000 clean +.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean all x68000: x68000-lib x68000-examples @@ -170,6 +170,11 @@ $(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR) x68000-verify-serial: $(BINDIR)/SERIAL.X $(REPO_DIR)/scripts/verify-x68000-serial.sh +# Mouse gate: injected MAME mouse packets through the IOCS ROM into +# jlMouse* state, read back from KEYS.X's verification scanline. +x68000-verify-mouse: $(BINDIR)/KEYS.X + $(REPO_DIR)/scripts/verify-x68000-mouse.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). x68000-verify-golden: $(LIB) $(LIBXMP_AR) diff --git a/nvram/x68000_0/nvram b/nvram/x68000_0/nvram new file mode 100644 index 0000000..cefd27f Binary files /dev/null and b/nvram/x68000_0/nvram differ diff --git a/patches/mame-0.264-apple2gs-scc-clock.patch b/patches/mame-0.264-apple2gs-scc-clock.patch new file mode 100644 index 0000000..bc6ef88 --- /dev/null +++ b/patches/mame-0.264-apple2gs-scc-clock.patch @@ -0,0 +1,18 @@ +--- /tmp/apple2gs.cpp.orig 2026-08-13 15:22:16.445448309 -0500 ++++ toolchains/cache/mame-mame0264/src/mame/apple/apple2gs.cpp 2026-08-13 15:22:16.497448131 -0500 +@@ -3839,8 +3839,13 @@ + ADDRESS_MAP_BANK(config, A2GS_C300_TAG).set_map(&apple2gs_state::c300bank_map).set_options(ENDIANNESS_LITTLE, 8, 32, 0x100); + + /* serial */ +- SCC85C30(config, m_scc, A2GS_14M / 2); +- m_scc->configure_channels(3'686'400, 3'686'400, 3'686'400, 3'686'400); ++ // RetroNet patch: a real IIgs feeds the SCC BRG a 3.6864 MHz PCLK (the classic 115200 ++ // time-constant base); A2GS_14M/2 = 7.159 MHz made every firmware/native BRG rate ~1.94x ++ // fast. Zeroed rxc/txc: the m_rxc>0 path in z80scc update_serial() otherwise FORCES the ++ // receive rate to rxc/16 = 230400 regardless of the programmed BRG, which no rs232 peer ++ // can match. ++ SCC85C30(config, m_scc, 3'686'400); ++ m_scc->configure_channels(0, 0, 0, 0); + m_scc->out_int_callback().set(FUNC(apple2gs_state::scc_irq_w)); + m_scc->out_txda_callback().set("printer", FUNC(rs232_port_device::write_txd)); + m_scc->out_txdb_callback().set("modem", FUNC(rs232_port_device::write_txd)); diff --git a/scripts/verify-iigs-serial.sh b/scripts/verify-iigs-serial.sh new file mode 100755 index 0000000..2e00b29 --- /dev/null +++ b/scripts/verify-iigs-serial.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# verify-iigs-serial.sh - Cross-channel SCC gate for the Apple IIgs port. +# +# The 8530 carries both built-in ports on one chip: channel A is the printer +# port (AppleTalk's), channel B is the modem port. WR9's reset command is +# chip-wide, so an open that issues the force-hardware-reset (0xC0) wipes the +# other channel too. This gate proves it does not. +# +# SCCPROBE (examples/sccprobe/sccprobe.c) brings channel A up through the real +# HAL and sends "A1", hands it back, opens channel B -- the operation under +# test -- and then pokes "A2" straight into channel A's data register. Both +# strings reach the host only if channel A's transmitter is still enabled after +# the modem-port open, so: +# +# A1 present, A2 present -> PASS (channel A survived) +# A1 present, A2 missing -> FAIL (the open wiped the other channel) +# A1 missing -> the probe never ran / printer path is broken +# +# MAME CONNECTS OUT to the bitbanger socket, so the peer here listens first. +# Needs the PATCHED apple2gs (patches/mame-0.264-apple2gs-scc-clock.patch): +# stock 0.264 feeds the SCC BRG the wrong clock and pins RX at 230400, so no +# baud ever matches the null_modem. +# +# scripts/verify-iigs-serial.sh +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +MAME="$repo/toolchains/cache/mame-mame0264/apple2gs" +sys_disk=$repo/toolchains/emulators/support/gsos-system.po +rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" +PORT="${IIGS_SCC_PORT:-45231}" +readFrame="${MAME_READ_FRAME:-9000}" +WALL="${IIGS_SCC_WALL:-420}" + +[ -x "$MAME" ] || { echo "verify-iigs-serial: missing patched MAME at $MAME" >&2; exit 2; } +[ -f "$sys_disk" ] || { echo "verify-iigs-serial: missing $sys_disk" >&2; exit 2; } +[ -f "$repo/build/iigs/bin/SCCPROBE" ] || { echo "verify-iigs-serial: build SCCPROBE first" >&2; exit 2; } + +work=$(mktemp -d -t joeylib-iigs-scc.XXXXXX) +peer_pid="" +cleanup() { + [ -n "$peer_pid" ] && kill -9 "$peer_pid" 2>/dev/null + rm -rf "$work" +} +trap cleanup EXIT + +# A disk holding just the probe, so this never disturbs the normal joey.2mg. +JOEY_DISK_EXAMPLES="SCCPROBE" bash "$repo/scripts/make-iigs-disk.sh" "$work/scc.2mg" >/dev/null 2>&1 || { + echo "verify-iigs-serial: FAIL - could not build the probe disk" >&2 + exit 1 +} +cp "$sys_disk" "$work/boot.po" + +# Serial peer: listen, then log every byte the IIgs sends. +cat > "$work/peer.py" <<'PY' +import socket, sys, time + +port = int(sys.argv[1]) +out = sys.argv[2] +srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +srv.bind(("127.0.0.1", port)) +srv.listen(1) +srv.settimeout(300) +try: + conn, _ = srv.accept() +except socket.timeout: + open(out, "wb").write(b"") + sys.exit(0) +conn.settimeout(1.0) +buf = b"" +deadline = time.time() + 280 +while time.time() < deadline: + try: + chunk = conn.recv(256) + except socket.timeout: + continue + except OSError: + break + if not chunk: + break + buf += chunk + with open(out, "wb") as f: + f.write(buf) +with open(out, "wb") as f: + f.write(buf) +PY +python3 "$work/peer.py" "$PORT" "$work/rx.bin" & +peer_pid=$! +sleep 1 + +# Finder keystroke timeline, same calibration as verify-iigs.sh: select the +# JOEYLIB volume, Cmd-O to open it, type the program name, Cmd-O to launch. +cat > "$work/scc.lua" <= steps[idx][1] do + steps[idx][2]() + idx = idx + 1 + end +end) +LUA + +cd "$work" +out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$WALL" "$MAME" apple2gs \ + -rompath "$rompath" \ + -flop3 "$work/boot.po" -flop4 "$work/scc.2mg" \ + -printer null_modem -bitb "socket.127.0.0.1:$PORT" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/scc.lua" &1) || true + +kill -9 "$peer_pid" 2>/dev/null +peer_pid="" +sleep 1 + +line=$(echo "$out" | grep -E '^VERIFY-IIGS-SCC ' | tail -1) +rx=$(od -A n -c "$work/rx.bin" 2>/dev/null | tr -s ' ' | tr -d '\n') +echo "$line" +echo " printer-port bytes:$rx" + +if [ -z "$line" ]; then + echo "verify-iigs-serial: FAIL - MAME produced no report" >&2 + echo "$out" | tail -10 >&2 + exit 1 +fi +if ! grep -q "A1" "$work/rx.bin" 2>/dev/null; then + echo "verify-iigs-serial: FAIL - the printer port never sent 'A1'." >&2 + echo " Either SCCPROBE did not run (check the flags above) or the" >&2 + echo " channel A transmit path is broken." >&2 + exit 1 +fi +if ! grep -q "A2" "$work/rx.bin" 2>/dev/null; then + echo "verify-iigs-serial: FAIL - 'A1' arrived but 'A2' did not." >&2 + echo " Opening the MODEM port killed the PRINTER channel: that is the" >&2 + echo " chip-wide WR9 0xC0 force-hardware-reset. Use the per-channel" >&2 + echo " reset (0x80 channel A / 0x40 channel B) in serialInitScc." >&2 + exit 1 +fi +echo "verify-iigs-serial: PASS (printer channel still transmits after the modem port was opened)" diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh index bb86bc2..6e8238c 100755 --- a/scripts/verify-x68000-golden.sh +++ b/scripts/verify-x68000-golden.sh @@ -6,8 +6,14 @@ # runs it headless under the patched MAME, extracts joeylog.txt back off the # image with xdftool, and diffs the hashes against the Apple IIgs reference. # -# Takes roughly 70 minutes: UBER on the generic renderer is slow, and the run -# must reach jlLogFlush at the very end before any hashes exist on disk. +# Takes a few minutes. The run EXITS ON COMPLETION, not on a frame budget: UBER +# ends by clearing the whole stage to colour index 2 and presenting it before it +# blocks in jlWaitForAnyKey, so the Lua below watches GVRAM for that screen. +# (It used to burn a flat 300,000 frames -- ~90 minutes of emulated time -- a +# budget sized back when this port still drew through the slow generic renderer. +# The actual work is a Human68k boot plus well under 10,000 frames.) +# X68K_GOLDEN_FRAMES is now only a backstop: reaching it means the done-screen +# was never detected, which is a FAILURE, not a normal exit. # # X68K_SCRATCH= bash scripts/verify-x68000-golden.sh set -uo pipefail @@ -17,7 +23,7 @@ SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" MAME="$repo/toolchains/cache/mame-mame0264/x68k" TEMPLATE="$SP/x68mame/HUMAN302.XDF" GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}" -FRAMES="${X68K_GOLDEN_FRAMES:-300000}" +FRAMES="${X68K_GOLDEN_FRAMES:-60000}" WALL="${X68K_GOLDEN_WALL:-7200}" export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH" @@ -34,14 +40,13 @@ m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ "$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \ -o "$work/UBER.X" || exit 1 -# TWO DISKS. UBER.X is ~240 KB and Human68k needs ~90 KB, which leaves a 1232 KB -# floppy with under a cluster spare -- so joeylog.txt cannot be written and the -# run silently produces nothing after ~50 minutes. The boot disk therefore holds -# only the binary, and a blank second disk on B: takes the log. AUTOEXEC switches -# to B: before launching so the log lands there. -printf 'B:\r\nA:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" +# TWO DISKS. UBER.X is ~250 KB and Human68k needs ~90 KB, which no longer fit +# together on a 1232 KB floppy at all -- so the boot disk carries only Human68k +# and AUTOEXEC, while the binary AND joeylog.txt live on the second disk on B:. +# AUTOEXEC switches to B: before launching so the log lands there too. +printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" cp "$TEMPLATE" "$work/gold.xdf" -# Blank data disk = the template with every file removed (keeps the format). +# Data disk = the template with every file removed (keeps the format). cp "$TEMPLATE" "$work/data.xdf" for f in HUMAN.SYS CONFIG.SYS KEY.SYS USKCG.SYS BEEP.SYS STARTUP.ENV COMMAND.X AUTOEXEC.BAT; do python3 "$repo/tools/xdftool.py" delete "$work/data.xdf" "$f" >/dev/null 2>&1 @@ -50,27 +55,100 @@ done for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do python3 "$repo/tools/xdftool.py" delete "$work/gold.xdf" "$f" >/dev/null 2>&1 done -python3 "$repo/tools/xdftool.py" add "$work/gold.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 +python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 python3 "$repo/tools/xdftool.py" add "$work/gold.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null -# UBER ends on jlWaitForAnyKey, AFTER jlLogFlush -- post keys late so it exits -# cleanly rather than being killed mid-write. +# Completion-driven exit. UBER's last act before jlWaitForAnyKey is +# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM +# is the "hashes are computed and flushed" signal. The benchmark's own +# jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the +# reading only counts once it has held for GREEN_STABLE frames -- the finished +# screen never changes again, a mid-benchmark frame never lasts. +# +# jlLogFlush only fflush()es; the directory entry is finalised by the atexit +# fclose. So the keypress that releases jlWaitForAnyKey still has to be posted, +# and MAME must not be killed until UBER has returned to Human68k -- hence the +# repeated posts and the EXIT_SETTLE wait before exiting. cat > "$work/gold.lua" < fclose -> back at the Human68k prompt + +-- crtmod 13, 256-colour graphics plane: one 16-bit word per pixel at \$C00000, +-- 512 words per row, the 320x200 stage centred at origin (96, 156). +local function stagePixel(px, py) + return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1) +end + +local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100}, + {4,196}, {160,196}, {315,196}, {80,50}, {240,150}, {200,80} } + +local function screenIsDone() + for _, p in ipairs(kProbes) do + if stagePixel(p[1], p[2]) ~= 2 then + return false + end + end + return true +end + emu.register_frame_done(function() frame = frame + 1 - if frame % 20000 == 0 then io.write("GOLD f"..frame.."\n"); io.flush() end - if frame > ($FRAMES - 60000) and frame % 2000 == 0 then - manager.machine.natkeyboard:post(" ") + if done then return end + if frame % 2000 == 0 then io.write("GOLD f"..frame.."\n"); io.flush() end + + if exitAt == 0 then + if screenIsDone() then + greenRun = greenRun + 1 + if greenRun >= GREEN_STABLE then + io.write("GOLD done-screen f"..frame.."\n"); io.flush() + exitAt = frame + EXIT_SETTLE + end + else + greenRun = 0 + end + else + -- Release jlWaitForAnyKey; extra presses land harmlessly at the prompt. + if frame % 60 == 0 then manager.machine.natkeyboard:post(" ") end + if frame >= exitAt then + done = true + io.write("GOLD exit f"..frame.."\n"); io.flush() + manager.machine:exit() + end + end + + -- Backstop only: reaching this means the done-screen never appeared. + if frame > FRAMES then + done = true + io.write("GOLD budget-exhausted f"..frame.."\n"); io.flush() + manager.machine:exit() end - if frame > $FRAMES then manager.machine:exit() end end) LUA -timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ +runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ -rompath "$SP/x68mame/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \ -video none -sound none -nothrottle \ - -autoboot_script "$work/gold.lua" &1 | grep '^GOLD' + -autoboot_script "$work/gold.lua" &1 | grep '^GOLD') +echo "$runLog" + +# The backstop is not a normal exit: it means UBER never reached its done-screen, +# so whatever log is on the disk (if any) is from an unfinished run. +if echo "$runLog" | grep -q 'budget-exhausted'; then + echo "verify-x68000-golden: FAIL - UBER's done-screen never appeared within $FRAMES frames" >&2 + echo " (raise X68K_GOLDEN_FRAMES if UBER legitimately got slower; otherwise the run died early)" >&2 + exit 1 +fi +if ! echo "$runLog" | grep -q 'done-screen'; then + echo "verify-x68000-golden: FAIL - MAME stopped before UBER finished (wall timeout ${WALL}s?)" >&2 + exit 1 +fi python3 "$repo/tools/xdftool.py" extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || { echo "verify-x68000-golden: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2 diff --git a/scripts/verify-x68000-mouse.sh b/scripts/verify-x68000-mouse.sh new file mode 100755 index 0000000..c1a42d1 --- /dev/null +++ b/scripts/verify-x68000-mouse.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# verify-x68000-mouse.sh - Headless mouse acceptance gate for the X68000 +# port. Boots Human68k under the patched MAME with KEYS.X in +# AUTOEXEC.BAT, waits for the mouse verification scanline's 5A5A +# sentinel in GVRAM, then drives the emulated mouse (an x68k_mouse +# serial device on SCC channel B, decoded by the real IOCS ROM) through +# MAME's input-port override API and reads the scanline back after each +# step. Covered: delta accumulation in both directions, clamping at all +# four stage edges, movement after clamping, and press/hold/release of +# both buttons. +# +# The mouse axes are 12-bit absolute counters the device diffs into +# delta packets, so each Lua set_value(v) below moves the mouse by +# (v - previous v). Values are biased to 2000 mid-boot for headroom in +# both directions. MAME's analog override is timing-sensitive: an +# injected delta is sometimes consumed once and sometimes re-sent over +# several solicitations, so exact interior positions are NOT testable +# through this API. Every position expectation therefore lands ON a +# clamp edge, where over-travel is absorbed: each axis is slammed +# independently into all four stage edges, which proves delta sign, +# axis attribution, and all four clamps deterministically. Slams use +# 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. +# +# X68K_SCRATCH= bash scripts/verify-x68000-mouse.sh +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" +MAME="$repo/toolchains/cache/mame-mame0264/x68k" +TEMPLATE="$SP/x68mame/HUMAN302.XDF" +WALL="${X68K_MOUSE_WALL:-900}" + +[ -x "$MAME" ] || { echo "verify-x68000-mouse: missing patched MAME at $MAME" >&2; exit 2; } +[ -f "$TEMPLATE" ] || { echo "verify-x68000-mouse: missing $TEMPLATE" >&2; exit 2; } +[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-mouse: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; } + +work=$(mktemp -d -t joey-x68mouse.XXXXXX) +trap 'rm -rf "$work"' EXIT + +printf 'A:\\KEYS.X\r\n\x1a' > "$work/AUTOEXEC.BAT" +cp "$TEMPLATE" "$work/boot.xdf" +for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do + python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 +done +python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1 +python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null + +cat > "$work/mouse.lua" <<'LUA' +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local frame = 0 +local t0 = 0 +local biased = false +local done = false +local checks = 0 +local fails = {} + +-- Stage pixel (px, y) -> GVRAM word low byte. +local function pix(px, y) + local rowBase = 0xC00000 + ((156 + y) * 512 + 96) * 2 + return mem:read_u8(rowBase + 2 * px + 1) +end + +-- Decode the mouse verification row (stage y 199): 5A5A sentinel, +-- present/left/right/middle nibbles, then x and y as 4 nibbles each. +local function readMouse() + local m = {} + m.sentinel = pix(0, 199) == 0x5 and pix(1, 199) == 0xA and + pix(2, 199) == 0x5 and pix(3, 199) == 0xA + m.present = pix(4, 199) + m.left = pix(5, 199) + m.right = pix(6, 199) + m.middle = pix(7, 199) + m.x = (pix(8, 199) << 12) | (pix(9, 199) << 8) | (pix(10, 199) << 4) | pix(11, 199) + m.y = (pix(12, 199) << 12) | (pix(13, 199) << 8) | (pix(14, 199) << 4) | pix(15, 199) + return m +end + +-- The x68k mouse device (rs232 slot "mouse_port", option "x68k") owns +-- three ports: X and Y are 12-bit absolute counters it diffs into +-- delta packets, BTN holds left (mask 2) and right (mask 1). +local ports = manager.machine.ioport.ports +local axisX, axisY, btnLeft, btnRight +local px_port = ports[":mouse_port:x68k:X"] +local py_port = ports[":mouse_port:x68k:Y"] +local pb_port = ports[":mouse_port:x68k:BTN"] +if px_port then for _, f in pairs(px_port.fields) do axisX = f end end +if py_port then for _, f in pairs(py_port.fields) do axisY = f end end +if pb_port then + for _, f in pairs(pb_port.fields) do + if f.mask == 2 then btnLeft = f end + if f.mask == 1 then btnRight = f end + end +end +if not (axisX and axisY and btnLeft and btnRight) then + io.write("VERIFY-X68K-MOUSE error: mouse ioports not found\n") + io.flush() + manager.machine:exit() +end + +local function expect(name, got, want) + checks = checks + 1 + if got ~= want then + fails[#fails + 1] = string.format("%s=%d(want %d)", name, got, want) + end +end + +local function expectRow(step, m, x, y, l, r) + checks = checks + 1 + if not m.sentinel then fails[#fails + 1] = step .. ":sentinel" end + expect(step .. ":present", m.present, 1) + expect(step .. ":x", m.x, x) + expect(step .. ":y", m.y, y) + expect(step .. ":left", m.left, l) + expect(step .. ":right", m.right, r) + expect(step .. ":middle", m.middle, 0) +end + +-- Steps run at fixed offsets from t0 (the frame the sentinel landed +-- plus settle time). Slam sub-steps are 15 frames apart and every +-- read sits 60+ frames after the last injection, so the 4800-baud +-- packets, the IOCS solicitation, and the app's poll all settle. +-- Down-slams walk the counter 2000 -> 1365, up-slams walk it back. +local function walkDown(axis, at, steps) + for i = 1, 5 do + steps[#steps + 1] = { at = at + 15 * (i - 1), run = function() axis:set_value(2000 - 127 * i) end } + end +end + +local function walkUp(axis, at, steps) + for i = 1, 5 do + steps[#steps + 1] = { at = at + 15 * (i - 1), run = function() axis:set_value(1365 + 127 * i) end } + end +end + +local steps = {} +-- Slam both axes to the (0, 0) corner: any possible starting position +-- is absorbed by the clamps, making the state absolute from here on. +walkDown(axisX, 0, steps) +walkDown(axisY, 0, steps) +steps[#steps + 1] = { at = 120, run = function() expectRow("slamOrigin", readMouse(), 0, 0, 0, 0) end } +-- One axis at a time into each remaining edge: proves delta sign and +-- axis attribution (the untouched axis must stay put) plus the clamp. +walkUp(axisX, 130, steps) +steps[#steps + 1] = { at = 250, run = function() expectRow("slamRight", readMouse(), 319, 0, 0, 0) end } +walkUp(axisY, 260, steps) +steps[#steps + 1] = { at = 380, run = function() expectRow("slamBottom", readMouse(), 319, 199, 0, 0) end } +walkDown(axisX, 390, steps) +steps[#steps + 1] = { at = 510, run = function() expectRow("slamLeft", readMouse(), 0, 199, 0, 0) end } +walkDown(axisY, 520, steps) +steps[#steps + 1] = { at = 640, run = function() expectRow("slamTop", readMouse(), 0, 0, 0, 0) end } +-- Button choreography at a settled position. +steps[#steps + 1] = { at = 650, run = function() btnLeft:set_value(1) end } +steps[#steps + 1] = { at = 710, run = function() expectRow("leftPress", readMouse(), 0, 0, 1, 0) end } +steps[#steps + 1] = { at = 740, run = function() expectRow("leftHold", readMouse(), 0, 0, 1, 0) end } +steps[#steps + 1] = { at = 750, run = function() btnLeft:clear_value(); btnRight:set_value(1) end } +steps[#steps + 1] = { at = 810, run = function() expectRow("rightPress", readMouse(), 0, 0, 0, 1) end } +steps[#steps + 1] = { at = 820, run = function() btnRight:clear_value() end } +steps[#steps + 1] = { at = 880, run = function() + expectRow("released", readMouse(), 0, 0, 0, 0) + done = true + io.write(string.format("VERIFY-X68K-MOUSE checks=%d fails=%d%s\n", + checks, #fails, + #fails > 0 and (" detail=" .. table.concat(fails, ",")) or "")) + io.flush() + manager.machine:exit() +end } + +emu.register_frame_done(function() + frame = frame + 1 + if done then return end + if not biased and frame == 300 then + -- Mid-boot counter bias for two-directional headroom. However + -- the boot-time solicitation history consumes this, the slam + -- steps above make the position deterministic afterwards. + biased = true + axisX:set_value(2000) + axisY:set_value(2000) + end + if t0 == 0 and frame > 600 and frame % 100 == 0 then + -- KEYS is up once both verification sentinels are stamped. + if pix(0, 198) == 0xA and pix(1, 198) == 0x5 and + pix(0, 199) == 0x5 and pix(1, 199) == 0xA then + t0 = frame + 60 + end + end + if t0 ~= 0 then + for _, s in ipairs(steps) do + if frame == t0 + s.at then s.run() end + end + end + if t0 == 0 and frame > 30000 then + done = true + io.write("VERIFY-X68K-MOUSE timeout: sentinel never appeared\n") + io.flush() + manager.machine:exit() + end +end) +LUA + +# Run from the work dir so MAME's cfg/ and nvram/ droppings land there +# instead of the caller's cwd. +cd "$work" +out=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ + -rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/mouse.lua" &1) || true + +line=$(echo "$out" | grep -E '^VERIFY-X68K-MOUSE ' | tail -1) +echo "$line" +if [ -z "$line" ] || echo "$line" | grep -q -e timeout -e error; then + echo "verify-x68000-mouse: FAIL - KEYS never came up or ports missing" >&2 + echo "$out" | tail -10 >&2 + exit 1 +fi +if echo "$line" | grep -q 'fails=0$'; then + echo "verify-x68000-mouse: PASS (deltas, all four clamps, and both buttons verified through the IOCS ROM)" +else + echo "verify-x68000-mouse: FAIL" >&2 + exit 1 +fi diff --git a/src/amiga/input.c b/src/amiga/input.c index eaedf10..f59b8de 100644 --- a/src/amiga/input.c +++ b/src/amiga/input.c @@ -332,6 +332,7 @@ void jlpInputInit(void) { TAG_DONE); if (gWindow != NULL) { + gMouseAttached = true; // Intuition delivers IDCMP mouse events on this window gMouseX = (int16_t)(gWindow->Width / 2); gMouseY = (int16_t)(gWindow->Height / 2); diff --git a/src/amiga/serial.c b/src/amiga/serial.c index 2e52d9c..596fec9 100644 --- a/src/amiga/serial.c +++ b/src/amiga/serial.c @@ -105,35 +105,45 @@ void jlpSerialFlush(void) { } +// Why the last jlpSerialOpen failed, for host-side diagnosis: a silent false was unattributable +// through three swallowing layers (this file -> core gate -> the client's "non-fatal" open). +// stage: 1=CreateMsgPort 2=CreateIORequest 3=OpenDevice 4=SDCMD_SETPARAMS 5=write IORequest. +uint8_t gAmigaSerialFailStage = 0u; +BYTE gAmigaSerialFailErr = 0; +BYTE gAmigaSerialFailIoErr = 0; +uint8_t gAmigaSerialFailDevice = 0u; + + bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { ULONG unit; UBYTE flags; + BYTE err; if (gOpen) { return false; } + gAmigaSerialFailStage = 0u; + gAmigaSerialFailErr = 0; gPort = CreateMsgPort(); if (gPort == NULL) { + gAmigaSerialFailStage = 1u; return false; } gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); if (gReadReq == NULL) { + gAmigaSerialFailStage = 2u; serialTeardown(); return false; } - // Any device but SLOT maps to the built-in port (unit 0); SLOT picks a unit. - unit = (device == JL_SERIAL_SLOT) ? (ULONG)config->unit : 0ul; - if (OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul) != 0) { - serialTeardown(); - return false; - } - - // Line parameters via SDCMD_SETPARAMS. + // io_SerFlags participates in OPENDEVICE, not just SETPARAMS -- the autodoc contract is "the + // request must be zeroed EXCEPT io_SerFlags, set before open". Setting the flags only after the + // open (the old order) both violated that contract and left the unit opened EXCLUSIVE with + // xon/xoff ENABLED until SETPARAMS ran. flags = 0u; if (config->flow != JL_SERIAL_FLOW_XONXOFF) { - flags |= SERF_XDISABLED; // no software flow control + flags |= SERF_XDISABLED; // no software flow control: 0x11/0x13 are DATA here } if (config->flow == JL_SERIAL_FLOW_RTSCTS) { flags |= SERF_7WIRE; // hardware RTS/CTS handshake @@ -144,13 +154,42 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { flags |= SERF_PARTY_ODD; } } + gReadReq->io_SerFlags = flags; + + // Any device but SLOT maps to the built-in port (unit 0); SLOT picks a unit. + unit = (device == JL_SERIAL_SLOT) ? (ULONG)config->unit : 0ul; + err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul); + if (err != 0) { + // Exclusive access is refused if ANYONE holds the unit; fall back to a shared open, which is + // fine for this HAL (it is the only serial user in the program). + gReadReq->io_SerFlags = (UBYTE)(flags | SERF_SHARED); + err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul); + } + if (err != 0) { + gAmigaSerialFailStage = 3u; + gAmigaSerialFailErr = err; + // Distinguish a REAL open failure from a lying return value: io_Error is the device's own + // verdict and io_Device is non-NULL if the device actually attached. + gAmigaSerialFailIoErr = gReadReq->IOSer.io_Error; + gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u; + serialTeardown(); + return false; + } gReadReq->io_Baud = config->baud; gReadReq->io_ReadLen = config->dataBits; gReadReq->io_WriteLen = config->dataBits; gReadReq->io_StopBits = config->stopBits; - gReadReq->io_SerFlags = flags; + // CreateIORequest zero-fills the request, and serial.device REJECTS a SETPARAMS whose io_RBufLen + // is below the documented 64-byte minimum -- so leaving it at 0 made every open fail (silently, + // three swallowed layers up). Ask for a real ring while at it: the X68000 port just proved a + // ~64-byte receive window loses mid-frame bytes whenever the client blits or polls input. + gReadReq->io_RBufLen = 4096ul; + // io_SerFlags already holds the value the successful open used (possibly including the + // SERF_SHARED fallback) -- do not strip it here. gReadReq->IOSer.io_Command = SDCMD_SETPARAMS; if (DoIO((struct IORequest *)gReadReq) != 0) { + gAmigaSerialFailStage = 4u; + gAmigaSerialFailErr = gReadReq->IOSer.io_Error; serialTeardown(); return false; } @@ -158,6 +197,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { // Second request sharing the same open device (copy carries io_Device/Unit). gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); if (gWriteReq == NULL) { + gAmigaSerialFailStage = 5u; serialTeardown(); return false; } diff --git a/src/atarist/input.c b/src/atarist/input.c index 2edd845..312d334 100644 --- a/src/atarist/input.c +++ b/src/atarist/input.c @@ -379,6 +379,7 @@ void jlpJoystickReset(jlJoystickE js) { void jlpInputInit(void) { _KEYTAB *keyTab; + gMouseAttached = true; // the IKBD mouse line is always wired on an ST memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); diff --git a/src/atarist/serial.c b/src/atarist/serial.c index 312545f..43705a4 100644 --- a/src/atarist/serial.c +++ b/src/atarist/serial.c @@ -28,10 +28,29 @@ // bit 1 parity even (1) / odd (0) #define ST_UCR_CLK_DIV16 0x80 +// Rsconf sentinels. -1 in any slot means "leave that field alone" (TOS writes a +// field only when the value it is handed is in range). -2 in the SPEED slot is +// the separate, documented baud inquire (mint/ostruct.h BAUD_INQUIRE): EmuTOS's +// rsconf_mfp returns iorec->baudrate and returns BEFORE touching any register, +// so it is side-effect free. Do NOT use -1 to inquire; -1 is "no change". +#define ST_RSCONF_NO_CHANGE ((int16_t)-1) +#define ST_RSCONF_INQUIRE ((int16_t)-2) +#define ST_BAUD_CODE_MAX 15 + + +// ----- Module state ----- + +// What jlpSerialOpen took away, so jlpSerialClose can hand it back. Both sit at +// ST_RSCONF_NO_CHANGE when there is nothing to restore, so a close with no +// matching open -- or a second close -- lowers to an Rsconf that writes nothing. +static int16_t gSavedBaudCode = ST_RSCONF_NO_CHANGE; +static int16_t gSavedUcr = ST_RSCONF_NO_CHANGE; + // ----- Prototypes ----- static int16_t serialBaudCode(uint32_t baud); +static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr); static int16_t serialUcr(const jlSerialConfigT *cfg); @@ -61,6 +80,46 @@ static int16_t serialBaudCode(uint32_t baud) { } +// XBIOS 15 (Rsconf) done by hand, because mint's Rsconf() macro CANNOT be +// trusted with computed arguments. +// +// osbind.h's own change log admits it: the seven-word trap macro relaxed its +// operand constraints from "r" to "g" on the grounds that "these args will +// never be expressions". With "g", gcc is free to leave an argument in a stack +// temp -- and the macro then pushes seven words with movew ...,sp@-, moving sp +// out from under that temp, so the push reads the wrong offset. The disassembly +// showed exactly that: the baud code was parked at sp@(14) and pushed after sp +// had already moved. MEASURED under EmuTOS/Hatari: every requested rate (19200, +// 9600, 4800, 2400, 1200, 300) programmed 9600, and shuffling the C around only +// changed WHICH wrong code came out. The UCR happened to survive in a register. +// +// Pushing the words here, with the three variable ones bound to registers, +// removes the sp-relative temp entirely. rsr/tsr/scr are immediates: no caller +// wants them, and leaving them "no change" means there is nothing to undo. +static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr) { + // Bound to d0 the way osbind.h's own trap_14_* helpers bind theirs: XBIOS + // returns in d0, and an unbound output operand would leave gcc free to pick + // another register that the asm never writes (which silently returned + // garbage here until the probe caught it). + register int32_t ret __asm__("d0"); + + __asm__ volatile ( + "movew #-1,%%sp@-\n\t" /* scr: no change */ + "movew #-1,%%sp@-\n\t" /* tsr: no change */ + "movew #-1,%%sp@-\n\t" /* rsr: no change */ + "movew %3,%%sp@-\n\t" /* ucr */ + "movew %2,%%sp@-\n\t" /* flow */ + "movew %1,%%sp@-\n\t" /* baud */ + "movew #15,%%sp@-\n\t" /* XBIOS 15 = Rsconf */ + "trap #14\n\t" + "lea %%sp@(14),%%sp" + : "=d" (ret) + : "d" (baud), "d" (flow), "d" (ucr) + : "d1", "d2", "a0", "a1", "cc", "memory"); + return ret; +} + + static int16_t serialUcr(const jlSerialConfigT *cfg) { int16_t ucr; @@ -85,8 +144,23 @@ uint16_t jlpSerialAvailable(void) { } +// The trap path hijacks no vector, but jlpSerialOpen did change the line +// settings, and those live in the MFP and TOS's iorec -- both of which outlive +// the program. So hand back the two fields TOS lets us recover: the baud code +// (from the -2 inquire) and the UCR (the high byte of what Rsconf returned when +// it installed ours). rsr/tsr/scr were never written -- open passes -1 for them +// -- so they need no undo, and scr is not in the return value anyway. +// +// Flow control is deliberately NOT restored: Rsconf has no inquire for it (TOS +// keeps it in iorec->flowctrl with no way back out), so the only options are +// leaving it or guessing, and guessing a value onto a working port is the +// mistake the X68000 _SET232C note in that port's serial.c warns about. A user +// who had hardware handshaking on keeps the app's setting until something sets +// it again; that is a documented limitation, not a silent corruption. void jlpSerialClose(void) { - // Trap path hijacks no vector -- nothing to tear down. + (void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr); + gSavedBaudCode = ST_RSCONF_NO_CHANGE; + gSavedUcr = ST_RSCONF_NO_CHANGE; } @@ -99,6 +173,10 @@ void jlpSerialFlush(void) { bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { int16_t flow; + int16_t baudCode; + int16_t ucr; + int32_t inquired; + uint32_t previous; (void)device; // the base ST has one RS-232 line: the Modem port. switch (config->flow) { @@ -106,8 +184,33 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { case JL_SERIAL_FLOW_XONXOFF: flow = 1; break; default: flow = 0; break; } - // Rsconf(speed, flow, ucr, rsr, tsr, scr); -1 leaves a field unchanged. - Rsconf(serialBaudCode(config->baud), flow, serialUcr(config), -1, -1, -1); + + // EVALUATE THE HELPERS INTO LOCALS FIRST -- never inline a call into the + // Rsconf() argument list. mint/osbind.h's own change log says so: the + // seven-word trap macro relaxed its operand constraints from "r" to "g" + // and notes that is "ok since these args will never be expressions", so + // gcc may materialise an argument from a stack slot AFTER the macro has + // moved sp out from under it. Passing serialBaudCode(...) inline was + // exactly that, and it lost the baud code: MEASURED under EmuTOS/Hatari, + // every requested rate -- 19200, 9600, 4800, 2400, 1200, 300 -- programmed + // code 1 (9600). serialUcr's result survived; the baud one did not. + baudCode = serialBaudCode(config->baud); + ucr = serialUcr(config); + + // Snapshot the baud code BEFORE installing ours -- the inquire reports the + // rate currently in force. Range-gate the answer: a TOS that did not + // implement the -2 inquire would fall through to the normal path, write + // nothing (every field is out of range), and hand back the packed + // ucr/rsr/tsr instead, which is >= 0x01000000 for any non-zero UCR. + inquired = serialRsconf(ST_RSCONF_INQUIRE, ST_RSCONF_NO_CHANGE, ST_RSCONF_NO_CHANGE); + if (inquired >= 0 && inquired <= ST_BAUD_CODE_MAX) { + gSavedBaudCode = (int16_t)inquired; + } + + // The return is the PREVIOUS (ucr << 24) | (rsr << 16) | (tsr << 8), so the + // same call that installs our line settings also reports the UCR we displaced. + previous = (uint32_t)serialRsconf(baudCode, flow, ucr); + gSavedUcr = (int16_t)((previous >> 24) & 0xFFu); return true; } diff --git a/src/core/input.c b/src/core/input.c index 10e3b82..69b4a5d 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -27,6 +27,7 @@ uint8_t gKeyState [KEY_COUNT]; uint8_t gKeyPrev [KEY_COUNT]; int16_t gMouseX = 0; +bool gMouseAttached = false; // set by jlpInputInit where the HAL confirms a mouse int16_t gMouseY = 0; uint8_t gMouseButtonState[MOUSE_BUTTON_COUNT]; uint8_t gMouseButtonPrev [MOUSE_BUTTON_COUNT]; @@ -205,6 +206,11 @@ bool jlMouseReleased(jlMouseButtonE button) { } +bool jlMousePresent(void) { + return gMouseAttached; +} + + int16_t jlMouseX(void) { return gMouseX; } diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h index d012871..ef40eef 100644 --- a/src/core/inputInternal.h +++ b/src/core/inputInternal.h @@ -22,6 +22,7 @@ extern uint8_t gKeyState[KEY_COUNT]; extern uint8_t gKeyPrev [KEY_COUNT]; +extern bool gMouseAttached; extern int16_t gMouseX; extern int16_t gMouseY; extern uint8_t gMouseButtonState[MOUSE_BUTTON_COUNT]; diff --git a/src/core/port.h b/src/core/port.h index ca7d781..177c6de 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -36,6 +36,16 @@ #ifdef JOEYLIB_PLATFORM_IIGS +// Root the save tree at the APPLICATION directory via the GS/OS "1/" prefix +// designator (the Loader sets prefix 1 to the app's directory at launch). +// Unrooted paths resolve against prefix 0, which a Finder launch leaves +// pointing away from (or nowhere near) the app's volume - measured: every +// plain "SAVES/x" open failed under MAME GS/OS 6 while the same file opened +// fine through "1/SAVES/x". libc's fopen passes the string verbatim to +// GS/OS Open, which accepts prefix-designator syntax. +#define JL_SAVE_DIR "1/SAVES" +#define JL_SAVE_DIR_PREFIX "1/SAVES/" + // ===================================================================== // IIgs direct-dispatch macros. // diff --git a/src/core/save.c b/src/core/save.c index 415deda..82c8468 100644 --- a/src/core/save.c +++ b/src/core/save.c @@ -14,8 +14,16 @@ #include "port.h" -#define SAVE_DIR "SAVES" -#define SAVE_DIR_PREFIX "SAVES/" +// A port that must root the save tree elsewhere overrides these in port.h +// (IIgs: GS/OS prefix-designator "1/" = the application directory - unrooted +// paths resolve against prefix 0, which a Finder launch does not point at the +// app's volume, so plain "SAVES/x" opens fail there). +#ifndef JL_SAVE_DIR +#define JL_SAVE_DIR "SAVES" +#define JL_SAVE_DIR_PREFIX "SAVES/" +#endif +#define SAVE_DIR JL_SAVE_DIR +#define SAVE_DIR_PREFIX JL_SAVE_DIR_PREFIX #define SAVE_PATH_MAX 256 diff --git a/src/dos/input.c b/src/dos/input.c index 45fc6f6..e089eee 100644 --- a/src/dos/input.c +++ b/src/dos/input.c @@ -580,6 +580,7 @@ void jlpInputInit(void) { gHooked = true; gMousePresent = mouseInit(); + gMouseAttached = gMousePresent; gJoystickPresent = joystickInit(); } diff --git a/src/iigs/hal.c b/src/iigs/hal.c index d98fff6..86df021 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -117,6 +117,8 @@ extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, // bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index // table (PERF-AUDIT #3). extern void iigsInitRowLut(void); +// Serial soft-ring pump (serial.c) -- called from every long-running HAL path. +extern void iigsSerialPump(void); // Filled circle, scanline-style. Takes the raw fill nibble; the asm // derives the doubled fill byte and RMW nibble bytes (PERF-AUDIT #41). extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); @@ -318,8 +320,10 @@ void jlpPresent(const jlSurfaceT *src) { // rows, which seed all 16 palettes, present, then do pixel-only presents // over rows 190-199 (where a descending-push overrun would land first) // and re-compare $E1 against the stage. + iigsSerialPump(); uploadScbAndPaletteIfNeeded(src); iigsBlitStageToShr(); + iigsSerialPump(); } @@ -380,16 +384,19 @@ void jlpShutdown(void) { // would already be too late to keep that struct out of the display. static void *gStagePixelsHandle = NULL; -// Bank-0 region the linker uses without telling the Memory Manager: -// the C heap ($BC67-$BF00 in the current layout) AND the bank-0 -// ALIASES of joeyDraw.s's D-relative pointer-scratch symbols -// (dpxlScratch/dlnScratch/dcPixPtr/fcPixPtr, ~$BBB8-$BC66) -- the -// [pix],y pattern reads/writes bank 0 at the symbol's OFFSET. If an MM -// handle (codegen arena, jlpBigAlloc scratch) lands there, every draw -// primitive plots through stomped pointers (PERF-AUDIT.md #88, the -// codegen-on showcase hang). Claim generously: $00:B900-$BEFF. -#define IIGS_BANK0_CLAIM_BASE ((void *)0x00B900L) -#define IIGS_BANK0_CLAIM_BYTES 0x0600ul +// Bank-0 region the linker uses without telling the Memory Manager: the +// link-time BSS (which holds the bank-0 ALIASES of joeyDraw.s's D-relative +// pointer-scratch symbols -- the [pix],y pattern reads/writes bank 0 at the +// symbol's OFFSET) plus the link-time C heap window. If an MM handle +// (codegen arena, jlpBigAlloc scratch, a GS/OS file buffer) lands there, +// every draw primitive plots through stomped pointers (PERF-AUDIT.md #88, +// the codegen-on showcase hang). +// The span is COMPUTED from the link symbols, not hard-coded: the old +// $B900-$BEFF constant was the DEMO layout's top slice, and the bigger +// RetroNet client linked its scratch at ~$84CC -- entirely unprotected, +// with the claim failing against whatever GS/OS had placed at $B900. +extern char __bss_start[]; +extern char __heap_end[]; static void *gBank0ScratchHandle = NULL; @@ -408,12 +415,17 @@ static void claimStagePixels(void) { } } if (gBank0ScratchHandle == NULL) { - gBank0ScratchHandle = NewHandle(IIGS_BANK0_CLAIM_BYTES, - _ownerid, - attrAddr | attrFixed | attrLocked, - IIGS_BANK0_CLAIM_BASE); + unsigned long base = (unsigned long)(void *)__bss_start; + unsigned long end = (unsigned long)(void *)__heap_end; + + if (end > base) { + gBank0ScratchHandle = NewHandle(end - base, + _ownerid, + attrAddr | attrFixed | attrLocked, + (void *)base); + } if (gBank0ScratchHandle == NULL) { - coreSetError("bank-0 heap/scratch MM reservation failed"); + coreSetError("bank-0 bss/heap MM reservation failed"); } } } @@ -502,7 +514,10 @@ void jlpWaitVBL(void) { tick = iigsGetTickWord(); while (iigsGetTickWord() == tick) { - /* wait for the VBL interrupt to advance the tick */; + // The VBL spin is the client loop's longest deaf window (~16ms); drain the 3-byte SCC FIFO + // into the serial soft ring while waiting or a 9600-baud downlink loses mid-frame bytes + // (the X68000 lesson, same Z8530). + iigsSerialPump(); } } diff --git a/src/iigs/input.c b/src/iigs/input.c index ab1998a..8a8c7f7 100644 --- a/src/iigs/input.c +++ b/src/iigs/input.c @@ -421,6 +421,7 @@ void jlpInputInit(void) { memset(gKeyPrev, 0, sizeof(gKeyPrev)); buildAsciiTable(); + gMouseAttached = true; // the ADB mouse is part of every IIgs gMouseAbsX = SURFACE_WIDTH / 2; gMouseAbsY = SURFACE_HEIGHT / 2; gMouseX = gMouseAbsX; @@ -432,6 +433,12 @@ void jlpInputInit(void) { void jlpInputPoll(void) { + // Keep the serial soft ring fed across the input scan (multiple I/O-page reads). + { + extern void iigsSerialPump(void); + + iigsSerialPump(); + } uint8_t kbd; uint8_t ascii; uint8_t key; @@ -450,6 +457,14 @@ void jlpInputPoll(void) { // ever fails to clear. strobeObserved = false; for (drainGuard = 0; drainGuard < KBD_DRAIN_GUARD; drainGuard++) { + { + extern void iigsSerialPump(void); + + // Per-iteration pump (the X68000 per-group lesson): the FIFO drain + decode below is a + // multi-ms deaf window at 9600 and the Z8530 FIFO is 3 bytes - a pump only at poll ENTRY + // measurably lost a 4-byte clump mid-frame. + iigsSerialPump(); + } kbd = *IIGS_KBD; if ((kbd & KBD_STROBE_BIT) == 0) { break; diff --git a/src/iigs/serial.c b/src/iigs/serial.c index 33f58dd..3e26be8 100644 --- a/src/iigs/serial.c +++ b/src/iigs/serial.c @@ -1,10 +1,17 @@ // Apple IIgs serial HAL -- direct, polled register access. // // Three endpoints behind one API, selected at open: -// JL_SERIAL_MODEM -- modem port = Zilog 8530 SCC channel A -// JL_SERIAL_PRINTER -- printer port = Zilog 8530 SCC channel B +// JL_SERIAL_MODEM -- modem port = Zilog 8530 SCC channel B +// JL_SERIAL_PRINTER -- printer port = Zilog 8530 SCC channel A // JL_SERIAL_SLOT -- 6551 ACIA on a Super Serial Card in cfg.unit's slot -// JL_SERIAL_DEFAULT -- the modem port (SCC channel A) +// JL_SERIAL_DEFAULT -- the modem port (SCC channel B) +// +// Channel-to-port assignment: MEASURED against MAME 0.264's apple2gs (its +// machine config wires out_txda -> "printer" and out_txdb -> "modem"); with +// the old modem=A mapping every TX byte left the unconnected printer port and +// the modem-port peer saw silence. If real silicon turns out to disagree, +// pick the other port at open (MODEM <-> PRINTER) rather than editing this +// table. // // Direct register pokes through 24-bit long pointers to $00C0xx, exactly like // input.c / the video + DOC pokes -- no GS/OS driver, no ROM firmware, lowest @@ -29,10 +36,17 @@ // ----- SCC (modem/printer) registers -- 24-bit long pointers ----- -#define SCC_B_CTRL ((volatile uint8_t *)0x00C038L) // printer port command/control (ch B) -#define SCC_A_CTRL ((volatile uint8_t *)0x00C039L) // modem port command/control (ch A) -#define SCC_B_DATA ((volatile uint8_t *)0x00C03AL) // printer port data (ch B) -#define SCC_A_DATA ((volatile uint8_t *)0x00C03BL) // modem port data (ch A) +#define SCC_B_CTRL ((volatile uint8_t *)0x00C038L) // modem port command/control (ch B) +#define SCC_A_CTRL ((volatile uint8_t *)0x00C039L) // printer port command/control (ch A) +#define SCC_B_DATA ((volatile uint8_t *)0x00C03AL) // modem port data (ch B) +#define SCC_A_DATA ((volatile uint8_t *)0x00C03BL) // printer port data (ch A) + +// WR9 bits 7-6 are the reset command: 00 none, 01 channel B, 10 channel A, +// 11 FORCE HARDWARE RESET. Only ever issue the one for the channel being +// opened: 0xC0 also wipes the OTHER channel's registers, FIFOs, baud generator +// and DTR/RTS, and on a IIgs that is the port AppleTalk runs on. +#define SCC_WR9_RESET_A 0x80u +#define SCC_WR9_RESET_B 0x40u // RR0 (status) bits, read directly from the command register. #define SCC_RR0_RX_AVAIL 0x01 @@ -57,6 +71,11 @@ // ----- Module state ----- static uint8_t gRxRing[RX_RING_SIZE]; + +// Flow counters for the RetroNet RXSTAT diagnostic (read via extern): bytes moved SCC->ring and +// ring->caller since open. Cheap enough to keep unconditionally. +uint16_t gIigsRxPumped = 0u; +uint16_t gIigsRxRead = 0u; static uint16_t gRxHead; static uint16_t gRxTail; static bool gOpen; @@ -68,7 +87,7 @@ static volatile uint8_t *gData; // data register (both) // ----- Prototypes ----- static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg); -static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, const jlSerialConfigT *cfg); +static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg); static bool serialRxReady(void); static bool serialTxReady(void); static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val); @@ -85,7 +104,7 @@ static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val) { } -static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, const jlSerialConfigT *cfg) { +static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg) { uint8_t wr4; uint8_t wr3; uint8_t wr5; @@ -110,12 +129,32 @@ static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, const default: wr3 = 0xC0u; wr5 = 0x60u; break; // 8 } + // baud = PCLK(3.6864MHz) / (2 * (TC+2) * 16) with the WR4 /16 clock mode -- the real-hardware + // formula, which MAME's z80scc implements faithfully. Stock MAME 0.264 apple2gs feeds the SCC + // the WRONG clock (14.318MHz/2 instead of the 3.6864MHz PCLK) and pins RX at rxc/16 = 230400 via + // configure_channels; that is fixed in the DRIVER (patches/mame-0.264-apple2gs-scc-clock.patch), + // not here. tc = SCC_TC_NUM / (int32_t)cfg->baud - 2; if (tc < 0) { tc = 0; } - sccWrite(ctrl, 9u, 0x00u); // reset pointer / no interrupts (polled) + // Reset THIS CHANNEL ONLY, first: on real silicon the firmware has already reset the chip at boot, + // but MAME's z80scc powers up unreset and its TX path stays dead without it (measured: every + // register write landed - DTR visibly rose at the far end - yet no data byte ever left the chip). + // This used to be WR9 = 0xC0, a force-hardware-reset that took the OTHER channel down with it. + // + // Not fully solved, and it cannot be from here: WR9 is a single chip-wide register AND is + // write-only, so the reset command unavoidably rewrites the chip's interrupt-control bits + // (MIE/VIS/NV) with our zeroes, and there is no way to read the previous value back to preserve + // them. A driver on the other channel that takes interrupts -- AppleTalk -- is therefore still + // disturbed, just no longer wiped. Sharing the SCC properly would mean going through GS/OS's + // serial drivers, which this HAL deliberately bypasses (see the file header). + sccWrite(ctrl, 9u, reset); + sccWrite(ctrl, 1u, 0x00u); // no interrupts from THIS channel -- WR1 is per-channel, WR9 is not + sccWrite(ctrl, 10u, 0x00u); // NRZ, no loop/mark-idle: a CHANNEL reset preserves the + // encoding bits the old chip-wide reset cleared, so a + // channel left in FM0 by LocalTalk would stay there. sccWrite(ctrl, 4u, wr4); sccWrite(ctrl, 3u, wr3); // Rx bits, not yet enabled sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x82u)); // Tx bits + DTR + RTS, not yet enabled @@ -224,9 +263,9 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { switch (device) { case JL_SERIAL_PRINTER: gIsScc = true; - gStatus = SCC_B_CTRL; - gData = SCC_B_DATA; - serialInitScc(SCC_B_CTRL, SCC_B_DATA, config); + gStatus = SCC_A_CTRL; + gData = SCC_A_DATA; + serialInitScc(SCC_A_CTRL, SCC_A_DATA, SCC_WR9_RESET_A, config); break; case JL_SERIAL_SLOT: { volatile uint8_t *base; @@ -245,9 +284,9 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { case JL_SERIAL_DEFAULT: default: gIsScc = true; - gStatus = SCC_A_CTRL; - gData = SCC_A_DATA; - serialInitScc(SCC_A_CTRL, SCC_A_DATA, config); + gStatus = SCC_B_CTRL; + gData = SCC_B_DATA; + serialInitScc(SCC_B_CTRL, SCC_B_DATA, SCC_WR9_RESET_B, config); break; } @@ -256,6 +295,15 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { } +// Drain the SCC hardware FIFO into the soft ring from ANY wait/blit/input site (the X68000 lesson: +// a polled port with a tiny hardware FIFO loses mid-frame bytes during every deaf window - the Z8530 +// RX FIFO is 3 bytes, ~3ms at 9600, and the client's VBL-paced loop is deaf for ~16ms per pass). +// Safe to call at any time; no-ops when the port is closed. +void iigsSerialPump(void) { + jlpSerialPoll(); +} + + void jlpSerialPoll(void) { uint16_t tail; uint16_t guard; @@ -275,6 +323,7 @@ void jlpSerialPoll(void) { if (next != gRxHead) { gRxRing[tail] = b; tail = next; + gIigsRxPumped++; } guard--; } @@ -292,6 +341,7 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { head = (uint16_t)((head + 1u) & RX_RING_MASK); } gRxHead = head; + gIigsRxRead = (uint16_t)(gIigsRxRead + n); return n; } @@ -303,6 +353,10 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t guard = 0u; while (!serialTxReady()) { + // Full-duplex: drain the RX FIFO while waiting for TX-ready. Each TX byte at 9600 is a + // ~1ms wait and the Z8530 RX FIFO is 3 bytes - an unpumped uplink mid-downlink-burst + // measurably ate 4-byte clumps out of arriving frames. + jlpSerialPoll(); guard++; if (guard == 0u) { // wrapped 65536 spins -- link stalled return n; @@ -310,5 +364,6 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { } *gData = buf[n]; } + jlpSerialPoll(); return n; } diff --git a/src/x68000/hal.c b/src/x68000/hal.c index 0ea6e52..0b4d451 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -211,9 +211,31 @@ void jlpShutdown(void) { if (!gModeSet) { return; } + // Restore Human68k's Timer C vector BEFORE dropping supervisor mode: the chained x68kTickIsr + // lives in this program's RAM, and leaving $114 pointing at it after exit sends the OS clock's + // next tick into freed memory (the dangling-ISR trap this port already fixed for the SCC RX + // vectors in jlpSerialClose). + if (gTickHooked) { + static volatile uintptr_t addr = X68K_TICK_VEC_ADDR; + volatile uint32_t *vec = (volatile uint32_t *)addr; + uint16_t sr; + + __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr)); + *vec = x68kTickChainAddr; + __asm__ volatile ("move.w %0,%%sr" :: "d" (sr)); + gTickHooked = false; + } if (gPrevCrtMode >= 0) { _iocs_crtmod(gPrevCrtMode); } + // Hand the mouse back the way the lines above hand back crtmod, the Timer + // C vector and user mode. jlpInputInit boxed the IOCS cursor tracker into + // the 320x200 stage with _MS_LIMIT, and that box lives in the IOCS work + // area, which outlives the program -- leaving it set would confine the + // next Human68k program's pointer to a corner of the screen. _MS_INIT + // re-establishes IOCS's own defaults, and it runs AFTER the crtmod restore + // above so those defaults match the screen the user is going back to. + _iocs_ms_init(); gModeSet = false; if (gPrevSsp >= 0) { (void)_dos_super(gPrevSsp); // back to user mode for Human68k @@ -457,10 +479,38 @@ void jlpInputInit(void) { } (void)_iocs_b_keyinp(); } + + // The mouse plugs into the keyboard and reaches the machine over + // SCC channel B, which IOCS owns end to end -- _MS_INIT (re)arms + // that path and zeroes its work area so boot-time motion never + // leaks into the first poll. This runs before jlSerialOpen can + // steal channel A's RX vectors, so the two SCC clients never touch + // the chip at the same time. + // + // Position comes from the IOCS cursor tracker (_MS_CURGT), not + // from _MS_GETDT deltas: the IPL 1.0 ROM's _MS_GETDT work area + // holds the LAST packet's displacement and is not cleared by + // reading (verified against the real ROM under MAME by + // verify-x68000-mouse.sh), so re-reading it between packets + // re-counts stale motion. The tracker integrates each packet + // exactly once at receive time and clamps to the _MS_LIMIT box, + // which is set to stage coordinates here -- no cursor is ever + // shown (_MS_CURON is never called), the tracker just does the + // bookkeeping. + _iocs_ms_init(); + _iocs_ms_limit(0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1); + _iocs_ms_curst(SURFACE_WIDTH / 2, SURFACE_HEIGHT / 2); + gMouseAttached = true; + gMouseX = SURFACE_WIDTH / 2; + gMouseY = SURFACE_HEIGHT / 2; } +// The _MS_LIMIT box is handed back in jlpShutdown instead of here: it has to +// be re-established AFTER the CRT mode is restored, and core's jlShutdown +// calls this first. void jlpInputShutdown(void) { + gMouseAttached = false; } @@ -514,6 +564,8 @@ void jlpInputPoll(void) { uint16_t key; uint16_t drain; uint8_t scan; + uint32_t mousePos; + uint32_t mouseButtons; for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { // Per-group, not per-poll: the whole bitsns trap storm can pass the ~2.8 ms the 64-byte RSDRV @@ -546,6 +598,25 @@ void jlpInputPoll(void) { } 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); + + mouseButtons = (uint32_t)_iocs_ms_getdt(); + gMouseButtonState[MOUSE_BUTTON_LEFT] = (mouseButtons & 0xFFu) != 0u; + gMouseButtonState[MOUSE_BUTTON_RIGHT] = ((mouseButtons >> 8) & 0xFFu) != 0u; + gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; } diff --git a/tools/xdftool.py b/tools/xdftool.py index 974581f..e386c18 100755 --- a/tools/xdftool.py +++ b/tools/xdftool.py @@ -134,8 +134,37 @@ class Xdf: # ----- File operations ----- + def find_in_subdir(self, dirname, leaf): + # Resolve SUB/LEAF: a subdirectory's data clusters are themselves a directory table. xdftool + # only ever creates one-cluster subdirs, but the guest may have grown one, so follow the chain. + _, _, de = self.find(dirname) + if de is None or not (de[11] & ATTR_DIR): + return None + want = self.encode_name(leaf).upper() + cluster = struct.unpack(" self.max_cluster: + return None + return None + def read_file(self, name): - _, _, e = self.find(name) + if "/" in name: # SAVES/RN.CFG: resolve through the subdirectory + sub, _, leaf = name.partition("/") + e = self.find_in_subdir(sub, leaf) + else: + _, _, e = self.find(name) if e is None: raise FileNotFoundError(f"{name} not in {self.path}") size = struct.unpack("