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