Mouse added to X68000. Many serial fixes.

This commit is contained in:
Scott Duensing 2026-08-15 01:54:20 -05:00
parent e1cae64e7b
commit 30c61609e7
26 changed files with 1210 additions and 88 deletions

5
cfg/default.cfg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0"?>
<!-- This file is autogenerated; comments and unknown tags will be stripped -->
<mameconfig version="10">
<system name="default" />
</mameconfig>

19
cfg/x68000.cfg Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- This file is autogenerated; comments and unknown tags will be stripped -->
<mameconfig version="10">
<system name="x68000">
<image_directories>
<device instance="floppydisk1" directory="/tmp/joey-x68gold.mQ0r7E/" />
<device instance="floppydisk2" directory="/tmp/joey-x68gold.mQ0r7E/" />
<device instance="floppydisk3" directory="" />
<device instance="floppydisk4" directory="" />
<device instance="sasihd" directory="" />
</image_directories>
<input>
<keyboard tag=":keyboard:x68k" enabled="1" />
</input>
<video>
<target index="0" view="Disk Drive and Keyboard LEDs" />
</video>
</system>
</mameconfig>

View file

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

View file

@ -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 <joey/serial.h>
// 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 (;;) {
}
}

View file

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

View file

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

View file

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

BIN
nvram/x68000_0/nvram Normal file

Binary file not shown.

View file

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

179
scripts/verify-iigs-serial.sh Executable file
View file

@ -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" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function report()
-- Probe mailbox, stamped into the SHR framebuffer (it never calls jlInit,
-- so nothing else writes here): signature, then one flag per step.
local base = 0xE12000
io.write(string.format("VERIFY-IIGS-SCC frame=%d sig=%02X%02X printerOpen=%d printerWrote=%d modemOpen=%d directWrote=%d\n",
frame,
mem:read_u8(base), mem:read_u8(base + 1),
mem:read_u8(base + 2), mem:read_u8(base + 3),
mem:read_u8(base + 4), mem:read_u8(base + 5)))
io.flush()
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("SCCPROBE") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{$readFrame, function() report(); manager.machine:exit() end},
}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= 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" </dev/null 2>&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)"

View file

@ -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=<dir with x68mame/> 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" <<LUA
local frame = 0
local mem = manager.machine.devices[":maincpu"].spaces["program"]
local frame = 0
local greenRun = 0
local exitAt = 0
local done = false
local FRAMES = $FRAMES
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
local EXIT_SETTLE = 900 -- keypress -> 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" </dev/null 2>&1 | grep '^GOLD'
-autoboot_script "$work/gold.lua" </dev/null 2>&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

227
scripts/verify-x68000-mouse.sh Executable file
View file

@ -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=<dir with x68mame/> 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" </dev/null 2>&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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -580,6 +580,7 @@ void jlpInputInit(void) {
gHooked = true;
gMousePresent = mouseInit();
gMouseAttached = gMousePresent;
gJoystickPresent = joystickInit();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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("<H", de[26:28])[0]
guard = 0
while 2 <= cluster < EOC_MIN:
base = self.cluster_offset(cluster)
for i in range(self.cluster_bytes // DIR_ENTRY_SIZE):
e = self.data[base + i * DIR_ENTRY_SIZE:base + (i + 1) * DIR_ENTRY_SIZE]
if e[0] == FREE_MARKER:
return None
if e[0] == DELETED_MARKER or (e[11] & (ATTR_VOLUME | ATTR_DIR)):
continue
if bytes(e[0:11]).upper() == want:
return e
cluster = self.fat_get(cluster)
guard += 1
if guard > 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("<I", e[28:32])[0]