diff --git a/LICENSES.md b/LICENSES.md
index a454076..4279090 100644
--- a/LICENSES.md
+++ b/LICENSES.md
@@ -215,9 +215,9 @@ the source of each before shipping anything that depends on them.
### Downloaded by the installer
-- **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, and `apple2gsr1`,
- ROM 01): Apple Inc. proprietary firmware, from the archive.org MAME 0.240
- split set. Component `iigsFirmware`.
+- **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, `apple2gsr1`, ROM 01,
+ and `a2ssc`, the Super Serial Card): Apple Inc. proprietary firmware, from
+ the archive.org MAME 0.240 split set. Component `iigsFirmware`.
- **Apple IIgs System 6.0.4** (`System.Disk`): Apple Inc. proprietary OS, from
the Apple II archive (). Component
`iigsFirmware`.
@@ -236,6 +236,11 @@ project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in
- **Amiga Kickstart 3.1 ROM**: Cloanto / Amiga Corp. proprietary, sold today
(Amiga Forever). Required by the Amiga test gates; `joey-run` falls back to
FS-UAE's AROS without it. Component `kickstart`.
+- **Amiga serial.device 37.4** (Workbench 3.1's `Devs/serial.device`):
+ Cloanto / Amiga Corp. proprietary, part of Workbench 3.1 (Amiga Forever).
+ Kickstart 3.1 has no serial.device in ROM, so `joey-run` and the Amiga
+ serial gate put this copy on the boot drive; `import` also finds it inside
+ a Workbench `.adf`. Component `amigaSerial`.
- **X68000 CG (font) ROM**: not part of Sharp's free release. MAME's x68000
driver needs it. Component `x68kCgrom`.
- **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original
diff --git a/Makefile b/Makefile
index 32b1146..135f4e9 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@
# make x68000 builds Sharp X68000 only
# make docs renders the manual to build/docs (HTML + PDF)
# make verify-audio runs every port's audio gate (needs emulators)
+# make verify-serial runs every port's serial loopback gate
# make clean removes all build outputs
#
# Cross tools come from ./install.sh, which records their paths in
@@ -43,7 +44,7 @@ ifeq ($(HAVE_X68000),1)
ALL_TARGETS += x68000
endif
-.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 blank-check tools clean help status verify-audio docs
+.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 blank-check tools clean help status verify-audio verify-serial docs
all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),)
@@ -92,7 +93,13 @@ docs:
# non-zero on purpose, so a run that tested nothing cannot look green.
# Takes a subset: make verify-audio PORTS="atarist amiga"
verify-audio:
- @$(REPO_DIR)/scripts/verify-audio-all.sh $(PORTS)
+ @$(REPO_DIR)/scripts/verify-all.sh audio $(PORTS)
+
+# The host check of the shared serial layer, then every port's loopback gate
+# (banner out, probe echoed back), sequentially, with the same exit codes.
+# Takes a subset: make verify-serial PORTS="host dos"
+verify-serial:
+ @$(REPO_DIR)/scripts/verify-all.sh serial $(PORTS)
clean:
rm -rf $(REPO_DIR)/build
@@ -110,5 +117,6 @@ help:
@echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)"
@echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)"
+ @echo " verify-serial (host check + all 5 serial loopback gates; PORTS= subset)"
@echo " docs (manual to build/docs; FORMAT=html for HTML only)"
@echo "Install toolchains first: ./install.sh install toolchains"
diff --git a/README.md b/README.md
index 44e7d89..9033366 100644
--- a/README.md
+++ b/README.md
@@ -65,9 +65,10 @@ the emulators and their firmware:
./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw
```
-Three files are never downloaded -- the Amiga Kickstart, the X68000 font ROM
-and Space Taxi's C64 memory image -- so supply your copies once with
-`./install.sh import PATH`. The manual's Getting Started chapter
+Four files are never downloaded -- the Amiga Kickstart, the Amiga
+`serial.device` from Workbench 3.1, the X68000 font ROM and Space Taxi's C64
+memory image -- so supply your copies once with `./install.sh import PATH`
+(files, folders, `.zip` archives or Amiga `.adf` disk images). The manual's Getting Started chapter
covers `joey-run`'s options and the private-file bundle.
@@ -386,13 +387,9 @@ re-indexing on the device.
Two blob formats:
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
- planar layout (Amiga plane-major; Atari ST row-major-with-planes-
- per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
- rejects files baked for the wrong target. There is no X68000 target:
- `assetbake.py` bakes only the four above, and the loader still expects
- the DOS chunky target byte there (`src/core/assetLoad.c`) even though
- that port's surfaces and tile ops are now planar -- so `.tbk` baking
- is not wired up for the X68000 yet.
+ planar layout (Amiga and X68000 plane-major; Atari ST row-major-with-
+ planes-per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
+ rejects files baked for the wrong target.
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky
and converts to planar at draw time, so the same blob serves every
@@ -401,7 +398,7 @@ Two blob formats:
Both formats embed an optional 16-entry `$0RGB` palette.
```
-tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs} in.png out.tbk
+tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
```
@@ -411,7 +408,8 @@ per target so `make ` produces baked blobs under
runtime tree at `build//.../DATA/`. `make/iigs.mk` runs no
`assetbake.py`: its `generated/iigs` tree is produced out-of-band and the
fragment only bakes `.spc` from the committed `.spr`. `make/x68000.mk`
-has no asset rules at all.
+builds only what Space Taxi loads there: its levels and pre-compiled cel
+banks.
For runtime-extracted content (sprites peeled out of a procedural
or captured surface), use `jlSpriteCreateFromSurface` -- the
diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc
index 22c2d10..995f5c0 100644
--- a/docs/ApiSerial.adoc
+++ b/docs/ApiSerial.adoc
@@ -19,15 +19,44 @@ the module entirely.
One byte-stream API spans every port while the HAL hides very different
hardware: the IIgs drives a Zilog 8530 SCC directly or a 6551 card in a slot;
DOS uses an interrupt-driven 8250/16550 UART; the Amiga uses `serial.device`;
-the ST goes through the BIOS Aux channel.
+the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS
+and receives on its own SCC interrupt.
The model is *non-blocking and buffered*. Received bytes accumulate in a
-background ring -- a hardware interrupt on DOS, the OS on Amiga and ST, or
-`jlSerialPoll` on the IIgs -- and reads never block. Call `jlSerialPoll` once
-per frame: it is a no-op where the interrupt or OS already fills the ring, and
-the hardware drain where it does not, so calling it every frame is always
+background ring -- a hardware interrupt on DOS, the X68000 and the IIgs, the
+OS on Amiga and ST -- and reads never block. Call `jlSerialPoll` once per
+frame: it is next to a no-op where the interrupt or OS already fills the ring,
+and the hardware drain where nothing does, so calling it every frame is always
correct.
+On the IIgs a Super Serial Card only interrupts with its interrupt switch
+(SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but
+loses what arrives while the program is busy. The card's 6551 holds a single
+received byte, about 1 ms at 9600 baud, which is less than the IIgs's own
+worst-case interrupt latency during a present: it runs loss-free at 2400 baud,
+and above that relies on the protocol to recover a dropped byte. The built-in
+modem and printer ports buffer three bytes and stay loss-free at 9600 through
+full-screen presents, joystick reads and sound effects. To get there, JoeyLib
+cuts its own long interrupts-off stretches into pieces of at most ~1.7 ms while
+a port is open: the present and the full-surface clear, the joystick read
+(~2.8 ms otherwise, with the stick at full deflection), and the sound-effect
+calls into the NinjaTrackerPlus replayer (~2.4 ms to start an effect, and a
+refill interrupt that tops up every streaming effect that is due at once). A
+full-screen present gets about 5% slower; a joystick read costs about what it
+does with no port open, up to ~0.8 ms more with the stick in a corner, and its
+reading can differ from the same stick's reading with no port open by a count.
+With no port open all of these run as before. Two stretches are not
+JoeyLib's to cut and can still cost bytes: GS/OS disk access (a 3.5-inch
+floppy holds interrupts off for 65 to 220 ms at a time), and starting a MOD
+song with `jlAudioPlayMod`, which loads its instruments into the sound chip
+with interrupts off, roughly 12 ms per KB of samples.
+
+On the Amiga the port is `serial.device`, which AmigaOS loads from `DEVS:`:
+every Workbench installation has it, but Kickstart 3.1 on its own does not, so
+a program booted without Workbench cannot open the port. `joey-run` puts the
+installer's copy (component `amigaSerial`) on the boot drive; a game shipped
+to boot on its own must do the same.
+
A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call
becomes a safe no-op, exactly like a failed audio init.
@@ -37,8 +66,8 @@ becomes a safe no-op, exactly like a failed audio init.
----
typedef enum {
JL_SERIAL_DEFAULT = 0, // the platform's natural RS-232 port
- JL_SERIAL_MODEM, // IIgs modem port (SCC channel A)
- JL_SERIAL_PRINTER, // IIgs printer port (SCC channel B)
+ JL_SERIAL_MODEM, // IIgs modem port (SCC channel B)
+ JL_SERIAL_PRINTER, // IIgs printer port (SCC channel A)
JL_SERIAL_SLOT // card/port selected by config->unit
} jlSerialDeviceE;
@@ -64,7 +93,19 @@ typedef struct {
The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM`
and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects
-the IIgs modem port, DOS COM1, the ST Modem port or Amiga unit 0.
+the IIgs modem port, DOS COM1, the ST Modem port, Amiga unit 0 or the X68000's
+RS-232C port.
+
+On the X68000 the line is set by the RS-232C routines in charge. With
+Human68k's RS-232C driver, RSDRV.SYS, loaded (the stock Human68k 3.02 system
+disk's CONFIG.SYS loads it, and so do JoeyLib's own X68000 boot disks), the
+port runs at 75 to 38400 bps and `JL_SERIAL_FLOW_RTSCTS` makes transmit wait
+for CTS; RTS stays up, since JoeyLib's receive interrupt, not the driver's,
+fills the receive ring. 38400 is really 39062 bps, 1.7% fast, within the
+usual tolerance of a UART at the far end. Without RSDRV.SYS the ROM's
+routines allow 75 to 9600 bps: a faster request clamps to 9600, and
+`JL_SERIAL_FLOW_RTSCTS` falls back to no handshaking. JoeyLib checks for the
+driver at every `jlSerialOpen`.
`unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super
Serial Card slot 1..7, a DOS COM number 1..4, or an Amiga `serial.device` unit.
@@ -96,7 +137,37 @@ platform has no serial support -- and then every other call no-ops.
void jlSerialClose(void);
----
-Closes the port and releases its resources. Safe when not open.
+Closes the port and releases its resources. Safe when not open. A port still
+open when the program exits is closed then: the first successful
+`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000 and
+the IIgs the port's receive interrupt vector points into the program until it
+is closed.
+
+Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so
+a modem call or a BBS caller survives the program handing the line back: a
+door returning to its BBS, or a terminal program exiting while the call stays
+up. RTS is not part of that promise; on the Amiga, `serial.device` puts back
+whatever RTS was before the open.
+
+To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than
+the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default),
+then close. The line stays down.
+
+[NOTE]
+====
+Two ports cannot keep the line perfectly steady, though neither gap is long
+enough to hang up a modem (a Hayes modem ignores DTR drops shorter than its
+S25 setting, 50 ms by default):
+
+* On the Amiga, when another program had the port open first, the last close
+ can drop DTR (`serial.device` puts back the lines as that program found
+ them), and JoeyLib raises it again straight away: the line is off for under
+ a millisecond. Otherwise JoeyLib raises DTR before its own open, so the
+ close keeps it up with no gap.
+* On the IIgs, opening a port resets its channel, which drops DTR briefly
+ even if it was already up: about a quarter of a millisecond on the modem
+ and printer ports, about a millisecond on a Super Serial Card.
+====
*Parameters:* none. *Returns:* nothing.
@@ -107,7 +178,9 @@ Closes the port and releases its resources. Safe when not open.
void jlSerialPoll(void);
----
-Pumps the receive and transmit buffers. Call once per game frame.
+Pumps the receive and transmit buffers. Call once per game frame. Where an
+interrupt fills the ring this does next to nothing; it is the drain for a port
+that cannot interrupt.
*Parameters:* none. *Returns:* nothing.
@@ -188,6 +261,47 @@ Discards any buffered received bytes.
*Parameters:* none. *Returns:* nothing.
+=== jlSerialSetDtr
+
+[source,c]
+----
+void jlSerialSetDtr(bool on);
+----
+
+Raises or drops DTR. `jlSerialOpen` raises DTR and `jlSerialClose` leaves it
+as it is, so this is how a program hangs up:
+
+[source,c]
+----
+jlSerialSetDtr(false); // hang up
+// ...wait longer than the modem's DTR-drop detection time (Hayes S25,
+// 50 ms by default)...
+jlSerialClose(); // DTR stays down
+----
+
+[cols="1,1,4", options="header"]
+|===
+|Parameter |Type |Description
+|`on` |`bool` |`true` raises DTR, `false` drops it.
+|===
+
+*Returns:* nothing. A closed port no-ops, so this can be driven
+unconditionally.
+
+Two ports tie DTR to more than the line:
+
+* On an IIgs Super Serial Card the 6551 has no DTR that is only a line:
+ dropping DTR also stops the card's receiver until DTR is raised again.
+* On the X68000 the line setup in the ROM and in RSDRV.SYS raises DTR
+ whenever it runs, so the next `jlSerialOpen` -- or another program setting
+ the line -- raises it again.
+
+Closing after a hang-up keeps DTR down, but on two ports it rises briefly on
+the way: on the Amiga for under a millisecond (`serial.device` puts back the
+lines as they were at its open) and on the X68000 for about 0.2 ms (close puts
+back the line settings, which raises DTR), before JoeyLib drops it again. A
+modem that has hung up ignores that.
+
=== jlSerialSetRts
[source,c]
diff --git a/docs/Assets.adoc b/docs/Assets.adoc
index febeba3..d707556 100644
--- a/docs/Assets.adoc
+++ b/docs/Assets.adoc
@@ -74,12 +74,12 @@ $ python3 tools/assetbake/assetbake.py --type tile --target atarist \
----
Tile banks are *per-target*, because the 8x8 block layout is the platform's
-native pixel format -- plane-major on Amiga, row-major-with-planes-per-row on
-ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
+native pixel format -- plane-major on Amiga and X68000, row-major-with-planes-
+per-row on ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
loader refuses a file baked for a different target rather than drawing garbage.
-`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos` or
-`iigs`.
+`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
+`iigs` or `x68000`.
==== Baking Sprites
diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc
index 1b906da..2f76b6c 100644
--- a/docs/GettingStarted.adoc
+++ b/docs/GettingStarted.adoc
@@ -88,7 +88,8 @@ such as FS-UAE without a Kickstart.
| `import PATH...`
| Installs the files that are not downloaded (see <>) from files, folders or `.zip` archives, matched by checksum.
+downloaded>>) from files, folders, `.zip` archives or Amiga `.adf` disk
+images, matched by checksum.
| `bundle FILE`
| Packs those files, as present on this machine, into `FILE` (a `.tar.gz`)
@@ -392,18 +393,22 @@ toolchain.
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST,
the Apple IIgs ROMs, GS/OS 6.0.4 and the CFFA2 card's firmware, and Sharp's own
-free release of the X68000 IPL-ROM and Human68k 3.02. Three files are not,
+free release of the X68000 IPL-ROM and Human68k 3.02. Four files are not,
because they were never released for free: the Amiga Kickstart 3.1 ROM, the
-X68000 CG (font) ROM, and the memory image of the original Space Taxi that its
-levels are extracted from. Supply your copies once:
+Amiga `serial.device` from Workbench 3.1 (Kickstart 3.1 has none in ROM, and
+a program cannot open the serial port without it), the X68000 CG (font) ROM,
+and the memory image of the original Space Taxi that its levels are extracted
+from. Supply your copies once:
[source,console]
----
$ ./install.sh import ~/my-roms
----
-`import` takes files, folders and `.zip` archives, identifies each file by its
-checksum (the list is `installer/files.sha1`) and puts it in place.
+`import` takes files, folders, `.zip` archives and Amiga `.adf` disk images
+(point it at your Workbench 3.1 disk for `serial.device`), identifies each
+file by its checksum (the list is `installer/files.sha1`) and puts it in
+place.
A project that hands its setup to testers can host these files instead:
`./install.sh bundle joeylib-private.tar.gz` packs the ones present on
diff --git a/examples/serial/serial.c b/examples/serial/serial.c
index ddd17e2..204f236 100644
--- a/examples/serial/serial.c
+++ b/examples/serial/serial.c
@@ -45,9 +45,24 @@
#define COLOR_LINK_OPEN 6
#define COLOR_LINK_SHUT 7
+// The line, overridable at build time (-DSERIAL_BAUD=19200u ...) so an emulator
+// gate can run the example on another line (scripts/verify-x68000-serial.sh,
+// X68K_SERIAL_LINE).
+#ifndef SERIAL_BAUD
#define SERIAL_BAUD 9600u
+#endif
+#ifndef SERIAL_DATABITS
#define SERIAL_DATABITS 8u
+#endif
+#ifndef SERIAL_STOPBITS
#define SERIAL_STOPBITS 1u
+#endif
+#ifndef SERIAL_PARITY
+#define SERIAL_PARITY JL_SERIAL_PARITY_NONE
+#endif
+#ifndef SERIAL_FLOW
+#define SERIAL_FLOW JL_SERIAL_FLOW_NONE
+#endif
#define RX_CHUNK 32
@@ -184,8 +199,8 @@ int main(void) {
serialCfg.baud = SERIAL_BAUD;
serialCfg.dataBits = SERIAL_DATABITS;
serialCfg.stopBits = SERIAL_STOPBITS;
- serialCfg.parity = JL_SERIAL_PARITY_NONE;
- serialCfg.flow = JL_SERIAL_FLOW_NONE;
+ serialCfg.parity = SERIAL_PARITY;
+ serialCfg.flow = SERIAL_FLOW;
serialCfg.unit = 0u;
linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg);
if (linkOpen) {
diff --git a/examples/serloss/serloss.c b/examples/serloss/serloss.c
new file mode 100644
index 0000000..a3ec9eb
--- /dev/null
+++ b/examples/serloss/serloss.c
@@ -0,0 +1,408 @@
+// Does a serial receiver survive the longest deaf window a JoeyLib program has?
+//
+// The host peer (scripts/verify-iigs-serial-loss.sh) waits for "READY" from
+// this program, then streams SERLOSS_BYTES bytes whose value is the low byte of
+// their index, paced by the emulated line at 9600 baud. Meanwhile every loop
+// iteration dirties the whole stage and presents it -- a full-screen present is
+// the longest stretch a program spends away from its own code -- and then reads
+// whatever the port buffered, calling jlSerialPoll once per frame as the API
+// asks. A receiver that fills its ring from an interrupt loses nothing; one that
+// is drained only when the program gets around to it loses the bytes that
+// arrive while the hardware FIFO is full.
+//
+// Build variants (the harness passes these with -D):
+// SERLOSS_SLOT=n open a Super Serial Card in slot n instead of the modem port
+// SERLOSS_CLEAR dirty the stage with jlSurfaceClear rather than jlFillRect
+// SERLOSS_IDLE no drawing at all: only the interrupts the system itself takes
+// SERLOSS_BAUD=n line rate (default 9600; the harness sets the peer to match)
+// SERLOSS_JOY also poll the input (joystick included) once per frame and
+// report the spread of the calibrated stick readings; the
+// harness holds the paddles still (SERLOSS_PADDLE), so any
+// spread is read jitter
+// SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot
+// effect once per frame, so NinjaTrackerPlus's entry calls and
+// its DOC refill interrupt run throughout the receive
+// SERLOSS_LOOSE drop the serial add-on's hold on the tight interrupt-latency
+// bound once the port is open: the negative control, which
+// must lose bytes wherever a masked stretch outlasts the FIFO
+//
+// Results go to a mailbox in the unused tail of the SHR SCB page, which the
+// present never writes and the harness reads straight out of emulated memory.
+
+#include
+#include
+
+#define MB_BASE ((volatile uint8_t *)0x00E19DD0L)
+#define MB_SIG_0 0
+#define MB_SIG_1 1
+#define MB_PHASE 2
+#define MB_RECEIVED 4
+#define MB_IN_SEQUENCE 6
+#define MB_BREAKS 8
+#define MB_PRESENTS 10
+#define MB_TICKS 12
+#define MB_OVERRUNS 14
+#define MB_DROPPED 16
+#define MB_SUM 18
+#define MB_NOW 20
+#define MB_FILL_TICKS 22
+#define MB_PRESENT_TICKS 24
+#define MB_JOY_POLLS 26
+#define MB_JOY_MIN_X 28 // stick readings, biased by MB_JOY_BIAS
+#define MB_JOY_MAX_X 29
+#define MB_JOY_MIN_Y 30
+#define MB_JOY_MAX_Y 31
+#define MB_SFX_SHOTS 32
+#define MB_SIG_VALUE_0 0xA5u
+#define MB_SIG_VALUE_1 0x5Au
+#define MB_JOY_BIAS 128
+
+#define PHASE_INIT 1u
+#define PHASE_OPEN 2u
+#define PHASE_READY 3u
+#define PHASE_RECEIVING 4u
+#define PHASE_DONE 5u
+#define PHASE_FAILED 0xEEu
+
+#ifndef SERLOSS_BAUD
+#define SERLOSS_BAUD 9600u
+#endif
+#define SERLOSS_BYTES 4096u
+#define IDLE_TICKS 120u // done once streaming has been silent for 2 s
+#define START_TICKS 3600u // give up if nothing arrives in 60 s
+#define READ_CHUNK 64u
+#define BENCH_FRAMES 60u // fills (+ presents) timed before the port opens
+#define SCREEN_W 320
+#define SCREEN_H 200
+
+// SERLOSS_SFX: a streamed effect on one slot plus a one-shot retriggered once
+// per frame on another. The stream rate sets how often the replayer's DOC
+// interrupt refills it (every 256 samples). The stream is raw (DOC-ready,
+// 0x00-free bytes) and filled by jlMemCopy, as Space Taxi's speech is, so
+// producing it costs the loop next to nothing.
+#define SFX_STREAM_SLOT 4u
+#define SFX_STREAM_RATE 22050u
+#define SFX_SHOT_SLOT 3u
+#define SFX_SHOT_RATE 11025u
+#define SFX_SHOT_BYTES 2048u
+#define SFX_SHOT_PERIOD 32u // one-shot square wave period, in samples
+#define SFX_LEVEL 96 // one-shot square amplitude
+#define SFX_WAVE_BYTES 256u // one period of the stream's sawtooth
+
+// Slot I/O select: bit n routes slot n's $Cn00 space and I/O to the card rather
+// than the built-in port (slot 2 defaults to the internal modem port, which
+// shadows a card's 6551 at $C0A8).
+#define SLTROMSEL ((volatile uint8_t *)0x00C02DL)
+
+extern uint16_t gIigsRxOverruns;
+extern uint16_t gIigsRxDropped;
+#ifdef SERLOSS_LOOSE
+extern void iigsIrqLatencyRelax(void);
+#endif
+
+#ifdef SERLOSS_JOY
+typedef struct {
+ uint16_t polls;
+ int8_t minX;
+ int8_t maxX;
+ int8_t minY;
+ int8_t maxY;
+} JoySpreadT;
+#endif
+
+#ifdef SERLOSS_SFX
+static int8_t gSfxShot[SFX_SHOT_BYTES];
+static uint8_t gSfxWave[SFX_WAVE_BYTES];
+#endif
+
+static uint16_t benchLoop(jlSurfaceT *stage, bool present);
+#ifdef SERLOSS_JOY
+static void joyPoll(JoySpreadT *spread);
+#endif
+static void mailbox16(uint16_t slot, uint16_t value);
+static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now);
+#ifdef SERLOSS_SFX
+static bool sfxStart(void);
+static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count);
+#endif
+
+
+// Ticks for BENCH_FRAMES full-stage fills, each followed by a present if asked: the difference
+// between the two runs is what a full-screen present costs.
+static uint16_t benchLoop(jlSurfaceT *stage, bool present) {
+ uint16_t start;
+ uint16_t frame;
+
+ start = jlFrameCount();
+ for (frame = 0u; frame < BENCH_FRAMES; frame++) {
+ jlFillRect(stage, 0, 0, SCREEN_W, SCREEN_H, (uint8_t)(frame & 0x0Fu));
+ if (present) {
+ jlStagePresent();
+ }
+ }
+ return (uint16_t)(jlFrameCount() - start);
+}
+
+
+#ifdef SERLOSS_JOY
+// One input poll (the joystick read is the long part), folded into the spread and published. The
+// stick was calibrated before the loop, so a still stick reads 0 and anything else is jitter.
+static void joyPoll(JoySpreadT *spread) {
+ int8_t x;
+ int8_t y;
+
+ jlInputPoll();
+ x = jlJoystickX(JOYSTICK_0);
+ y = jlJoystickY(JOYSTICK_0);
+ if (spread->polls == 0u || x < spread->minX) {
+ spread->minX = x;
+ }
+ if (spread->polls == 0u || x > spread->maxX) {
+ spread->maxX = x;
+ }
+ if (spread->polls == 0u || y < spread->minY) {
+ spread->minY = y;
+ }
+ if (spread->polls == 0u || y > spread->maxY) {
+ spread->maxY = y;
+ }
+ spread->polls++;
+ mailbox16(MB_JOY_POLLS, spread->polls);
+ MB_BASE[MB_JOY_MIN_X] = (uint8_t)(spread->minX + MB_JOY_BIAS);
+ MB_BASE[MB_JOY_MAX_X] = (uint8_t)(spread->maxX + MB_JOY_BIAS);
+ MB_BASE[MB_JOY_MIN_Y] = (uint8_t)(spread->minY + MB_JOY_BIAS);
+ MB_BASE[MB_JOY_MAX_Y] = (uint8_t)(spread->maxY + MB_JOY_BIAS);
+}
+#endif
+
+
+static void mailbox16(uint16_t slot, uint16_t value) {
+ MB_BASE[slot] = (uint8_t)(value & 0xFFu);
+ MB_BASE[slot + 1u] = (uint8_t)(value >> 8);
+}
+
+
+__attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now) {
+ mailbox16(MB_RECEIVED, received);
+ mailbox16(MB_IN_SEQUENCE, inSequence);
+ mailbox16(MB_BREAKS, breaks);
+ mailbox16(MB_PRESENTS, presents);
+ mailbox16(MB_TICKS, ticks);
+ mailbox16(MB_OVERRUNS, gIigsRxOverruns);
+ mailbox16(MB_DROPPED, gIigsRxDropped);
+ mailbox16(MB_SUM, sum);
+ mailbox16(MB_NOW, now);
+}
+
+
+#ifdef SERLOSS_SFX
+// Bring up audio, build the one-shot's square wave and the stream's sawtooth, and start the
+// endless stream.
+static bool sfxStart(void) {
+ uint16_t i;
+
+ if (!jlAudioInit()) {
+ return false;
+ }
+ for (i = 0u; i < SFX_SHOT_BYTES; i++) {
+ gSfxShot[i] = (int8_t)(((i / (SFX_SHOT_PERIOD / 2u)) & 1u) != 0u ? SFX_LEVEL : -SFX_LEVEL);
+ }
+ for (i = 0u; i < SFX_WAVE_BYTES; i++) {
+ gSfxWave[i] = (uint8_t)(i == 0u ? 1u : i); // 0x00 would stop the DOC
+ }
+ // The first trigger of a sample converts it for the DOC (~0.2 s for this one); do it before
+ // the port opens, so the loop's triggers are the cheap repeats and a slow first frame cannot
+ // overflow the receive ring.
+ jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);
+ jlAudioPlaySfxStreamRaw((uint8_t)SFX_STREAM_SLOT, sfxStreamFill, (void *)0, SFX_STREAM_RATE);
+ return true;
+}
+
+
+// Endless sawtooth, whole periods: never returns 0, so the stream runs until the program ends.
+static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) {
+ uint32_t done;
+
+ (void)ctx;
+ done = 0u;
+ while (count - done >= SFX_WAVE_BYTES) {
+ jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES);
+ done += SFX_WAVE_BYTES;
+ }
+ return done;
+}
+#endif
+
+
+int main(void) {
+ static const char ready[] = "READY\r\n";
+ jlConfigT config;
+ jlSerialConfigT cfg;
+ jlSurfaceT *stage;
+ uint8_t buf[READ_CHUNK];
+ uint16_t received;
+ uint16_t inSequence;
+ uint16_t breaks;
+ uint16_t presents;
+ uint16_t sum;
+ uint16_t lastTick;
+ uint16_t firstTick;
+ uint16_t now;
+ uint16_t got;
+ uint16_t i;
+ uint16_t n;
+ uint8_t expect;
+ uint8_t color;
+ bool started;
+#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
+ uint16_t frameTick;
+#endif
+#ifdef SERLOSS_JOY
+ JoySpreadT spread;
+#endif
+#ifdef SERLOSS_SFX
+ uint16_t shots;
+#endif
+
+ config.codegenBytes = 0u;
+ config.audioBytes = 0u;
+ if (!jlInit(&config)) {
+ MB_BASE[MB_PHASE] = PHASE_FAILED;
+ for (;;) {
+ }
+ }
+ // Signed only after jlInit, which rewrites the SCB page this mailbox shares.
+ MB_BASE[MB_SIG_0] = MB_SIG_VALUE_0;
+ MB_BASE[MB_SIG_1] = MB_SIG_VALUE_1;
+ MB_BASE[MB_PHASE] = PHASE_INIT;
+ mailbox16(MB_JOY_POLLS, 0u);
+ mailbox16(MB_SFX_SHOTS, 0u);
+ stage = jlStageGet();
+ mailbox16(MB_FILL_TICKS, benchLoop(stage, false));
+ mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true));
+#ifdef SERLOSS_SFX
+ if (!sfxStart()) {
+ MB_BASE[MB_PHASE] = PHASE_FAILED;
+ for (;;) {
+ }
+ }
+ shots = 0u;
+#endif
+
+ cfg.baud = SERLOSS_BAUD;
+ cfg.dataBits = 8u;
+ cfg.stopBits = 1u;
+ cfg.parity = JL_SERIAL_PARITY_NONE;
+ cfg.flow = JL_SERIAL_FLOW_NONE;
+#ifdef SERLOSS_SLOT
+ cfg.unit = SERLOSS_SLOT;
+ *SLTROMSEL = (uint8_t)(*SLTROMSEL | (uint8_t)(1u << SERLOSS_SLOT));
+ started = jlSerialOpen(JL_SERIAL_SLOT, &cfg);
+#else
+ cfg.unit = 0u;
+ started = jlSerialOpen(JL_SERIAL_MODEM, &cfg);
+#endif
+ if (!started) {
+ MB_BASE[MB_PHASE] = PHASE_FAILED;
+ for (;;) {
+ }
+ }
+#ifdef SERLOSS_LOOSE
+ iigsIrqLatencyRelax();
+#endif
+#ifdef SERLOSS_JOY
+ // Calibrate on the first poll, in the latency mode the rest will run in, so a still stick
+ // reads 0 from then on.
+ jlJoystickReset(JOYSTICK_0, 0u);
+ spread.polls = 0u;
+ joyPoll(&spread);
+#endif
+ MB_BASE[MB_PHASE] = PHASE_OPEN;
+ (void)jlSerialWrite((const uint8_t *)ready, (uint16_t)(sizeof(ready) - 1u));
+ MB_BASE[MB_PHASE] = PHASE_READY;
+
+ received = 0u;
+ inSequence = 0u;
+ breaks = 0u;
+ presents = 0u;
+ sum = 0u;
+ expect = 0u;
+ color = 1u;
+ started = false;
+ firstTick = jlFrameCount();
+ lastTick = firstTick;
+ now = firstTick;
+#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
+ frameTick = firstTick;
+#endif
+ for (;;) {
+ // Published at the top so every exit below is a bare compare-and-branch: llvm816 has
+ // been seen hoisting this function's byte splits between a cmp and its branch,
+ // clobbering the flags the branch reads (see the report).
+ publish(received, inSequence, breaks, presents, (uint16_t)(lastTick - firstTick), sum, now);
+ if (received >= SERLOSS_BYTES) {
+ break;
+ }
+#if defined(SERLOSS_CLEAR)
+ jlSurfaceClear(stage, color);
+ jlStagePresent();
+#elif !defined(SERLOSS_IDLE)
+ jlFillRect(stage, 0, 0, SCREEN_W, SCREEN_H, color);
+ jlStagePresent();
+#endif
+ presents++;
+ color = (uint8_t)((color & 0x0Fu) + 1u);
+#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
+ // Once per frame, like a game: an idle loop would otherwise run these back to back.
+ if (jlFrameCount() != frameTick) {
+ frameTick = jlFrameCount();
+#ifdef SERLOSS_JOY
+ joyPoll(&spread);
+#endif
+#ifdef SERLOSS_SFX
+ jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);
+ jlAudioFrameTick();
+ shots++;
+ mailbox16(MB_SFX_SHOTS, shots);
+#endif
+ }
+#endif
+ jlSerialPoll();
+ got = 0u;
+ n = jlSerialRead(buf, READ_CHUNK);
+ while (n > 0u) {
+ for (i = 0u; i < n; i++) {
+ if (buf[i] == expect) {
+ inSequence++;
+ } else {
+ breaks++;
+ }
+ expect = (uint8_t)(buf[i] + 1u);
+ sum = (uint16_t)(sum + buf[i]);
+ }
+ got = (uint16_t)(got + n);
+ n = jlSerialRead(buf, READ_CHUNK);
+ }
+ received = (uint16_t)(received + got);
+ now = jlFrameCount();
+ if (got > 0u) {
+ if (!started) {
+ started = true;
+ firstTick = now;
+ MB_BASE[MB_PHASE] = PHASE_RECEIVING;
+ }
+ lastTick = now;
+ }
+ if (started) {
+ if ((uint16_t)(now - lastTick) > IDLE_TICKS) {
+ break;
+ }
+ } else if ((uint16_t)(now - firstTick) > START_TICKS) {
+ break;
+ }
+ }
+ publish(received, inSequence, breaks, presents, (uint16_t)(lastTick - firstTick), sum, now);
+ MB_BASE[MB_PHASE] = PHASE_DONE;
+ for (;;) {
+ }
+}
diff --git a/examples/spacetaxi/stMarshalIigs.s b/examples/spacetaxi/stMarshalIigs.s
deleted file mode 100644
index ea291c5..0000000
--- a/examples/spacetaxi/stMarshalIigs.s
+++ /dev/null
@@ -1,354 +0,0 @@
-; stMarshalIigs.s - Space Taxi's stSimMarshal for the Apple IIgs.
-;
-; GAS syntax for the llvm-mos w65816 toolchain (see src/iigs/peislam.s
-; for the conventions). The C version copies eight sprites' shadow
-; registers into the displayed frame through far pointers, which the
-; w65816 backend turns into ~5000 cycles of pointer re-materialisation
-; per tick (5% of a ride frame). This is the same copy as straight-line
-; [dp],y moves: ~700 cycles.
-;
-; void stSimMarshalIigs(const StMarshalArgsT *args)
-; args (A = low word, X = bank) points at { spr, frame, multi }:
-; +0 const StSpriteT *spr (32-bit: low word, bank word)
-; +4 StFrameT *frame
-; +8 uint8_t multi
-; StSpriteT (6 bytes): col, msb, row, enable, ptr, color.
-; StFrameT: x[8] (u16, +0), y[8] (+16), ptr[8] (+24), color[8] (+32),
-; enableMask (+40), multiMask (+41).
-; stSim.c checks these layouts at compile time.
-;
-; Every store runs with M=16 and writes two bytes: the byte after each
-; single-byte field gets a junk high byte that the NEXT store repairs,
-; so the blocks run in order x, y, ptr, color and finish with the
-; enable/multi word. The last colour store's junk lands on enableMask,
-; which the final word overwrites.
-;
-; Direct-page $E0-$EF are the compiler's own per-function scratch
-; (re-set before every use, never live across a call); $C0 is left alone.
- .section .text.stSimMarshalIigs,"ax"
- .globl stSimMarshalIigs
-stSimMarshalIigs:
- php
- rep #0x30
- .a16
- .i16
- sta 0xe0 ; args low
- txa
- sta 0xe2 ; args bank
- ldy #0
- lda [0xe0],y
- sta 0xe4 ; spr low
- ldy #2
- lda [0xe0],y
- sta 0xe6 ; spr bank
- ldy #4
- lda [0xe0],y
- sta 0xe8 ; frame low
- ldy #6
- lda [0xe0],y
- sta 0xea ; frame bank
- ldy #8
- lda [0xe0],y
- and #0xff
- sta 0xec ; multi
- stz 0xee ; enable mask accumulator
-; sprite 0: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #0
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX0clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX0st
-mX0clr:
- lda 0xf0
- and #0x00ff
-mX0st:
- ldy #0
- sta [0xe8],y
- ldy #3
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE0off
- lda 0xee
- ora #1
- sta 0xee
-mE0off:
-; sprite 1: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #6
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX1clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX1st
-mX1clr:
- lda 0xf0
- and #0x00ff
-mX1st:
- ldy #2
- sta [0xe8],y
- ldy #9
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE1off
- lda 0xee
- ora #2
- sta 0xee
-mE1off:
-; sprite 2: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #12
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX2clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX2st
-mX2clr:
- lda 0xf0
- and #0x00ff
-mX2st:
- ldy #4
- sta [0xe8],y
- ldy #15
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE2off
- lda 0xee
- ora #4
- sta 0xee
-mE2off:
-; sprite 3: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #18
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX3clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX3st
-mX3clr:
- lda 0xf0
- and #0x00ff
-mX3st:
- ldy #6
- sta [0xe8],y
- ldy #21
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE3off
- lda 0xee
- ora #8
- sta 0xee
-mE3off:
-; sprite 4: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #24
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX4clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX4st
-mX4clr:
- lda 0xf0
- and #0x00ff
-mX4st:
- ldy #8
- sta [0xe8],y
- ldy #27
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE4off
- lda 0xee
- ora #16
- sta 0xee
-mE4off:
-; sprite 5: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #30
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX5clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX5st
-mX5clr:
- lda 0xf0
- and #0x00ff
-mX5st:
- ldy #10
- sta [0xe8],y
- ldy #33
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE5off
- lda 0xee
- ora #32
- sta 0xee
-mE5off:
-; sprite 6: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #36
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX6clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX6st
-mX6clr:
- lda 0xf0
- and #0x00ff
-mX6st:
- ldy #12
- sta [0xe8],y
- ldy #39
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE6off
- lda 0xee
- ora #64
- sta 0xee
-mE6off:
-; sprite 7: x = (msb ? 0x100 : 0) | col; enable bit
- ldy #42
- lda [0xe4],y ; col | msb<<8
- sta 0xf0
- and #0xff00
- beq mX7clr
- lda 0xf0
- and #0x00ff
- ora #0x0100
- bra mX7st
-mX7clr:
- lda 0xf0
- and #0x00ff
-mX7st:
- ldy #14
- sta [0xe8],y
- ldy #45
- lda [0xe4],y ; enable | ptr<<8
- and #0xff
- beq mE7off
- lda 0xee
- ora #128
- sta 0xee
-mE7off:
-; y[] block (16-bit stores; the junk high byte is repaired by the next store)
- ldy #2
- lda [0xe4],y
- ldy #16
- sta [0xe8],y
- ldy #8
- lda [0xe4],y
- ldy #17
- sta [0xe8],y
- ldy #14
- lda [0xe4],y
- ldy #18
- sta [0xe8],y
- ldy #20
- lda [0xe4],y
- ldy #19
- sta [0xe8],y
- ldy #26
- lda [0xe4],y
- ldy #20
- sta [0xe8],y
- ldy #32
- lda [0xe4],y
- ldy #21
- sta [0xe8],y
- ldy #38
- lda [0xe4],y
- ldy #22
- sta [0xe8],y
- ldy #44
- lda [0xe4],y
- ldy #23
- sta [0xe8],y
-; ptr[] block (16-bit stores; the junk high byte is repaired by the next store)
- ldy #4
- lda [0xe4],y
- ldy #24
- sta [0xe8],y
- ldy #10
- lda [0xe4],y
- ldy #25
- sta [0xe8],y
- ldy #16
- lda [0xe4],y
- ldy #26
- sta [0xe8],y
- ldy #22
- lda [0xe4],y
- ldy #27
- sta [0xe8],y
- ldy #28
- lda [0xe4],y
- ldy #28
- sta [0xe8],y
- ldy #34
- lda [0xe4],y
- ldy #29
- sta [0xe8],y
- ldy #40
- lda [0xe4],y
- ldy #30
- sta [0xe8],y
- ldy #46
- lda [0xe4],y
- ldy #31
- sta [0xe8],y
-; color[] block (16-bit stores; the junk high byte is repaired by the next store)
- ldy #5
- lda [0xe4],y
- ldy #32
- sta [0xe8],y
- ldy #11
- lda [0xe4],y
- ldy #33
- sta [0xe8],y
- ldy #17
- lda [0xe4],y
- ldy #34
- sta [0xe8],y
- ldy #23
- lda [0xe4],y
- ldy #35
- sta [0xe8],y
- ldy #29
- lda [0xe4],y
- ldy #36
- sta [0xe8],y
- ldy #35
- lda [0xe4],y
- ldy #37
- sta [0xe8],y
- ldy #41
- lda [0xe4],y
- ldy #38
- sta [0xe8],y
- ldy #47
- lda [0xe4],y
- ldy #39
- sta [0xe8],y
-; enableMask | multiMask<<8
- lda 0xec
- xba
- and #0xff00
- ora 0xee
- ldy #40
- sta [0xe8],y
- plp
- rtl
diff --git a/examples/spacetaxi/stSim.c b/examples/spacetaxi/stSim.c
index 3a6069a..de974ac 100644
--- a/examples/spacetaxi/stSim.c
+++ b/examples/spacetaxi/stSim.c
@@ -1489,31 +1489,7 @@ void stSimMarkChar(StSimT *sim, uint8_t ch) {
// $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC
// shows next. The title and level-intro loops flush the same way.
-#if defined(__W65816__)
-// The IIgs copies the frame in asm (stMarshalIigs.s); it takes one far
-// pointer to this block so no stack-argument convention is involved.
-typedef struct {
- const StSpriteT *spr;
- StFrameT *frame;
- uint8_t multi;
-} StMarshalArgsT;
-extern void stSimMarshalIigs(const StMarshalArgsT *args);
-// The asm hardcodes these layouts; a mismatch fails the build here.
-typedef char stMarshalCheckSprite[(sizeof(StSpriteT) == 6u && offsetof(StSpriteT, col) == 0u && offsetof(StSpriteT, msb) == 1u && offsetof(StSpriteT, row) == 2u && offsetof(StSpriteT, enable) == 3u && offsetof(StSpriteT, ptr) == 4u && offsetof(StSpriteT, color) == 5u) ? 1 : -1];
-typedef char stMarshalCheckFrame[(offsetof(StFrameT, x) == 0u && offsetof(StFrameT, y) == 16u && offsetof(StFrameT, ptr) == 24u && offsetof(StFrameT, color) == 32u && offsetof(StFrameT, enableMask) == 40u && offsetof(StFrameT, multiMask) == 41u) ? 1 : -1];
-typedef char stMarshalCheckArgs[(sizeof(void *) == 4u && offsetof(StMarshalArgsT, frame) == 4u && offsetof(StMarshalArgsT, multi) == 8u) ? 1 : -1];
-#endif
-
-
void stSimMarshal(StSimT *sim) {
-#if defined(__W65816__)
- static StMarshalArgsT args;
-
- args.spr = sim->spr;
- args.frame = &sim->frame;
- args.multi = sim->multiColorMask;
- stSimMarshalIigs(&args);
-#else
uint8_t i;
uint8_t enable = 0u;
@@ -1528,7 +1504,6 @@ void stSimMarshal(StSimT *sim) {
}
sim->frame.enableMask = enable;
sim->frame.multiMask = sim->multiColorMask;
-#endif
}
diff --git a/include/joey/platform.h b/include/joey/platform.h
index 92fcf19..749b207 100644
--- a/include/joey/platform.h
+++ b/include/joey/platform.h
@@ -170,6 +170,8 @@
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH
+ // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
+ #define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
@@ -243,6 +245,8 @@
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH
+ // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
+ #define JL_HAS_SERIAL_SET_DTR
// save files + disk space (CreateDir/DeleteFile/Info via dos.library)
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
@@ -313,6 +317,8 @@
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH
+ // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
+ #define JL_HAS_SERIAL_SET_DTR
// save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
@@ -361,6 +367,8 @@
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH
+ // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
+ #define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
@@ -427,6 +435,8 @@
#define JL_HAS_SERIAL_READ
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_FLUSH
+ // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
+ #define JL_HAS_SERIAL_SET_DTR
// Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what
// the portable JYM1 tracker rides, with no CPU mixing.
#define JL_HAS_AUDIO_INIT
diff --git a/include/joey/serial.h b/include/joey/serial.h
index b9224ad..c49ada7 100644
--- a/include/joey/serial.h
+++ b/include/joey/serial.h
@@ -7,15 +7,18 @@
// One byte-stream API spans every port; the per-port HAL hides the very
// different hardware:
// Apple IIgs -- direct Zilog 8530 SCC (modem/printer ports) or a 6551
-// Super Serial Card in a slot; polled, drained on
-// jlSerialPoll().
+// Super Serial Card in a slot; its own RX interrupt fills
+// the ring (a card needs its interrupt switch, SW2-6, on).
// DOS -- 8250/16550 UART on a COM port; interrupt-driven RX ring.
// Amiga -- serial.device (async exec IO); the OS buffers RX.
// Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX.
+// X68000 -- SCC channel A: IOCS for transmit and line setup (75-9600
+// bps, no RTS/CTS; 75-38400 bps and RTS/CTS with Human68k's
+// RSDRV.SYS loaded); its own RX interrupt fills the ring.
//
// Model: non-blocking + buffered. Received bytes accumulate in a background
-// ring (a hardware IRQ on DOS, the OS on Amiga/ST, or jlSerialPoll() on the
-// IIgs); jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll()
+// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST);
+// jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll()
// once per game frame -- it is a no-op where the IRQ/OS already fills the
// ring, and the hardware drain where it does not, so calling it every frame is
// always correct. Transmit is a bounded poll.
@@ -30,8 +33,8 @@
#include "types.h"
// Which physical interface to open. The IIgs is the oddball with three:
-// JL_SERIAL_MODEM -- IIgs modem port (SCC channel A). Elsewhere == DEFAULT.
-// JL_SERIAL_PRINTER -- IIgs printer port (SCC channel B). Only the IIgs has
+// JL_SERIAL_MODEM -- IIgs modem port (SCC channel B). Elsewhere == DEFAULT.
+// JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has
// a second built-in port; elsewhere == DEFAULT.
// JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs
// Super Serial Card slot (1..7); DOS COM number (1..4);
@@ -75,12 +78,23 @@ typedef struct {
bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
// Close the open port and release its resources. Safe to call when not open.
+// A port still open when the program exits is closed then (the first
+// successful jlSerialOpen registers an atexit handler): where the port's RX
+// interrupt is taken over (DOS, X68000, IIgs), the vector points into the
+// program until it is closed.
+//
+// Closing does NOT hang up: DTR is left exactly as it was, so a modem call or a
+// BBS caller survives a program handing the line back -- a door returning to
+// its BBS, a terminal exiting. RTS is not part of that promise (on the Amiga,
+// serial.device puts back what it found at open). To hang up, drop DTR with
+// jlSerialSetDtr(false), wait longer than the modem's DTR-drop detection time
+// (Hayes S25: 50 ms by default), then close: the line stays down.
void jlSerialClose(void);
-// Pump the RX/TX buffers. Call once per game frame. No-op where a hardware IRQ
-// or the OS already services the port (DOS/Amiga/ST); drains the hardware FIFO
-// into the RX ring where it does not (IIgs). Calling it every frame is always
-// correct.
+// Pump the RX/TX buffers. Call once per game frame. Next to a no-op where a
+// hardware IRQ or the OS already services the port; drains the hardware into
+// the RX ring where nothing else does (an IIgs Super Serial Card whose
+// interrupt switch is off). Calling it every frame is always correct.
void jlSerialPoll(void);
// Bytes waiting in the RX buffer, readable now without blocking.
@@ -101,6 +115,20 @@ int16_t jlSerialReadByte(void);
// Discard any buffered received bytes.
void jlSerialFlush(void);
+// RAISE OR DROP DTR -- how a program hangs up.
+//
+// jlSerialOpen raises DTR and jlSerialClose leaves it as it is, so this is the one call that drops
+// it. To hang up a modem: jlSerialSetDtr(false), wait longer than the modem's DTR-drop detection
+// time (Hayes S25: 50 ms by default), then jlSerialClose -- the line stays down. `on` = true raises
+// it again.
+//
+// Two ports tie DTR to more than the line. An IIgs Super Serial Card's 6551 has no separate DTR
+// bit: dropping DTR also stops the card's receiver until DTR is raised again. On the X68000 the
+// line setup in the ROM and in RSDRV.SYS raises DTR whenever it runs, so the next jlSerialOpen
+// (or anything else that sets the line) raises it again. A closed port no-ops, so a caller can
+// drive it unconditionally.
+void jlSerialSetDtr(bool on);
+
// ASSERT OR DROP RTS, for a caller that does its own receive-side flow control.
//
// JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying
diff --git a/installer/components/amigaSerial.sh b/installer/components/amigaSerial.sh
new file mode 100644
index 0000000..a3fc0e6
--- /dev/null
+++ b/installer/components/amigaSerial.sh
@@ -0,0 +1,34 @@
+# Amiga serial.device (37.4, from Workbench 3.1's Devs drawer). Kickstart 3.1
+# on the A500 has no serial.device in ROM: AmigaOS loads it from DEVS:, so a
+# JoeyLib program that opens the serial port needs it on the boot drive, and
+# joey-run and the Amiga serial gate put it there. Cloanto still sells
+# Workbench 3.1 (Amiga Forever), so it is never downloaded from a public
+# source: it comes from the private bundle or `install.sh import`, which also
+# finds it inside a Workbench .adf disk image.
+
+compDescription="Amiga firmware: serial.device from Workbench 3.1 (private: bundle or import)"
+compGroups="emulator:amiga"
+compRequires=""
+compHostDeps=""
+compPrivateFile="toolchains/firmware/amiga/Devs/serial.device"
+compPin="$(privateSha1For "$compPrivateFile")"
+
+
+compInstall() {
+ privateEnsure "$compPrivateFile"
+}
+
+
+compInstalled() {
+ privatePresent "$compPrivateFile"
+}
+
+
+compVerify() {
+ privatePresent "$compPrivateFile"
+}
+
+
+compEnv() {
+ envVar AMIGA_SERIAL_DEVICE "$JOEY_REPO/$compPrivateFile"
+}
diff --git a/installer/components/iigsFirmware.sh b/installer/components/iigsFirmware.sh
index bb0a9e8..b29a7b6 100644
--- a/installer/components/iigsFirmware.sh
+++ b/installer/components/iigsFirmware.sh
@@ -1,12 +1,13 @@
# Apple IIgs firmware and system software for MAME:
-# - the MAME apple2gs (ROM 03) and apple2gsr1 (ROM 01) ROM sets, from the
-# archive.org MAME 0.240 split set (the source llvm816 also uses)
+# - the MAME apple2gs (ROM 03) and apple2gsr1 (ROM 01) ROM sets, and a2ssc
+# (the Super Serial Card firmware the serial gates' slot card needs), from
+# the archive.org MAME 0.240 split set (the source llvm816 also uses)
# - the CFFA2 card firmware, freely redistributed by dreher.net, needed
# to boot images larger than an 800 KB floppy
# - the GS/OS 6.0.4 System.Disk, from the Apple II archive
# Everything lands in toolchains/firmware/iigs.
-compDescription="IIgs firmware: MAME ROM sets (ROM 01 + 03), CFFA2 firmware, GS/OS 6.0.4"
+compDescription="IIgs firmware: MAME ROM sets (ROM 01 + 03, Super Serial Card), CFFA2 firmware, GS/OS 6.0.4"
compGroups="emulator:iigs"
compRequires="host"
compHostDeps="unzip"
@@ -19,6 +20,7 @@ fwDir="$JOEY_FIRMWARE/iigs"
romSets=(
"apple2gs.zip eddce4a9f9f6e73ffed06c5aede6fbcdaa01dda5568b81f90d865858a3d7d9bc"
"apple2gsr1.zip 1c16fec7b9e785c4118909e409e61dcc38d0921f774b8708bae176bc5eb27307"
+ "a2ssc.zip 2e46808466d578006d9e12f86af8e831aa4dffb554149e184c044a1d14ed8a20"
)
cffaSha256="48242b9e2e87d87bfef1f1b1d64d1091bbfca182d745c1d031690fac0f80c0fe"
gsosSha256="e42a4f1f50c1aae0cc17dc555415a2fbf21dc0b7b6b3e33e0d73bf22ef97506a"
@@ -44,12 +46,22 @@ compInstall() {
compInstalled() {
- [ -f "$fwDir/roms/apple2gs.zip" ] && [ -f "$fwDir/roms/apple2gsr1.zip" ] &&
- [ -f "$fwDir/roms/a2cffa2/cffa20eec02.bin" ] && [ -f "$fwDir/gsos-system.po" ]
+ local entry
+ for entry in "${romSets[@]}"; do
+ [ -f "$fwDir/roms/${entry%% *}" ] || return 1
+ done
+ [ -f "$fwDir/roms/a2cffa2/cffa20eec02.bin" ] && [ -f "$fwDir/gsos-system.po" ]
}
compVerify() {
+ local entry
+ local name
+ local sha
+ for entry in "${romSets[@]}"; do
+ read -r name sha <<< "$entry"
+ [ "$(sha256Of "$fwDir/roms/$name")" = "$sha" ] || return 1
+ done
[ "$(sha1sum "$fwDir/roms/a2cffa2/cffa20eec02.bin" | cut -d' ' -f1)" = "080ff88f19de22328e162954ee2b51ee65f9d5cd" ] &&
[ "$(sha1sum "$fwDir/gsos-system.po" | cut -d' ' -f1)" = "a57940a0b724fc7dc545236a4f9d4c6dd68f3e81" ]
}
diff --git a/installer/files.sha1 b/installer/files.sha1
index 65a311e..1dab3fc 100644
--- a/installer/files.sha1
+++ b/installer/files.sha1
@@ -5,5 +5,6 @@
#
# sha1 destination (repository-relative) what it is
3b7f1493b27e212830f989f26ca76c02049f09ca toolchains/firmware/amiga/kickstart.rom Amiga Kickstart 3.1 r40.63 (A500/A600/A2000)
+d814d6ce8efb4a87966dae581eddf4dca528094a toolchains/firmware/amiga/Devs/serial.device Amiga serial.device 37.4 (Workbench 3.1 Devs/)
8d72c5b4d63bb14c5dbdac495244d659aa1498b6 toolchains/firmware/x68000/roms/x68000/cgrom.dat X68000 character-generator (font) ROM
5bfa4ba70431135575dab610a73dde02b8095953 stuff/spacetaxi/raw.bin Space Taxi C64 memory image (level source)
diff --git a/installer/lib/bundle.sh b/installer/lib/bundle.sh
index 422caea..bd0c177 100644
--- a/installer/lib/bundle.sh
+++ b/installer/lib/bundle.sh
@@ -43,8 +43,10 @@ importOneFile() {
}
-# importPaths PATH... -- scan files, directories and .zip archives and
-# install every private file found. Prints what was installed.
+# importPaths PATH... -- scan files, directories, .zip archives and Amiga
+# .adf disk images and install every private file found. Prints what was
+# installed. (Workbench's own disk is how most people own AmigaOS files
+# such as Devs/serial.device.)
importPaths() {
local path
local file
@@ -63,6 +65,15 @@ importPaths() {
done
fi
;;
+ *.adf|*.ADF)
+ rm -rf "$tmp/adf"
+ mkdir -p "$tmp/adf"
+ if python3 "$JOEY_REPO/tools/adfTool.py" extractall "$file" "$tmp/adf" 2>/dev/null; then
+ find "$tmp/adf" -type f -print0 | while IFS= read -r -d '' inner; do
+ importOneFile "$inner"
+ done
+ fi
+ ;;
*)
importOneFile "$file"
;;
diff --git a/installer/patches/mame/0002-z80scc-rx-fifo.patch b/installer/patches/mame/0002-z80scc-rx-fifo.patch
index abc2cda..5cb1a6b 100644
--- a/installer/patches/mame/0002-z80scc-rx-fifo.patch
+++ b/installer/patches/mame/0002-z80scc-rx-fifo.patch
@@ -1,24 +1,37 @@
--- a/src/devices/machine/z80scc.cpp
+++ b/src/devices/machine/z80scc.cpp
-@@ -1045,7 +1045,7 @@
+@@ -1045,7 +1045,18 @@
m_uart->m_wr0_ptrbits = 0;
- m_rx_fifo_sz = (m_uart->m_variant & z80scc_device::SET_ESCC) ? 8 : 3;
-+ m_rx_fifo_sz = 512; // RetroNet patch: see z80scc.h - emulation-side depth for polled receivers
++ // RetroNet patch: see z80scc.h. JOEY_Z80SCC_RX_FIFO=N sets the depth, clamped to 3..512
++ // (3 = real Z8530; an ESCC keeps at least its real 8). Unset, it is the whole 512.
++ m_rx_fifo_sz = std::size(m_rx_data_fifo);
++ if (char const *const depth = osd_getenv("JOEY_Z80SCC_RX_FIFO"); depth && *depth)
++ {
++ m_rx_fifo_sz = std::clamp(atoi(depth), 3, m_rx_fifo_sz);
++ if (m_uart->m_variant & z80scc_device::SET_ESCC)
++ {
++ m_rx_fifo_sz = std::max(m_rx_fifo_sz, 8);
++ }
++ osd_printf_info("%s: RX FIFO depth %d (JOEY_Z80SCC_RX_FIFO)\n", tag(), m_rx_fifo_sz);
++ }
m_tx_fifo_sz = (m_uart->m_variant & z80scc_device::SET_ESCC) ? 4 : 1;
--- a/src/devices/machine/z80scc.h
+++ b/src/devices/machine/z80scc.h
-@@ -256,8 +256,10 @@
+@@ -256,8 +256,12 @@
unsigned int m_delayed_tx_brg_change;
// receiver state
- uint8_t m_rx_data_fifo[8]; // receive data FIFO
- uint8_t m_rx_error_fifo[8]; // receive error FIFO
+ // RetroNet patch: widened from 8 so a POLLED receiver (no RX interrupt) survives its deaf
-+ // windows under emulation; real silicon depth is 3/8 and real clients need interrupt RX.
++ // windows under emulation. The depth in use is m_rx_fifo_sz: all 512 unless the
++ // JOEY_Z80SCC_RX_FIFO environment variable sets it (3 = real silicon, where clients need
++ // interrupt RX). See device_start().
+ uint8_t m_rx_data_fifo[512]; // receive data FIFO
+ uint8_t m_rx_error_fifo[512]; // receive error FIFO
uint8_t m_rx_error; // current receive error
diff --git a/installer/patches/mame/README.md b/installer/patches/mame/README.md
index ca7733a..4e5c469 100644
--- a/installer/patches/mame/README.md
+++ b/installer/patches/mame/README.md
@@ -7,10 +7,32 @@ drivers. The gates depend on every one of them.
| Patch | What it fixes | Needed by |
|---|---|---|
| 0001-apple2gs-scc-clock | The IIgs SCC clock, so serial RX works at rates other than 230400 baud | `verify-iigs-serial.sh` |
-| 0002-z80scc-rx-fifo | Widens the Z8530 receive FIFO from 8 to 512 bytes, so a POLLED receiver (no RX interrupt) survives the emulator's deaf windows. Real silicon holds 3 bytes; real clients need interrupt-driven RX. Originally written for RetroNet. | IIgs serial gates |
+| 0002-z80scc-rx-fifo | Makes the Z8530 receive FIFO depth selectable at run time (see below). By default it is 512 bytes, so a POLLED receiver (no RX interrupt) survives the emulator's deaf windows; `JOEY_Z80SCC_RX_FIFO=3` restores the real chip's depth. Originally written for RetroNet. | IIgs serial gates (512); `verify-iigs-serial-loss.sh` (3) |
| 0003-x68k-crtc-timing | The X68000 CRTC: the graphic high-speed clear's timing (the main defect), and the text raster copy firing on both hsync edges instead of only the front porch | X68000 gates and timing |
| 0004-x68k-waitstates-rs232-mouse | Three X68000 changes that edit the same files: VRAM wait states (MAME modelled none), the RS-232C port wired to a slot so `-rs232c null_modem` works, and a backport of upstream commit ab6b6fc9e797 (Patrick Mackinlay, "x68k: add serial mouse"), which provides the `:mouse_port:x68k:X/Y/BTN` inputs | `verify-x68000-serial.sh`, `verify-x68000-mouse.sh`, X68000 timing |
+## Receive FIFO depth (0002)
+
+Real Z8530 silicon holds 3 received bytes (an ESCC, 8), so a real client
+needs interrupt-driven receive. Under emulation a polled receiver sees deaf
+windows no real machine has, so by default the patched MAME gives every
+Z8530 channel (IIgs and X68000 alike) a 512-byte receive FIFO. The
+environment variable `JOEY_Z80SCC_RX_FIFO` overrides that for one run:
+
+| `JOEY_Z80SCC_RX_FIFO` | Receive FIFO depth |
+|---|---|
+| unset or empty | 512 (the default the gates rely on) |
+| `3` | the real Z8530 (stock MAME's value); an ESCC keeps its real 8 |
+| `N` | N, clamped to 3..512; an ESCC never gets fewer than 8 |
+
+MAME's ring of size N holds N-1 unread bytes, so 3 overruns on the third
+unread byte, one byte earlier than silicon (whose 3-byte FIFO fills first):
+a slightly stricter test than the real chip. When the variable is set, MAME
+prints `: RX FIFO depth N (JOEY_Z80SCC_RX_FIFO)` for each
+channel at start-up, so a log shows the depth really was applied.
+`scripts/verify-iigs-serial-loss.sh` runs with 3, because a 512-byte FIFO
+would hide exactly the receive loss that gate looks for.
+
## Why the X68000 changes are one patch
They were first kept as three separate patches, but the upstream mouse
diff --git a/joey-run b/joey-run
index 8f2400b..99bd343 100755
--- a/joey-run
+++ b/joey-run
@@ -109,53 +109,6 @@ magicPort() {
}
-# exampleProgram NAME PORT -- the built file for an example name.
-exampleProgram() {
- local name="$1"
- local bin="$repo/build/$2/bin"
- local up="${name^^}"
- local cap
- local candidate
- case "$2" in
- iigs)
- printf '%s\n' "$bin/$up"
- ;;
- amiga)
- cap="${name,,}"
- case "$cap" in
- staxi|spacetaxi) cap=taxi ;;
- esac
- cap="${cap^}"
- for candidate in "$bin/$cap/$cap" "$bin/$cap"; do
- if [ -f "$candidate" ]; then
- printf '%s\n' "$candidate"
- return 0
- fi
- done
- printf '%s\n' "$bin/$cap"
- ;;
- atarist|dos)
- [ "$up" = "SPACETAXI" ] && up=STAXI
- local ext=".PRG"
- [ "$2" = "dos" ] && ext=".EXE"
- for candidate in "$bin/$up/$up$ext" "$bin/$up$ext"; do
- if [ -f "$candidate" ]; then
- printf '%s\n' "$candidate"
- return 0
- fi
- done
- printf '%s\n' "$bin/$up$ext"
- ;;
- x68000)
- printf '%s\n' "$bin/$up.X"
- ;;
- *)
- fail "unknown port '$2' (iigs, amiga, atarist, dos or x68000)"
- ;;
- esac
-}
-
-
# folderProgram DIR -- the one program file inside an app folder.
folderProgram() {
local file
@@ -350,6 +303,15 @@ runAmiga() {
stageFolder "$hd"
mkdir -p "$hd/s"
printf ':%s\n' "$name" > "$hd/s/startup-sequence"
+ # Kickstart 3.1 has no serial.device in ROM; AmigaOS loads it from
+ # DEVS: (SYS:Devs), so a program that opens the serial port needs it on
+ # this boot drive. Harmless for everything else.
+ if [ -n "${AMIGA_SERIAL_DEVICE:-}" ] && [ -f "$AMIGA_SERIAL_DEVICE" ]; then
+ mkdir -p "$hd/Devs"
+ cp "$AMIGA_SERIAL_DEVICE" "$hd/Devs/serial.device"
+ else
+ echo "joey-run: no serial.device installed (./install.sh install amigaSerial); serial will not open on the Amiga" >&2
+ fi
args=(--amiga_model=A500 --fast_memory=2048
--hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB
--automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1
@@ -428,28 +390,18 @@ runDos() {
runX68000() {
local name
- local xdf="$repo/tools/xdftool.py"
local stage="$work/files"
- local f
- local rel
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+ # shellcheck source=scripts/lib/x68kDisk.sh
+ source "$repo/scripts/lib/x68kDisk.sh"
name="$(basename "$program")"
# Human68k boots from a trimmed copy of the system disk whose
# AUTOEXEC.BAT runs the program from the data disk in drive B.
- printf 'B:\r\nB:\\%s\r\n\x1a' "$name" > "$work/AUTOEXEC.BAT"
- cp "$X68K_DIR/HUMAN302.XDF" "$work/boot.xdf"
- cp "$X68K_DIR/HUMAN302.XDF" "$work/data.xdf"
- python3 "$xdf" empty "$work/data.xdf" >/dev/null
- for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
- python3 "$xdf" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true
- done
- for f in SYS HIS BIN BASIC2 ASK ETC; do
- python3 "$xdf" deltree "$work/boot.xdf" "$f" >/dev/null 2>&1 || true
- done
- python3 "$xdf" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
- python3 "$xdf" add "$work/data.xdf" "$program" "$name" >/dev/null
+ x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$name" || fail "cannot build the boot disk"
+ x68kDataDisk "$work/data.xdf" || fail "cannot build the data disk"
+ x68kXdf add "$work/data.xdf" "$program" "$name" >/dev/null || fail "cannot copy $name onto the data disk"
rm -rf "$stage"
mkdir -p "$stage"
if [ -d "$(dirname "$program")/DATA" ]; then
@@ -460,10 +412,7 @@ runX68000() {
cp -r "$dataDir/." "$stage/DATA/"
fi
if [ -d "$stage/DATA" ]; then
- while IFS= read -r f; do
- rel="${f#"$stage"/}"
- python3 "$xdf" add "$work/data.xdf" "$f" "${rel^^}" >/dev/null
- done < <(find "$stage/DATA" -type f | sort)
+ x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk"
fi
echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop."
runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf"
@@ -519,7 +468,7 @@ if [ -d "$program" ]; then
program="$(folderProgram "$program")"
elif [ ! -e "$program" ]; then
[ -n "$port" ] || fail "$program is not a file; for an example name add --port"
- program="$(exampleProgram "$program" "$port")"
+ program="$(joeyExampleProgram "$program" "$port")" || exit 1
[ -e "$program" ] || fail "$program is not built (make $port)"
fi
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"
diff --git a/make/iigs.mk b/make/iigs.mk
index c0ef10c..d636b31 100644
--- a/make/iigs.mk
+++ b/make/iigs.mk
@@ -62,6 +62,7 @@ KEYS_SRC := $(EXAMPLES)/keys/keys.c
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c
+SERLOSS_SRC := $(EXAMPLES)/serloss/serloss.c
SAVE_SRC := $(EXAMPLES)/save/save.c
JOY_SRC := $(EXAMPLES)/joy/joy.c
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
@@ -69,17 +70,19 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
-STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c $(EXAMPLES)/spacetaxi/stMarshalIigs.s
+STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
# binary that ships next to the program; jlpAudioInit loads "ntpplayer" from
-# disk at run time (see the $(BINDIR)/AUDIO rule).
+# disk at run time (see the $(BINDIR)/AUDIO rule). The source is assembled
+# with JoeyLib's interrupt window added to NTPstreamsound (NTP_PATCH).
NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s
+NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
IIGS_MERLIN := $(IIGS_AS)
-.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean
+.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail clean-iigs clean
# Default: build (compile-check) the library. The MAME smoke test is
# `iigs-clang-smoke`; disks and examples are `iigs-disk`.
@@ -222,6 +225,19 @@ iigs-verify: iigs-disk
iigs-verify-serial: $(BINDIR)/SCCPROBE
$(REPO_DIR)/scripts/verify-iigs-serial.sh
+# Receive-loss gate: 4096 bytes streamed at 9600 baud while the program does
+# nothing but full-screen presents must all arrive. Runs the modem port (SCC)
+# and a Super Serial Card in slot 2 (6551) on the installed MAME, with the SCC
+# receive FIFO cut to the real chip's 3 bytes (JOEY_Z80SCC_RX_FIFO=3) and the
+# card's a2ssc firmware from iigsFirmware; see the script header. ~3 min per mode.
+# The card runs at SERLOSS_SSC_BAUD: its 6551 holds ONE received byte, so it is
+# lossless through full-screen presents only up to 2400 (docs/ApiSerial.adoc);
+# at 9600 this half could never pass.
+SERLOSS_SSC_BAUD := 2400
+iigs-verify-serial-loss: $(BINDIR)/SERLOSS $(BINDIR)/SERLOSSS
+ $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh scc
+ SERLOSS_BAUD=$(SERLOSS_SSC_BAUD) $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh ssc
+
# Boot-verify every launchable example on joey.2mg (per-example thresholds).
iigs-verify-all: iigs-disk
$(REPO_DIR)/scripts/verify-iigs-all.sh
@@ -259,9 +275,9 @@ iigs-verify-shrtail: $(LIB) $(NTP_BIN)
iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW
$(REPO_DIR)/scripts/verify-iigs-save.sh
-$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
+$(NTP_BIN): $(NTP_SRC) $(NTP_PATCH) $(IIGS_MERLIN)
@mkdir -p $(BUILD)/audio
- @cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
+ @python3 $(NTP_PATCH) $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@
@@ -331,6 +347,15 @@ $(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)
+# Receive-loss probes for iigs-verify-serial-loss: modem port and slot-2 SSC.
+$(BINDIR)/SERLOSS: $(SERLOSS_SRC) $(LIB) $(IIGS_CLANG_BUILD)
+ @mkdir -p $(dir $@) $(DEP_DIR)
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERLOSS.d $(INCLUDES) -o $@ $(SERLOSS_SRC) $(LIB)
+
+$(BINDIR)/SERLOSSS: $(SERLOSS_SRC) $(LIB) $(IIGS_CLANG_BUILD)
+ @mkdir -p $(dir $@) $(DEP_DIR)
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERLOSSS.d -DSERLOSS_SLOT=2 -DSERLOSS_BAUD=$(SERLOSS_SSC_BAUD)u $(INCLUDES) -o $@ $(SERLOSS_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/support/iigs/ntpIrqWindow.py b/make/support/iigs/ntpIrqWindow.py
new file mode 100755
index 0000000..0b7d680
--- /dev/null
+++ b/make/support/iigs/ntpIrqWindow.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+# ntpIrqWindow.py SRC DST - bound NinjaTrackerPlus's interrupts-off stretches.
+#
+# NinjaTrackerPlus moves sample data into the DOC with interrupts masked,
+# ~4 us a byte (measured under MAME), and the Z8530 SCC's receive FIFO only
+# lasts ~3.1 ms at 9600 baud (~2.3 ms under MAME). Two stretches outlast it:
+# - NTPstreamsound fills a stream's 512-byte DOC page with two unrolled
+# 256-byte copies under its own SEI: ~2.4 ms. stream_window goes between
+# the halves: pending interrupts run there, then the DOC's mode and
+# address are put back for the second half. The stream's oscillators are
+# already halted, and its job is cleared first, so a refill interrupt left
+# pending by the slot's previous sound cannot stream into the page.
+# - The DOC interrupt handler refills every streaming oscillator that is
+# due before it returns, 256 bytes each: ~1 ms apiece, ~2.5 ms for two.
+# It returns after one instead; the DOC keeps its IRQ up for the rest, so
+# the handler is re-entered at once, after anything the ROM's interrupt
+# manager puts first (the SCC is on its fast path).
+# Both only while the word at offset 27 of the replayer (irq_window, right
+# after the documented jump table) is nonzero. audio_full.c sets it from
+# hal.c's tight interrupt-latency bound before every NTPstreamsound, so with
+# the bound loose the replayer does exactly what it always did.
+#
+# The source is the installer's pinned ntpsources.zip (checksummed), so
+# every anchor below is exact; a miss means NTP changed and this script
+# must be revisited, so it fails the build instead of guessing.
+import sys
+
+TABLE_END = """ jmp streamsound
+"""
+TABLE_END_NEW = """ jmp streamsound
+
+; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off
+; stretches -- see stream_window and interrupt_handler_exit.
+irq_window dw 0
+"""
+FILL = """ ldy streamsound_osc2
+ jsr stream_copy ;fill buffer with 512 bytes for first play
+ jsr stream_copy_no_init
+"""
+FILL_NEW = """ ldy streamsound_osc2
+ jsr stream_copy ;fill buffer with 512 bytes for first play
+ jsr stream_window ;JoeyLib: interrupts in between the halves, if asked
+ jsr stream_copy_no_init
+"""
+COPY_HELPERS = """ mx %10
+streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
+"""
+COPY_HELPERS_NEW = """; stream_window (JoeyLib): between the two halves of streamsound's first
+; DOC fill, when irq_window is nonzero, let pending interrupts run, then
+; point the DOC back at the second half (a handler may have used it).
+; In and out: mx %00, Y = the stream's track * 2.
+ mx %00
+stream_window = *
+ lda irq_window
+ beq stream_window_1
+ sep #$20
+ mx %10
+ ldx streamsound_osc ;no refill job left pending from the slot's last sound
+ stz oscillator_job_tbl,x
+ cli
+ nop ;a pending interrupt is taken here
+ sei
+ jsr access_doc_ram
+ rep #$20
+ mx %00
+ lda stream_doc_page,y ;the page's second half
+ inc
+ xba
+ sta sound_address
+stream_window_1 = *
+ rts
+
+ mx %10
+streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
+"""
+HANDLER_JOB = """ ldx interrupt_job
+ jsr (interrupt_jump_tbl,x)
+
+]loop lda sound_control
+"""
+HANDLER_JOB_NEW = """ ldx interrupt_job
+ jsr (interrupt_jump_tbl,x)
+
+ lda irq_window ;JoeyLib: one oscillator an interrupt when asked;
+ bne interrupt_handler_exit ;the DOC keeps its IRQ up for any others
+]loop lda sound_control
+"""
+HANDLER_END = """ bpl interrupt_handler_loop
+
+ sep #$30
+"""
+HANDLER_END_NEW = """ bpl interrupt_handler_loop
+
+interrupt_handler_exit = *
+ sep #$30
+"""
+
+
+def patchOnce(text, old, new):
+ if text.count(old) != 1:
+ sys.exit("ntpIrqWindow: anchor not found exactly once -- NinjaTrackerPlus changed:\n" + old)
+ return text.replace(old, new)
+
+
+def main():
+ if len(sys.argv) != 3:
+ sys.exit("usage: ntpIrqWindow.py SRC DST")
+ with open(sys.argv[1], "r", encoding="ascii") as f:
+ text = f.read()
+ text = patchOnce(text, TABLE_END, TABLE_END_NEW)
+ text = patchOnce(text, FILL, FILL_NEW)
+ text = patchOnce(text, COPY_HELPERS, COPY_HELPERS_NEW)
+ text = patchOnce(text, HANDLER_JOB, HANDLER_JOB_NEW)
+ text = patchOnce(text, HANDLER_END, HANDLER_END_NEW)
+ with open(sys.argv[2], "w", encoding="ascii") as f:
+ f.write(text)
+
+
+main()
diff --git a/make/x68000.mk b/make/x68000.mk
index bbadb85..d751305 100644
--- a/make/x68000.mk
+++ b/make/x68000.mk
@@ -1,8 +1,8 @@
# Sharp X68000 (elf2x68k / m68k-xelf-gcc) build rules.
#
-# BRING-UP STATE. This builds libjoey.a and the serial example against the
-# generic backend -- no native primitives, no audio, no sprite codegen yet.
-# See claudeDocs/x68000_port.md for what is and is not real.
+# The port draws planar into the text plane (src/x68000/halPlanar.c), shares
+# the Amiga's 68k planar fills, plots and sprite emitter (src/m68k), plays
+# chip music on the OPM and samples through the ADPCM channel.
#
# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), installed
# by ./install.sh (X68K_ROOT). m68k-xelf-gcc emits ELF; the driver's
@@ -39,22 +39,11 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
LDFLAGS := -lm
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
-# Port-local 68000 asm, if any lands. Empty today: SCB per-band palettes are
-# done in the pixel expansion (DOS-style), not with a raster ISR, so the port is
-# pure C again.
+# Raw interrupt shims (timer tick, SCB raster, SCC receive): each saves every
+# register and returns with RTE, which a C function cannot.
PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
-# No src/m68k sources yet: those are the planar sprite emitters and the planar
-# surface helpers, and this port is still chunky. They come in with the storage
-# model decision.
-# common.mk filters spriteEmitStub.c out of GENERIC_C_SRCS because every
-# shipping port has a real per-CPU emitter. This port does not yet, so add it
-# back: jlSpriteCompile returns false and sprites stay INTERPRETER-ONLY, which
-# is correct but slow (see the "sprites must be compiled or they interpret"
-# note). Replacing this with a real 68k emitter is part of the storage-model
-# work -- src/m68k/spriteEmitPlanar68k.c already exists and, on the text-plane
-# path, needs only AMIGA_PLANE_STRIDE changed at three sites.
-# Real sprite codegen: the shared 68k PLANAR emitter, unchanged. Its
+# Sprite codegen is the shared 68k PLANAR emitter, unchanged. Its
# AMIGA_PLANE_STRIDE is SURFACE_WIDTH/8 = 40, which is this port's surface
# stride too -- the display's 128-byte stride never reaches the emitter, which
# only ever addresses the surface. So sprites COMPILE here rather than
@@ -94,11 +83,41 @@ $(LIBXMP_AR): $(LIBXMP_OBJS)
LIB := $(LIBDIR)/libjoey.a
+# Every example but Space Taxi is its sources plus the library, linked by the
+# one rule below. Worth knowing which ones the gates use:
+# PATTERN the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient
+# palettes, so a misplaced band boundary is immediately visible.
+# UBER the golden-hash vehicle: every public op, a hash per op, which
+# tools/diff-uber-hashes compares against the IIgs reference.
+# AUDIO both audio paths: JYM1 chip music on the OPM, MOD/SFX on ADPCM.
+# KEYS the input and mouse vehicle: key predicates, the typed line
+# (jlInputGetChar) and its verification row.
+# SERIAL the RS-232C gate's echo program.
+HELLO_SRC := $(EXAMPLES)/hello/hello.c
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
-SERIAL_SRC := $(EXAMPLES)/serial/serial.c
-UBER_SRC := $(EXAMPLES)/uber/uber.c
-AUDIO_SRC := $(EXAMPLES)/audio/audio.c
-KEYS_SRC := $(EXAMPLES)/keys/keys.c
+DRAW_SRC := $(EXAMPLES)/draw/draw.c
+KEYS_SRC := $(EXAMPLES)/keys/keys.c
+SERIAL_SRC := $(EXAMPLES)/serial/serial.c
+SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
+SAVE_SRC := $(EXAMPLES)/save/save.c
+JOY_SRC := $(EXAMPLES)/joy/joy.c
+SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
+AUDIO_SRC := $(EXAMPLES)/audio/audio.c
+UBER_SRC := $(EXAMPLES)/uber/uber.c
+ADV_SRC := $(EXAMPLES)/adventure/adventure.c
+ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
+AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
+EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SAVE JOY SPRITE AUDIO UBER ADV ADV2 AGI
+EXAMPLE_BINS := $(foreach e,$(EXAMPLES_X),$(BINDIR)/$(e).X)
+
+# AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program.
+DATA_DIR := $(BINDIR)/DATA
+DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
+
+# Space Taxi ships in its own folder with its DATA, as on the other ports,
+# so its megabyte of data never rides onto every other program's floppy.
+STAXI_DIR := $(BINDIR)/STAXI
+STAXI_BIN := $(STAXI_DIR)/STAXI.X
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c
.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean
@@ -107,7 +126,7 @@ all x68000: x68000-lib x68000-examples
x68000-lib: $(LIB) $(LIBXMP_AR)
-x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X $(BINDIR)/KEYS.X $(BINDIR)/STAXI.X
+x68000-examples: $(EXAMPLE_BINS) $(STAXI_BIN) $(DATA_FILES)
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@)
@@ -143,38 +162,19 @@ $(LIB): $(LIB_OBJS)
@mkdir -p $(dir $@)
$(X68K_AR) rcs $@ $(LIB_OBJS)
-# PATTERN is the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient
-# palettes, so a mis-placed band boundary is immediately visible.
-$(BINDIR)/PATTERN.X: $(PATTERN_SRC) $(LIB) $(LIBXMP_AR)
+.SECONDEXPANSION:
+$(EXAMPLE_BINS): $(BINDIR)/%.X: $$($$*_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@)
- $(X68K_CC) $(CFLAGS) $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
+ $(X68K_CC) $(CFLAGS) $($*_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
-$(BINDIR)/SERIAL.X: $(SERIAL_SRC) $(LIB) $(LIBXMP_AR)
+$(DATA_DIR)/%: $(REPO_DIR)/assets/%
@mkdir -p $(dir $@)
- $(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
-
-# UBER is the golden-hash vehicle: it exercises every public op and prints a
-# hash per op, which tools/diff-uber-hashes compares against the IIgs reference.
-$(BINDIR)/UBER.X: $(UBER_SRC) $(LIB) $(LIBXMP_AR)
- @mkdir -p $(dir $@)
- $(X68K_CC) $(CFLAGS) $(UBER_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
-
-# AUDIO exercises both audio paths: JYM1 chip music on the OPM and MOD/SFX
-# through the ADPCM channel.
-$(BINDIR)/AUDIO.X: $(AUDIO_SRC) $(LIB) $(LIBXMP_AR)
- @mkdir -p $(dir $@)
- $(X68K_CC) $(CFLAGS) $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
-
-# KEYS is the input acceptance vehicle: key predicates on the grid plus
-# the typed-character line (jlInputGetChar) and its verification row.
-$(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR)
- @mkdir -p $(dir $@)
- $(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
+ cp $< $@
# Space Taxi asset variables (the rules follow the binary below); make
# expands prerequisites when it reads the rule, so these come first.
STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000
-STAXI_RUN_DIR := $(BINDIR)/DATA
+STAXI_RUN_DIR := $(STAXI_DIR)/DATA
STAXI_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN))
@@ -227,7 +227,7 @@ $(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN)
# Human68k floppy: the unstripped binary is most of the volume on its
# own. The DATA tree next to it (levels + the precompiled cel bank) is
# what joey-run copies onto the data floppy.
-$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(call staxiData,$(STAXI_ASSET_DSTS))
+$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(call staxiData,$(STAXI_ASSET_DSTS))
@mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
diff --git a/scripts/bench-x68000.sh b/scripts/bench-x68000.sh
index a9a75b0..8ccc5a5 100755
--- a/scripts/bench-x68000.sh
+++ b/scripts/bench-x68000.sh
@@ -30,7 +30,7 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-TEMPLATE="$X68K_DIR/HUMAN302.XDF"
+source "$repo/scripts/lib/x68kDisk.sh"
FRAMES="${X68K_BENCH_FRAMES:-120000}"
WALL="${X68K_BENCH_WALL:-7200}"
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in
@@ -59,24 +59,10 @@ m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
# TWO DISKS: 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. Keeping the writable log off the boot
-# disk also means a run can never corrupt the thing that boots it.
-printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT"
-cp "$TEMPLATE" "$work/boot.xdf"
-# Data disk = the template emptied (keeps the BPB and boot sector). `empty`
-# rather than a list of filenames: the stock disk's utilities live in
-# SUBDIRECTORIES, which `delete` cannot see or free.
-cp "$TEMPLATE" "$work/data.xdf"
-python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
-# Trim the boot disk of anything not needed to boot and run one .X.
-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
-for d in SYS HIS BIN BASIC2 ASK ETC; do
- python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1
-done
-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/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
+# launching so the log lands there too.
+x68kBootDisk "$work/boot.xdf" 'B:' 'B:\UBER.X' || exit 1
+x68kDataDisk "$work/data.xdf" || exit 1
+x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
@@ -171,7 +157,8 @@ emu.register_frame_done(function()
end)
LUA
-runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
+# In the work dir, so MAME cfg/ and nvram/ never persist between runs.
+runLog=$(cd "$work" && timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/bench.lua" &1 | grep '^BENCH')
@@ -189,7 +176,7 @@ if ! echo "$runLog" | grep -q 'done-screen'; then
exit 1
fi
-python3 "$repo/tools/xdftool.py" extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
+x68kXdf extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
echo "bench-x68000: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
exit 1
}
diff --git a/scripts/check-serial.sh b/scripts/check-serial.sh
index 19ede89..96c1ad8 100755
--- a/scripts/check-serial.sh
+++ b/scripts/check-serial.sh
@@ -18,7 +18,7 @@ cd "$repo"
"$cc" -DJOEYLIB_PLATFORM_BLANK \
-DJL_HAS_SERIAL_OPEN -DJL_HAS_SERIAL_CLOSE -DJL_HAS_SERIAL_POLL \
-DJL_HAS_SERIAL_READ -DJL_HAS_SERIAL_WRITE -DJL_HAS_SERIAL_AVAILABLE \
- -DJL_HAS_SERIAL_FLUSH \
+ -DJL_HAS_SERIAL_FLUSH -DJL_HAS_SERIAL_SET_RTS -DJL_HAS_SERIAL_SET_DTR \
-Iinclude -Iinclude/joey -Isrc/core -Wall -Wextra \
src/core/serial.c tests/host/serialHost.c \
-o "$work/serialHost"
diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh
index 7f702f4..1c85807 100644
--- a/scripts/lib/joeyEnv.sh
+++ b/scripts/lib/joeyEnv.sh
@@ -38,6 +38,48 @@ joeyNeedFile() {
JOEY_DOSBOX_ISOLATE=(--noprimaryconf --nolocalconf)
+# joeyExampleProgram NAME PORT -- the built program for an example name. A
+# program that ships in its own folder with its DATA (Space Taxi on every
+# port but the IIgs) is found inside that folder. When nothing is built yet
+# it prints the flat path, so the caller's "not built" message names it.
+joeyExampleProgram() {
+ local name="$1"
+ local bin="$JOEY_REPO/build/$2/bin"
+ local up="${name^^}"
+ local ext=""
+ local candidate
+ [ "$up" = "SPACETAXI" ] && up=STAXI
+ case "$2" in
+ iigs)
+ printf '%s\n' "$bin/$up"
+ return 0
+ ;;
+ amiga)
+ # Mixed-case names, and Space Taxi is called Taxi there.
+ up="${name,,}"
+ case "$up" in
+ staxi|spacetaxi) up=taxi ;;
+ esac
+ up="${up^}"
+ ;;
+ atarist) ext=".PRG" ;;
+ dos) ext=".EXE" ;;
+ x68000) ext=".X" ;;
+ *)
+ echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, dos or x68000)" >&2
+ return 2
+ ;;
+ esac
+ for candidate in "$bin/$up/$up$ext" "$bin/$up$ext"; do
+ if [ -f "$candidate" ]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ printf '%s\n' "$bin/$up$ext"
+}
+
+
# joeyFindWindow DISPLAY PID -- print the X window owned by process PID.
# More robust than matching titles, which differ between emulator builds.
joeyFindWindow() {
diff --git a/scripts/lib/serialLoopPeer.py b/scripts/lib/serialLoopPeer.py
new file mode 100755
index 0000000..9b7d2a2
--- /dev/null
+++ b/scripts/lib/serialLoopPeer.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+"""serialLoopPeer.py - the host end of the serial loopback gates.
+
+The guest runs SERTEST (examples/sertest): it sends a banner, then echoes
+every byte it receives. This peer waits for the banner (the guest's
+transmit works), then sends a probe line and expects it back (its receive
+works too).
+
+ serialLoopPeer.py tcp-server PORT WALL [BANNER] listen; the emulator connects
+ serialLoopPeer.py tcp-client PORT WALL [BANNER] connect to the emulator
+ serialLoopPeer.py pty PATHFILE WALL [BANNER] make a raw pty pair, write the
+ guest end's path to PATHFILE
+
+WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL
+line; exit 0 on pass, 1 on fail.
+"""
+
+import os
+import select
+import socket
+import sys
+import time
+import tty
+
+DEFAULT_BANNER = b"JLSERTEST-READY"
+PROBE = b"PING12345"
+RESEND_SECONDS = 3.0 # the probe is re-sent until it comes back
+POLL_SECONDS = 0.5
+CONNECT_RETRY_SECONDS = 0.5
+READ_SIZE = 256
+SHOWN_BYTES = 80
+
+
+def acceptOne(port, wall):
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server.bind(("127.0.0.1", port))
+ server.listen(1)
+ server.settimeout(wall)
+ try:
+ conn, _ = server.accept()
+ except socket.timeout:
+ return None
+ return conn
+
+
+def connectOne(port, wall, start):
+ while time.time() - start < wall:
+ try:
+ return socket.create_connection(("127.0.0.1", port), timeout=2)
+ except OSError:
+ time.sleep(CONNECT_RETRY_SECONDS)
+ return None
+
+
+def openPty(pathFile):
+ master, slave = os.openpty()
+ tty.setraw(slave)
+ tty.setraw(master)
+ # Publish atomically: the gate polls for the file.
+ with open(pathFile + ".tmp", "w") as f:
+ f.write(os.ttyname(slave))
+ os.rename(pathFile + ".tmp", pathFile)
+ return master
+
+
+def loop(send, receive, wall, start, banner):
+ # send(bytes); receive() -> bytes, b"" for "nothing yet", None at EOF.
+ got = b""
+ sentAt = None
+ lastSend = 0.0
+ while time.time() - start < wall:
+ if banner in got and time.time() - lastSend > RESEND_SECONDS:
+ send(PROBE + b"\r\n")
+ lastSend = time.time()
+ if sentAt is None:
+ sentAt = len(got)
+ data = receive()
+ if data is None:
+ break
+ got += data
+ if sentAt is not None and PROBE in got[sentAt:]:
+ print("LOOP PASS: banner %r seen, probe echoed; stream=%r" % (banner, got[:SHOWN_BYTES]))
+ return 0
+ print("LOOP FAIL: stream=%r" % got[:SHOWN_BYTES + 40])
+ return 1
+
+
+def main(argv):
+ if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty"):
+ sys.stderr.write(__doc__)
+ return 2
+ mode = argv[1]
+ wall = float(argv[3])
+ banner = argv[4].encode() if len(argv) > 4 else DEFAULT_BANNER
+ start = time.time()
+
+ if mode == "pty":
+ master = openPty(argv[2])
+
+ def receive():
+ ready, _, _ = select.select([master], [], [], POLL_SECONDS)
+ if not ready:
+ return b""
+ try:
+ return os.read(master, READ_SIZE)
+ except OSError:
+ return b""
+
+ return loop(lambda data: os.write(master, data), receive, wall, start, banner)
+
+ port = int(argv[2])
+ conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)
+ if conn is None:
+ print("LOOP FAIL: no connection on port %d" % port)
+ return 1
+ conn.settimeout(POLL_SECONDS)
+
+ def receive():
+ try:
+ data = conn.recv(READ_SIZE)
+ except socket.timeout:
+ return b""
+ except OSError:
+ return None
+ return data if data else None
+
+ return loop(conn.sendall, receive, wall, start, banner)
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/scripts/lib/x68kDisk.sh b/scripts/lib/x68kDisk.sh
new file mode 100644
index 0000000..85b812d
--- /dev/null
+++ b/scripts/lib/x68kDisk.sh
@@ -0,0 +1,70 @@
+# Human68k floppy staging shared by joey-run and the X68000 gates and tools.
+# Every run boots a copy of the stock system disk (HUMAN302.XDF from the
+# x68kFirmware component) trimmed to what it takes to boot and run one
+# program; most also carry the program, its DATA tree and its log on an
+# emptied second disk in drive B, so a run can never corrupt what boots it.
+# Source after joeyEnv.sh and call `joeyNeed X68K_DIR x68kFirmware` first.
+
+X68K_SYSTEM_DISK="${X68K_DIR:-}/HUMAN302.XDF"
+
+
+# x68kAddTree DISK DIR PREFIX -- copy every file under DIR onto DISK as
+# PREFIX/, upper-cased the way Human68k shows names.
+x68kAddTree() {
+ local disk="$1"
+ local dir="$2"
+ local prefix="$3"
+ local files
+ local f
+ local rel
+ files="$(find "$dir" -type f | sort)"
+ while IFS= read -r f; do
+ [ -n "$f" ] || continue
+ rel="${f#"$dir"/}"
+ x68kXdf add "$disk" "$f" "$prefix/${rel^^}" >/dev/null || return 1
+ done <<< "$files"
+}
+
+
+# x68kBootDisk OUT LINE... -- OUT becomes a boot disk whose AUTOEXEC.BAT runs
+# the given command lines (pass them single-quoted: 'B:\UBER.X'). The stock
+# disk's keyboard, beep and font drivers go, and so do its utility folders
+# (~900 KB, in subdirectories only deltree can free): nothing a JoeyLib
+# program needs -- except \SYS\RSDRV.SYS, Human68k's RS-232C driver, which
+# comes back: the stock CONFIG.SYS loads it (DEVICE = \SYS\RSDRV.SYS), and
+# with it the serial add-on runs above 9600 bps and does RTS/CTS.
+x68kBootDisk() {
+ local out="$1"
+ local name
+ shift
+ cp "$X68K_SYSTEM_DISK" "$out" || return 1
+ for name in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
+ x68kXdf delete "$out" "$name" >/dev/null 2>&1 || true
+ done
+ for name in SYS HIS BIN BASIC2 ASK ETC; do
+ x68kXdf deltree "$out" "$name" >/dev/null 2>&1 || true
+ done
+ # Optional: without it the serial add-on keeps the ROM's limits.
+ if x68kXdf extract "$X68K_SYSTEM_DISK" SYS/RSDRV.SYS "$out.rsdrv" >/dev/null 2>&1; then
+ x68kXdf add "$out" "$out.rsdrv" SYS/RSDRV.SYS >/dev/null || return 1
+ fi
+ rm -f "$out.rsdrv"
+ { printf '%s\r\n' "$@"; printf '\x1a'; } > "$out.autoexec"
+ x68kXdf add "$out" "$out.autoexec" AUTOEXEC.BAT >/dev/null || return 1
+ rm -f "$out.autoexec"
+}
+
+
+# x68kDataDisk OUT -- OUT becomes an empty disk: the stock disk's BPB and
+# boot sector with every file and folder freed (all 1221 clusters).
+x68kDataDisk() {
+ cp "$X68K_SYSTEM_DISK" "$1" || return 1
+ x68kXdf empty "$1" >/dev/null
+}
+
+
+# x68kXdf ARGS... -- tools/xdftool.py (list, add, delete, deltree, empty,
+# extract, free).
+x68kXdf() {
+ python3 "$JOEY_REPO/tools/xdftool.py" "$@"
+}
diff --git a/scripts/profile-x68000.sh b/scripts/profile-x68000.sh
index 7310b7f..97666e7 100755
--- a/scripts/profile-x68000.sh
+++ b/scripts/profile-x68000.sh
@@ -37,10 +37,9 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-bin=$repo/build/x68000/bin/$PROG.X
+source "$repo/scripts/lib/x68kDisk.sh"
+bin=$(joeyExampleProgram "$prog" x68000) || exit 2
elf=$bin.elf
-x68=$X68K_DIR
-template=$x68/HUMAN302.XDF
joeyNeed X68K_ROOT toolchain:x68000
export PATH="$X68K_ROOT/bin:$PATH"
@@ -69,25 +68,13 @@ mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "gMouseX" { print strtonum("0x" $1)
work=$(mktemp -d -t joey-x68prof.XXXXXX)
trap 'rm -rf "$work"' EXIT
-printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT"
-cp "$template" "$work/boot.xdf"
-cp "$template" "$work/data.xdf"
-python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
-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 || true
-done
-for d in SYS HIS BIN BASIC2 ASK ETC; do
- python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true
-done
-python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
-python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null
-data_dir=$repo/build/x68000/bin/DATA
-if [[ -d $data_dir ]]; then
- while IFS= read -r f; do
- rel=${f#"$data_dir"/}
- python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \
- "DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null
- done < <(find "$data_dir" -type f | sort)
+x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1
+x68kDataDisk "$work/data.xdf" || exit 1
+x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1
+# The program's DATA tree sits next to it: its own folder's for Space Taxi,
+# the shared bin/DATA for the rest.
+if [[ -d $(dirname "$bin")/DATA ]]; then
+ x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1
fi
cat > "$work/prof.lua" <mame.log 2>&1 ) || true
diff --git a/scripts/verify-audio-all.sh b/scripts/verify-all.sh
similarity index 64%
rename from scripts/verify-audio-all.sh
rename to scripts/verify-all.sh
index d5fdac3..691f586 100755
--- a/scripts/verify-audio-all.sh
+++ b/scripts/verify-all.sh
@@ -1,15 +1,25 @@
#!/usr/bin/env bash
-# verify-audio-all.sh - Run every port's audio gate and summarise.
+# verify-all.sh - Run one kind of gate on every port and summarise.
#
-# Driven by `make verify-audio`. Five gates, two kinds:
+# Driven by `make verify-audio` and `make verify-serial`.
#
+# audio: five gates, two kinds:
# iigs DOC wavetable + sound IRQ vector (register tap)
# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap)
# atarist YM2149 write trace, with the pc (register tap)
# amiga recorded sound, four loudness phases (Paula has no tap)
# dos recorded sound, four loudness phases (SB has no tap)
#
-# SEQUENTIAL, deliberately. The two recording gates drive the emulator on
+# serial: the host check of the shared layer, then each port's loopback
+# (banner out, probe echoed back):
+# host scripts/check-serial.sh (no emulator)
+# iigs SCC modem port -> MAME null_modem
+# x68000 SCC channel A -> MAME null_modem
+# atarist Modem port -> Hatari pty
+# amiga serial.device -> FS-UAE TCP
+# dos COM1 -> DOSBox nullmodem
+#
+# SEQUENTIAL, deliberately. The two recording audio gates drive the emulator on
# wall-clock sleeps and grade fixed time windows of the capture, so a
# second emulator competing for CPU can slide a phase boundary and fail a
# gate that is actually fine. Running five emulators at once also makes
@@ -27,17 +37,23 @@
# roll-up. Gate stdout/stderr is echoed as it happens, so a failure shows
# its diagnosis here and not just a verdict.
#
-# Usage: scripts/verify-audio-all.sh [port ...]
-# with no arguments, all five. Otherwise any of:
-# iigs x68000 atarist amiga dos
-# Env: AUDIO_ALL_LOG also append the full transcript to this file
+# Usage: scripts/verify-all.sh audio|serial [port ...]
+# with no ports, all of that kind's. Otherwise any of:
+# iigs x68000 atarist amiga dos (and host, for serial)
+# Env: VERIFY_ALL_LOG also append the full transcript to this file
# Requires: whatever each selected gate requires; missing ones SKIP.
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-label=verify-audio-all
+kind=${1:-}
+shift || true
+label="verify-all $kind"
-ALL_PORTS="iigs x68000 atarist amiga dos"
+case $kind in
+ audio) ALL_PORTS="iigs x68000 atarist amiga dos" ;;
+ serial) ALL_PORTS="host iigs x68000 atarist amiga dos" ;;
+ *) echo "usage: scripts/verify-all.sh audio|serial [port ...]" >&2; exit 2 ;;
+esac
ports=${*:-$ALL_PORTS}
for p in $ports; do
@@ -48,12 +64,11 @@ for p in $ports; do
done
gateFor() { # $1 = port -> script + args
- case $1 in
- iigs) echo "scripts/verify-iigs-audio.sh staxi" ;;
- x68000) echo "scripts/verify-x68000-audio.sh" ;;
- atarist) echo "scripts/verify-atarist-audio.sh" ;;
- amiga) echo "scripts/verify-amiga-audio.sh" ;;
- dos) echo "scripts/verify-dos-audio.sh" ;;
+ case $kind:$1 in
+ audio:iigs) echo "scripts/verify-iigs-audio.sh staxi" ;;
+ audio:*) echo "scripts/verify-$1-audio.sh" ;;
+ serial:host) echo "scripts/check-serial.sh" ;;
+ serial:*) echo "scripts/verify-$1-serial.sh" ;;
esac
}
@@ -67,8 +82,8 @@ for port in $ports; do
echo
echo "=== $label: $port ($gate) ==="
start=$SECONDS
- if [ -n "${AUDIO_ALL_LOG:-}" ]; then
- ( cd "$repo" && $gate ) 2>&1 | tee -a "$AUDIO_ALL_LOG"
+ if [ -n "${VERIFY_ALL_LOG:-}" ]; then
+ ( cd "$repo" && $gate ) 2>&1 | tee -a "$VERIFY_ALL_LOG"
rc=${PIPESTATUS[0]}
else
( cd "$repo" && $gate )
@@ -106,5 +121,5 @@ if [ -n "$skipped" ]; then
echo "$label: INCOMPLETE - skipped$skipped (build those ports, then re-run)" >&2
exit 3
fi
-echo "$label: PASS (all $(count "$passed") audio gates)"
+echo "$label: PASS (all $(count "$passed") $kind gates)"
exit 0
diff --git a/scripts/verify-amiga-serial.sh b/scripts/verify-amiga-serial.sh
new file mode 100755
index 0000000..8494c8e
--- /dev/null
+++ b/scripts/verify-amiga-serial.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+# verify-amiga-serial.sh - both-directions RS-232 gate for the Amiga port.
+#
+# Boots SerTest (examples/sertest: banner, then echo) off a directory hard
+# drive in FS-UAE with the serial port on a TCP socket, and has the host
+# peer (scripts/lib/serialLoopPeer.py) wait for the banner, send a probe and
+# expect it echoed. That exercises the chain: m68k-amigaos-gcc -> AmigaDOS ->
+# serial.device -> JoeyLib serial HAL -> Paula's UART -> host.
+#
+# Kickstart 3.1 has no serial.device in ROM: AmigaOS loads it from DEVS:, so
+# the boot drive gets Devs/serial.device from the installer (amigaSerial).
+#
+# Usage: scripts/verify-amiga-serial.sh
+# Env: AMIGA_SERIAL_PORT TCP port FS-UAE listens on (default 6801)
+# Requires: fs-uae, Xvfb; build/amiga/bin/SerTest (make amiga); the
+# Kickstart and serial.device (install.sh install emulator:amiga).
+set -uo pipefail
+
+repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed FSUAE fsuae
+joeyNeed KICKSTART_ROM kickstart
+joeyNeed AMIGA_SERIAL_DEVICE amigaSerial
+
+bin="$repo/build/amiga/bin/SerTest"
+port="${AMIGA_SERIAL_PORT:-6801}"
+wall=60
+[ -f "$bin" ] || { echo "verify-amiga-serial: build SerTest first (make amiga)" >&2; exit 2; }
+
+work=$(mktemp -d -t joeylib-amiga-serial.XXXXXX)
+cleanup() {
+ joeyStop "${fsuaePid:-}"
+ joeyStop "${xvfbPid:-}"
+ rm -rf "$work"
+}
+trap cleanup EXIT
+
+mkdir -p "$work/hd/s" "$work/hd/Devs" "$work/home"
+cp "$bin" "$work/hd/SerTest"
+cp "$AMIGA_SERIAL_DEVICE" "$work/hd/Devs/serial.device"
+printf 'stack 32768\n:SerTest\n' > "$work/hd/s/startup-sequence"
+
+display=":$((RANDOM % 400 + 1700))"
+Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
+xvfbPid=$!
+sleep 1
+
+# serial_port tcp://.../wait: FS-UAE listens and holds the emulation until
+# the peer connects, so the banner is never sent to nobody.
+HOME="$work/home" DISPLAY="$display" SDL_AUDIODRIVER=dummy ALSOFT_DRIVERS=null "$FSUAE" \
+ --amiga_model=A500 --kickstart_file="$KICKSTART_ROM" \
+ --hard_drive_0="$work/hd" --hard_drive_0_label=JOEYLIB \
+ --fade_out_duration=0 --joystick_port_0=none --joystick_port_1=none \
+ --amiga_enable_serial_port=1 --serial_port="tcp://127.0.0.1:$port/wait" \
+ > "$work/fsuae.log" 2>&1 &
+fsuaePid=$!
+
+result=$(python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-client "$port" "$wall")
+rc=$?
+echo "$result"
+if [ "$rc" -eq 0 ]; then
+ echo "verify-amiga-serial: PASS (banner out, probe echoed through serial.device)"
+else
+ echo "verify-amiga-serial: FAIL" >&2
+ tail -n 5 "$work/fsuae.log" >&2
+fi
+exit "$rc"
diff --git a/scripts/verify-atarist-serial.sh b/scripts/verify-atarist-serial.sh
new file mode 100755
index 0000000..890b27f
--- /dev/null
+++ b/scripts/verify-atarist-serial.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+# verify-atarist-serial.sh - both-directions RS-232 gate for the Atari ST port.
+#
+# Runs SERTEST.PRG (examples/sertest: banner, then echo) under Hatari and
+# EmuTOS with the Modem port on a pseudo-terminal whose other end is the host
+# peer (scripts/lib/serialLoopPeer.py): it waits for the banner, sends a
+# probe and expects it echoed. That exercises the chain: m68k-atari-mint-gcc
+# -> the BIOS Aux channel -> JoeyLib serial HAL -> the MFP's USART -> host.
+#
+# Usage: scripts/verify-atarist-serial.sh
+# Requires: hatari, EmuTOS (install.sh install emulator:atarist);
+# build/atarist/bin/SERTEST.PRG (make atarist).
+set -uo pipefail
+
+repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed HATARI hatari
+joeyNeed EMUTOS_IMG emutos
+
+prg="$repo/build/atarist/bin/SERTEST.PRG"
+wall=60
+[ -f "$prg" ] || { echo "verify-atarist-serial: build SERTEST.PRG first (make atarist)" >&2; exit 2; }
+
+work=$(mktemp -d -t joeylib-atarist-serial.XXXXXX)
+cleanup() {
+ joeyStop "${hatariPid:-}"
+ joeyStop "${peerPid:-}"
+ rm -rf "$work"
+}
+trap cleanup EXIT
+
+mkdir -p "$work/c"
+cp "$prg" "$work/c/SERTEST.PRG"
+
+python3 "$repo/scripts/lib/serialLoopPeer.py" pty "$work/pty" "$wall" > "$work/peer.log" 2>&1 &
+peerPid=$!
+for _ in $(seq 1 50); do
+ [ -f "$work/pty" ] && break
+ sleep 0.1
+done
+[ -f "$work/pty" ] || { echo "verify-atarist-serial: the peer made no pty" >&2; exit 1; }
+pty=$(cat "$work/pty")
+
+SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" --tos "$EMUTOS_IMG" \
+ --harddrive "$work/c" --gemdos-drive C --drive-a off --drive-b off \
+ --sound off --fast-forward on --rs232-in "$pty" --rs232-out "$pty" \
+ --auto 'C:\SERTEST.PRG' > "$work/hatari.log" 2>&1 &
+hatariPid=$!
+
+wait "$peerPid"
+rc=$?
+peerPid=""
+cat "$work/peer.log"
+if [ "$rc" -eq 0 ]; then
+ echo "verify-atarist-serial: PASS (banner out, probe echoed through the Modem port)"
+else
+ echo "verify-atarist-serial: FAIL" >&2
+fi
+exit "$rc"
diff --git a/scripts/verify-dos-serial.sh b/scripts/verify-dos-serial.sh
new file mode 100755
index 0000000..4b67921
--- /dev/null
+++ b/scripts/verify-dos-serial.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+# verify-dos-serial.sh - both-directions RS-232 gate for the DOS port.
+#
+# Runs SERTEST.EXE (examples/sertest: banner, then echo) in DOSBox Staging
+# with COM1 as a nullmodem connecting to the host peer
+# (scripts/lib/serialLoopPeer.py), which waits for the banner, sends a probe
+# and expects it echoed. That exercises the chain: DJGPP -> DPMI -> JoeyLib's
+# interrupt-driven 8250/16550 HAL -> DOSBox's UART -> host.
+#
+# Usage: scripts/verify-dos-serial.sh
+# Env: DOS_SERIAL_PORT TCP port the peer listens on (default 6802)
+# Requires: DOSBox Staging, Xvfb; build/dos/bin/SERTEST.EXE (make dos).
+set -uo pipefail
+
+repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed DOSBOX dosbox
+
+exe="$repo/build/dos/bin/SERTEST.EXE"
+port="${DOS_SERIAL_PORT:-6802}"
+wall=38
+runSeconds=40 # SERTEST echoes forever: DOSBox ends by timeout
+[ -f "$exe" ] || { echo "verify-dos-serial: build SERTEST.EXE first (make dos)" >&2; exit 2; }
+
+work=$(mktemp -d -t joeylib-dos-serial.XXXXXX)
+cleanup() {
+ joeyStop "${peerPid:-}"
+ joeyStop "${xvfbPid:-}"
+ rm -rf "$work"
+}
+trap cleanup EXIT
+
+mkdir -p "$work/c" "$work/xdg"
+cp "$exe" "$work/c/SERTEST.EXE"
+
+python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$wall" > "$work/peer.log" 2>&1 &
+peerPid=$!
+sleep 1
+
+display=":$((RANDOM % 400 + 1300))"
+Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
+xvfbPid=$!
+sleep 1
+
+# Foreground under timeout -s KILL, so nothing can outlive the gate.
+XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
+ timeout -s KILL "$runSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
+ --set "serial1=nullmodem server:127.0.0.1 port:$port transparent:1" \
+ -c "MOUNT C $work/c" -c "C:" -c "SERTEST.EXE" -c "exit" > "$work/dosbox.log" 2>&1
+
+wait "$peerPid"
+rc=$?
+peerPid=""
+cat "$work/peer.log"
+if [ "$rc" -eq 0 ]; then
+ echo "verify-dos-serial: PASS (banner out, probe echoed through COM1)"
+else
+ echo "verify-dos-serial: FAIL" >&2
+fi
+exit "$rc"
diff --git a/scripts/verify-iigs-serial-loss.sh b/scripts/verify-iigs-serial-loss.sh
new file mode 100755
index 0000000..069c29b
--- /dev/null
+++ b/scripts/verify-iigs-serial-loss.sh
@@ -0,0 +1,360 @@
+#!/usr/bin/env bash
+# verify-iigs-serial-loss.sh - Receive-loss gate for the Apple IIgs serial add-on.
+#
+# SERLOSS (examples/serloss/serloss.c) fills and presents the whole screen in a
+# loop while this script's peer streams 4096 bytes (byte k = k & 0xFF) into the
+# port at 9600 baud. The probe counts, sequence-checks and sums what arrives and
+# leaves the result in a mailbox at $E1:9DD0, which the MAME Lua reads.
+#
+# PASS all 4096 bytes arrived, in order, with the right sum
+# FAIL anything missing or out of sequence
+# SKIP (exit 2) a tool or ROM is missing, or MAME did not confirm the
+# real SCC receive FIFO depth (below)
+#
+# scripts/verify-iigs-serial-loss.sh scc modem port (SCC channel B)
+# scripts/verify-iigs-serial-loss.sh ssc Super Serial Card in slot 2
+#
+# The emulator must model the real receive FIFO depth, or a receiver that
+# falls behind still looks fine. The installed MAME's
+# installer/patches/mame/0002-z80scc-rx-fifo.patch gives the Z8530 a 512-byte
+# receive FIFO by default, so polled receivers survive in the other gates;
+# this script runs it with JOEY_Z80SCC_RX_FIFO=3, the real chip's depth. MAME
+# then prints "RX FIFO depth 3 (JOEY_Z80SCC_RX_FIFO)" per SCC channel, and an
+# scc PASS counts only with that line (installer/patches/mame/README.md). The
+# 6551 on the Super Serial Card has no FIFO to widen.
+#
+# The card's interrupt switch (SW2-6) is OFF by default, as on a real Super
+# Serial Card; the Lua turns it on so the 6551's IRQ reaches the slot. MAME
+# needs the card's firmware, a2ssc.zip, which the iigsFirmware component
+# installs into $IIGS_ROMPATH.
+#
+# Environment:
+# SERLOSS_MAME MAME binary (default $MAME); it runs with JOEY_Z80SCC_RX_FIFO=3.
+# An scc PASS from one that does not report that depth (a
+# build without the 0002 patch, whose FIFO is already the
+# real 3) stands, with a warning; from $MAME it is a SKIP,
+# since that means the installed MAME predates the switch
+# SERLOSS_BIN probe binary (default build/iigs/bin/SERLOSS or SERLOSSS)
+# SERLOSS_PORT TCP port for the bitbanger (default 45241)
+# SERLOSS_MACHINE apple2gs (ROM 03, default) or apple2gsr1 (ROM 01)
+# SERLOSS_LAUNCHER set to boot straight into the probe instead of driving
+# the Finder (whose timeline only fits ROM 03)
+# SERLOSS_BAUD peer line rate, must match the probe's -DSERLOSS_BAUD (default 9600)
+# SERLOSS_ROMPATH MAME rompath (default $IIGS_ROMPATH, which holds a2ssc)
+# SERLOSS_WALL wall-clock cap for MAME in seconds (default 600)
+# SERLOSS_MAX_FRAME emulated-frame cap (default 12000)
+# SERLOSS_TAP file: log every data-register read with its emulated time
+# SERLOSS_PADDLE plug MAME's joystick into the game port and hold both paddles
+# at this value (0..255) for a -DSERLOSS_JOY probe; 255, full
+# deflection, is the longest paddle read there is
+set -uo pipefail
+
+repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
+
+mode="${1:-scc}"
+case "$mode" in
+ scc) name=SERLOSS ;;
+ ssc) name=SERLOSSS ;;
+ *) echo "verify-iigs-serial-loss: mode must be scc or ssc" >&2; exit 2 ;;
+esac
+mame="${SERLOSS_MAME:-$MAME}"
+rompath="${SERLOSS_ROMPATH:-$IIGS_ROMPATH}"
+bin="${SERLOSS_BIN:-$repo/build/iigs/bin/$name}"
+PORT="${SERLOSS_PORT:-45241}"
+WALL="${SERLOSS_WALL:-600}"
+maxFrame="${SERLOSS_MAX_FRAME:-12000}"
+total=4096
+
+[ -x "$mame" ] || { echo "verify-iigs-serial-loss: no MAME at $mame" >&2; exit 2; }
+[ -f "$bin" ] || { echo "verify-iigs-serial-loss: build $bin first" >&2; exit 2; }
+if [ "$mode" = ssc ] && [ -z "${SERLOSS_ROMPATH:-}" ]; then
+ joeyNeedFile "$IIGS_ROMPATH/a2ssc.zip" iigsFirmware
+fi
+
+work=$(mktemp -d -t joeylib-iigs-loss.XXXXXX)
+peer_pid=""
+cleanup() {
+ [ -n "$peer_pid" ] && kill -9 "$peer_pid" 2>/dev/null
+ rm -rf "$work"
+}
+trap cleanup EXIT
+
+mkdir -p "$work/bin"
+cp "$bin" "$work/bin/$name"
+BINDIR="$work/bin" JOEY_DISK_EXAMPLES="$name" bash "$repo/scripts/make-iigs-disk.sh" "$work/loss.2mg" >/dev/null 2>&1 || {
+ echo "verify-iigs-serial-loss: FAIL - could not build the probe disk" >&2
+ exit 1
+}
+# SERLOSS_LAUNCHER=1 boots straight into the probe (make-iigs-boot-disk.sh)
+# instead of driving the Finder, whose keystroke timeline is calibrated for
+# ROM 03 only.
+if [ -n "${SERLOSS_LAUNCHER:-}" ]; then
+ bash "$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null 2>&1 || {
+ echo "verify-iigs-serial-loss: FAIL - could not build the launcher boot disk" >&2
+ exit 1
+ }
+ finderAt=999999
+else
+ cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
+ finderAt=3000
+fi
+
+# Peer: listen (MAME connects out), wait for the probe's READY, stream the
+# pattern, then keep draining whatever the probe sends until MAME goes away.
+cat > "$work/peer.py" <<'PY'
+import socket, sys, time
+
+port = int(sys.argv[1])
+total = int(sys.argv[2])
+log = open(sys.argv[3], "w")
+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(600)
+conn, _ = srv.accept()
+conn.settimeout(1.0)
+seen = b""
+sent = False
+while True:
+ try:
+ chunk = conn.recv(256)
+ except socket.timeout:
+ continue
+ except OSError:
+ break
+ if not chunk:
+ break
+ seen += chunk
+ if not sent and b"READY" in seen:
+ conn.sendall(bytes(k & 0xFF for k in range(total)))
+ sent = True
+ log.write("peer: READY seen, sent %d bytes\n" % total)
+ log.flush()
+log.write("peer: received %r\n" % seen[:64])
+log.close()
+PY
+python3 "$work/peer.py" "$PORT" "$total" "$work/peer.log" &
+peer_pid=$!
+sleep 1
+
+if [ "$mode" = ssc ]; then
+ serialArgs=(-sl2 ssc -sl2:ssc:rs232 null_modem -bitb "socket.127.0.0.1:$PORT")
+ dipPort=":sl2:ssc:DSWX"
+ modemTag=":sl2:ssc:rs232:null_modem"
+else
+ serialArgs=(-modem null_modem -bitb "socket.127.0.0.1:$PORT")
+ dipPort=""
+ modemTag=":modem:null_modem"
+fi
+# The apple2gs has nothing on its game port by default (every paddle reads 0).
+paddle="${SERLOSS_PADDLE:-}"
+gameArgs=()
+if [ -n "$paddle" ]; then
+ gameArgs=(-gameio joy)
+fi
+paddleValue="${paddle:-0}"
+case "${SERLOSS_BAUD:-9600}" in
+ 2400) baudCode=5 ;;
+ 4800) baudCode=6 ;;
+ 9600) baudCode=7 ;;
+ 19200) baudCode=9 ;;
+ *) echo "verify-iigs-serial-loss: unsupported SERLOSS_BAUD" >&2; exit 2 ;;
+esac
+
+cat > "$work/loss.lua" <= scr.frame_period then e = e - scr.frame_period end
+ if e < 0 then e = e + scr.frame_period end
+ e = e + scr.pixel_period / 2
+ local v = math.floor(e / scr.scan_period)
+ return v % 262, math.floor((e - v * scr.scan_period) / scr.pixel_period)
+ end
+ local function counters(offset, data, mask)
+ local v, hp = beam()
+ local line = v + 501
+ if line > 511 then line = line - 262 end
+ if (offset & 0xFF) == 0x2E then return line >> 1 end
+ local h = hp // 11
+ if h > 0 then h = h + 0x40 end
+ return h | ((line & 1) << 7)
+ end
+ for _, bank in ipairs({0x000000, 0xE00000, 0xE10000}) do
+ vcTaps[#vcTaps + 1] = mem:install_read_tap(bank + 0xC02E, bank + 0xC02F, "beam" .. bank, counters)
+ end
+end
+
+-- Hold both paddles of MAME's joystick (-gameio joy) still.
+local function paddlesHold()
+ if "$paddle" == "" then return end
+ for _, name in ipairs({"joystick_1_x", "joystick_1_y"}) do
+ local p = manager.machine.ioport.ports[":gameio:joy:" .. name]
+ if p ~= nil then
+ for _, f in pairs(p.fields) do f:set_value($paddleValue) end
+ end
+ end
+end
+
+local function sscInterruptsOn()
+ local p = manager.machine.ioport.ports["$dipPort"]
+ if p == nil then return end
+ local f = p.fields["Interrupts"]
+ if f ~= nil then f.user_value = 0 end
+end
+
+-- Optional (SERLOSS_TAP=file): log every CPU read of the data register with
+-- the emulated time, for a receive-latency histogram.
+local tapFile = "${SERLOSS_TAP:-}"
+local taps = {}
+local function installTaps()
+ if tapFile == "" then return end
+ local out = io.open(tapFile, "w")
+ local dataReg = ("$mode" == "ssc") and 0xC0A8 or 0xC03A
+ for _, bank in ipairs({0x000000, 0xE00000, 0xE10000}) do
+ taps[#taps + 1] = mem:install_read_tap(bank + dataReg, bank + dataReg, "rx" .. bank,
+ function(offset, data, mask)
+ out:write(string.format("%.7f %d\n", manager.machine.time:as_double(), data & 0xFF))
+ out:flush()
+ end)
+ end
+end
+
+-- The null_modem's line rate is a configuration setting (default 9600).
+local function peerBaud()
+ for _, name in ipairs({"RS232_TXBAUD", "RS232_RXBAUD"}) do
+ local p = manager.machine.ioport.ports["$modemTag:" .. name]
+ if p ~= nil then
+ for _, f in pairs(p.fields) do f.user_value = $baudCode end
+ end
+ end
+end
+
+local steps = {
+ {10, function() if "$dipPort" ~= "" then sscInterruptsOn() end; peerBaud(); paddlesHold(); beamCountersFix(); installTaps() end},
+ {11, function() if $finderAt > 3000 then launched = true end end},
+ {$finderAt, function() nat:post("J") end},
+ {$finderAt + 120, function() press(key_cmd) end},
+ {$finderAt + 126, function() nat:post("o") end},
+ {$finderAt + 180, function() release(key_cmd) end},
+ {$finderAt + 540, function() nat:post("$name") end},
+ {$finderAt + 660, function() press(key_cmd) end},
+ {$finderAt + 666, function() nat:post("o") end},
+ {$finderAt + 720, function() release(key_cmd); launched = true 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
+ if launched and frame % 60 == 0 then
+ local sig = mem:read_u8(0xE19DD0) == 0xA5 and mem:read_u8(0xE19DD1) == 0x5A
+ local phase = mem:read_u8(0xE19DD2)
+ if sig and (phase == 0x05 or phase == 0xEE) then
+ report(); manager.machine:exit()
+ end
+ end
+ if frame >= $maxFrame then
+ report(); manager.machine:exit()
+ end
+end)
+LUA
+
+cd "$work"
+out=$(JOEY_Z80SCC_RX_FIFO=3 QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
+ timeout -s KILL "$WALL" "$mame" "${SERLOSS_MACHINE:-apple2gs}" \
+ -rompath "$rompath" \
+ -flop3 "$work/boot.po" -flop4 "$work/loss.2mg" \
+ "${serialArgs[@]}" "${gameArgs[@]}" \
+ -video none -sound none -nothrottle \
+ -autoboot_script "$work/loss.lua" &1) || true
+
+sleep 1
+kill -9 "$peer_pid" 2>/dev/null
+peer_pid=""
+cat "$work/peer.log" 2>/dev/null
+
+line=$(echo "$out" | grep -E '^VERIFY-IIGS-LOSS ' | tail -1)
+echo "$line"
+if [ -z "$line" ]; then
+ echo "verify-iigs-serial-loss: FAIL - MAME produced no report" >&2
+ echo "$out" | tail -10 >&2
+ exit 1
+fi
+get() { echo "$line" | grep -oE "$1=[0-9A-F]+" | cut -d= -f2; }
+phase=$(get phase); received=$(get received); inSeq=$(get inSequence); breaks=$(get breaks); sum=$(get sum)
+presents=$(get presents); fillTicks=$(get fillTicks); bothTicks=$(get fillPresentTicks)
+if [ -n "$bothTicks" ] && [ -n "$fillTicks" ]; then
+ # 60 fills vs 60 fill+present pairs, in 60 Hz ticks: the difference is 60 presents.
+ echo " full-screen present: $(( (bothTicks - fillTicks) * 1000000 / 3600 )) us each (fills $fillTicks ticks, fills+presents $bothTicks ticks)"
+fi
+joyPolls=$(get joyPolls)
+if [ -n "$joyPolls" ] && [ "$joyPolls" != 0 ]; then
+ # A -DSERLOSS_JOY probe calibrates the held stick first, so every reading
+ # should be 0: the spread is how far interrupts pushed the paddle reads.
+ echo " joystick: $joyPolls polls, paddles held at ${paddle:-0}, X read $(echo "$line" | grep -oE 'joyX=[-0-9.]+' | cut -d= -f2), Y read $(echo "$line" | grep -oE 'joyY=[-0-9.]+' | cut -d= -f2)"
+fi
+if [ "$phase" = 05 ] && [ "$received" = "$total" ] && [ "$inSeq" = "$total" ] && [ "$breaks" = 0 ] && [ "$sum" = F800 ]; then
+ # An scc PASS proves something only at the real FIFO depth (see the header).
+ if [ "$mode" = scc ] && ! grep -qF 'RX FIFO depth 3 (JOEY_Z80SCC_RX_FIFO)' <<< "$out"; then
+ if [ -z "${SERLOSS_MAME:-}" ]; then
+ echo "verify-iigs-serial-loss: SKIP - $mame did not confirm RX FIFO depth 3 (installed before JOEY_Z80SCC_RX_FIFO existed?), so a 512-byte SCC FIFO may be hiding loss; run: ./install.sh install mame" >&2
+ exit 2
+ fi
+ echo "verify-iigs-serial-loss: WARNING - $mame did not report JOEY_Z80SCC_RX_FIFO=3; trusting it to model the real SCC FIFO" >&2
+ fi
+ echo "verify-iigs-serial-loss: PASS ($mode: $total/$total bytes, in order, through $presents full-screen presents)"
+ exit 0
+fi
+echo "verify-iigs-serial-loss: FAIL ($mode: received $received/$total, $breaks sequence breaks, phase $phase)" >&2
+exit 1
diff --git a/scripts/verify-x68000-audio.sh b/scripts/verify-x68000-audio.sh
index b50ac0b..106537d 100755
--- a/scripts/verify-x68000-audio.sh
+++ b/scripts/verify-x68000-audio.sh
@@ -41,13 +41,12 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-bin=$repo/build/x68000/bin/$PROG.X
-x68=$X68K_DIR
-template=$x68/HUMAN302.XDF
+source "$repo/scripts/lib/x68kDisk.sh"
+bin=$(joeyExampleProgram "$prog" x68000) || exit 2
source=$repo/src/x68000/audio.c
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; }
-[[ -f $template ]] || { echo "verify-x68000-audio: missing $template" >&2; exit 2; }
+[[ -f $X68K_SYSTEM_DISK ]] || { echo "verify-x68000-audio: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
# The noise table, straight out of the port so the two cannot drift apart.
table=$(python3 - "$source" <<'PY'
@@ -63,25 +62,13 @@ PY
work=$(mktemp -d -t joey-x68audio.XXXXXX)
trap 'rm -rf "$work"' EXIT
-printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT"
-cp "$template" "$work/boot.xdf"
-cp "$template" "$work/data.xdf"
-python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
-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 || true
-done
-for d in SYS HIS BIN BASIC2 ASK ETC; do
- python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true
-done
-python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
-python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null
-data_dir=$repo/build/x68000/bin/DATA
-if [[ -d $data_dir ]]; then
- while IFS= read -r f; do
- rel=${f#"$data_dir"/}
- python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \
- "DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null
- done < <(find "$data_dir" -type f | sort)
+x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1
+x68kDataDisk "$work/data.xdf" || exit 1
+x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1
+# The program's DATA tree sits next to it: its own folder's for Space Taxi,
+# the shared bin/DATA for the rest.
+if [[ -d $(dirname "$bin")/DATA ]]; then
+ x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1
fi
# The taps keep to arithmetic and file writes: a tap that reads memory
@@ -150,7 +137,7 @@ end)
LUA
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 1800 "$MAME" x68000 \
- -bios ipl10 -rompath "$x68/roms" \
+ -bios ipl10 -rompath "$X68K_DIR/roms" \
-flop1 boot.xdf -flop2 data.xdf \
-video none -sound none -nothrottle -skip_gameinfo \
-autoboot_script tap.lua mame.log 2>&1 ) || true
diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh
index 841f7a5..65fa98b 100755
--- a/scripts/verify-x68000-golden.sh
+++ b/scripts/verify-x68000-golden.sh
@@ -23,7 +23,7 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-TEMPLATE="$X68K_DIR/HUMAN302.XDF"
+source "$repo/scripts/lib/x68kDisk.sh"
GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
WALL="${X68K_GOLDEN_WALL:-7200}"
@@ -53,24 +53,10 @@ m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
# TWO DISKS: 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. Keeping the writable log off the boot
-# disk also means a run can never corrupt the thing that boots it.
-printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT"
-cp "$TEMPLATE" "$work/gold.xdf"
-# Data disk = the template emptied (keeps the BPB and boot sector). `empty`
-# rather than a list of filenames: the stock disk's utilities live in
-# SUBDIRECTORIES, which `delete` cannot see or free.
-cp "$TEMPLATE" "$work/data.xdf"
-python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
-# Trim the boot disk of anything not needed to boot and run one .X.
-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
-for d in SYS HIS BIN BASIC2 ASK ETC; do
- python3 "$repo/tools/xdftool.py" deltree "$work/gold.xdf" "$d" >/dev/null 2>&1
-done
-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
+# launching so the log lands there too.
+x68kBootDisk "$work/gold.xdf" 'B:' 'B:\UBER.X' || exit 1
+x68kDataDisk "$work/data.xdf" || exit 1
+x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
@@ -165,7 +151,8 @@ emu.register_frame_done(function()
end)
LUA
-runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
+# In the work dir, so MAME cfg/ and nvram/ never persist between runs.
+runLog=$(cd "$work" && timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/gold.lua" &1 | grep '^GOLD')
@@ -183,7 +170,7 @@ if ! echo "$runLog" | grep -q 'done-screen'; then
exit 1
fi
-python3 "$repo/tools/xdftool.py" extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
+x68kXdf extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
echo "verify-x68000-golden: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
exit 1
}
diff --git a/scripts/verify-x68000-input.sh b/scripts/verify-x68000-input.sh
index 5be8ebf..c926006 100755
--- a/scripts/verify-x68000-input.sh
+++ b/scripts/verify-x68000-input.sh
@@ -1,15 +1,15 @@
#!/usr/bin/env bash
# verify-x68000-input.sh - Headless typed-character acceptance gate for
# the X68000 port. Boots Human68k under the patched MAME with KEYS.X in
-# AUTOEXEC.BAT, waits for the example's A5A5 sentinel to appear in
-# GVRAM, types the RetroNet definition-of-done string on the emulated
-# keyboard, and reads the verification scanline back out of GVRAM.
+# AUTOEXEC.BAT, waits for the example's A5A5 sentinel to appear on the
+# screen, types the RetroNet definition-of-done string on the emulated
+# keyboard, and reads the verification scanline back out of text VRAM.
# KEYS encodes every character it received from jlInputGetChar as nibble
# pixel pairs, so the row must equal the typed ASCII exactly.
#
-# 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). Verification row = stage y 198 -> GVRAM y 354.
+# Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128
+# bytes per row, leftmost pixel in bit 7; the 320x200 stage is centred at
+# origin (96, 156). Verification row = stage y 198 -> screen y 354.
#
# bash scripts/verify-x68000-input.sh
set -uo pipefail
@@ -19,24 +19,19 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-TEMPLATE="$X68K_DIR/HUMAN302.XDF"
+source "$repo/scripts/lib/x68kDisk.sh"
WALL="${X68K_INPUT_WALL:-900}"
target="192.168.1.10:6510"
-[ -f "$TEMPLATE" ] || { echo "verify-x68000-input: missing $TEMPLATE" >&2; exit 2; }
+[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-input: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-input: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68input.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
+x68kBootDisk "$work/boot.xdf" 'A:\KEYS.X' || exit 1
+x68kXdf add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
cat > "$work/input.lua" <&2; exit 2; }
+[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-mouse: missing $X68K_SYSTEM_DISK" >&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
+x68kBootDisk "$work/boot.xdf" 'A:\KEYS.X' || exit 1
+x68kXdf add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
cat > "$work/mouse.lua" <<'LUA'
local cpu = manager.machine.devices[":maincpu"]
diff --git a/scripts/verify-x68000-serial.sh b/scripts/verify-x68000-serial.sh
index 6510433..3d8928b 100755
--- a/scripts/verify-x68000-serial.sh
+++ b/scripts/verify-x68000-serial.sh
@@ -16,26 +16,39 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
-TEMPLATE="$X68K_DIR/HUMAN302.XDF"
+source "$repo/scripts/lib/x68kDisk.sh"
ROMS="$X68K_DIR/roms"
PORT="${X68K_SERIAL_PORT:-6800}"
+# SERTEST (no jlInit, banner then echo) passes the same probe/echo test.
+PROG="${X68K_SERIAL_PROG:-SERIAL}"
+# The host end's line, "BAUD DATABITS PARITY STOPBITS" (e.g. "4800 7 E 1").
+# Empty keeps MAME's null_modem default, 9600 8 N 1. A guest that did not
+# apply its jlSerialConfigT would still be at the default, so running this
+# with a program built for another line proves the settings reach the SCC.
+# Build that program with the matching -DSERIAL_BAUD=... (and SERIAL_DATABITS,
+# SERIAL_PARITY, SERIAL_STOPBITS; see examples/serial/serial.c) and pass its
+# name in X68K_SERIAL_PROG. 19200 and 38400 need RSDRV.SYS, which the boot
+# disk loads (scripts/lib/x68kDisk.sh).
+LINE="${X68K_SERIAL_LINE:-}"
PROBE="${X68K_SERIAL_PROBE:-JOEYLIB-X68K}"
MAXWAIT="${X68K_SERIAL_TIMEOUT:-420}"
-for f in "$MAME" "$TEMPLATE"; do
+for f in "$MAME" "$X68K_SYSTEM_DISK"; do
[ -e "$f" ] || { echo "verify-x68000-serial: missing $f" >&2; exit 2; }
done
work=$(mktemp -d -t joey-x68ser.XXXXXX)
-trap 'rm -rf "$work"; kill %1 2>/dev/null' EXIT
+peer=""
+mame=""
+cleanup() {
+ joeyStop "$mame" "$peer"
+ rm -rf "$work"
+}
+trap cleanup EXIT
-# Autorun SERIAL.X. No ECHO OFF so a load failure is visible in a screenshot.
-printf 'A:\\SERIAL.X\r\n\x1a' > "$work/AUTOEXEC.BAT"
-cp "$TEMPLATE" "$work/serial.xdf"
-python3 "$repo/tools/xdftool.py" add "$work/serial.xdf" \
- "$repo/build/x68000/bin/SERIAL.X" SERIAL.X >/dev/null
-python3 "$repo/tools/xdftool.py" add "$work/serial.xdf" \
- "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
+# Autorun the program. No ECHO OFF so a load failure is visible in a screenshot.
+x68kBootDisk "$work/serial.xdf" "A:\\$PROG.X" || exit 1
+x68kXdf add "$work/serial.xdf" "$repo/build/x68000/bin/$PROG.X" "$PROG.X" >/dev/null || exit 1
# Host end of the link. MAME's null_modem socket form CONNECTS, so we listen.
python3 "$repo/scripts/x68kSerialPeer.py" \
@@ -44,13 +57,45 @@ python3 "$repo/scripts/x68kSerialPeer.py" \
peer=$!
sleep 2
-timeout -s KILL "$MAXWAIT" "$MAME" x68000 \
+# From the work dir, so MAME's cfg/ and nvram/ land there, not in the caller's
+# cwd where the next run would read them back.
+cd "$work" || exit 1
+if [ -n "$LINE" ]; then
+ python3 - "$LINE" > "$work/x68000.cfg" <<'PY' || exit 2
+import sys
+# MAME's RS232_* enum values (src/devices/bus/rs232/rs232.h).
+BAUD = {"300": 2, "600": 3, "1200": 4, "2400": 5, "4800": 6, "9600": 7, "19200": 9, "38400": 11}
+DATA = {"5": 0, "6": 1, "7": 2, "8": 3}
+PARITY = {"N": 0, "O": 1, "E": 2}
+STOP = {"1": 1, "2": 3}
+baud, data, parity, stop = sys.argv[1].split()
+tag = ":rs232c:null_modem:"
+ports = [("RS232_TXBAUD", 7, BAUD[baud]), ("RS232_RXBAUD", 7, BAUD[baud]),
+ ("RS232_DATABITS", 3, DATA[data]), ("RS232_PARITY", 0, PARITY[parity.upper()]),
+ ("RS232_STOPBITS", 1, STOP[stop])]
+print('\n\n \n ')
+for name, default, value in ports:
+ print(f' ')
+print(' \n \n')
+PY
+ mkdir -p "$work/cfg"
+ mv "$work/x68000.cfg" "$work/cfg/x68000.cfg"
+fi
+# MAME runs until the peer has its verdict and is then stopped by PID: waiting
+# out a whole run once the probe has echoed cost minutes per gate. The peer's
+# own deadline (MAXWAIT) bounds the run, and MAME is started directly rather
+# than under timeout, so $! is MAME itself and stopping it orphans nothing.
+"$MAME" x68000 \
-bios ipl10 -rompath "$ROMS" \
-flop1 "$work/serial.xdf" \
-rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \
- -video none -sound none -nothrottle /dev/null 2>&1
+ -video none -sound none -nothrottle /dev/null 2>&1 &
+mame=$!
wait "$peer"
rc=$?
+peer=""
+joeyStop "$mame"
+mame=""
cat "$work/peer.out"
exit $rc
diff --git a/src/amiga/serial.c b/src/amiga/serial.c
index 596fec9..8d9b54c 100644
--- a/src/amiga/serial.c
+++ b/src/amiga/serial.c
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
@@ -29,6 +30,11 @@
// Largest single async transmit; a longer jlSerialWrite is split across frames.
#define TX_STAGE_SIZE 1024
+// CIA-B, whose port A carries the serial control lines. DTR is bit 7 (CIAF_COMDTR), active low, and
+// is only driven while its DDRA bit makes it an output: an input floats high, which the line driver
+// turns into DTR off.
+#define AMIGA_CIAB ((volatile struct CIA *)0xBFD000)
+
// ----- Module state -----
@@ -38,16 +44,51 @@ static struct IOExtSer *gWriteReq = NULL;
static bool gOpen = false;
static bool gWritePending = false;
static UBYTE gTxStage[TX_STAGE_SIZE];
+static bool gDtrRaised = false; // jlpSerialOpen raised DTR and saved the bits below
+static UBYTE gPreOpenPra = 0u; // CIA-B PRA / DDRA DTR bits before that open
+static UBYTE gPreOpenDdra = 0u;
// ----- Prototypes -----
+static bool serialDtrIsOn(void);
+static void serialDtrRestore(void);
+static void serialDtrSet(bool on);
static void serialReapWrite(void);
-static void serialTeardown(void);
+static void serialTeardown(bool keepDtr);
// ----- Helpers -----
+static bool serialDtrIsOn(void) {
+ return (AMIGA_CIAB->ciaddra & CIAF_COMDTR) != 0u && (AMIGA_CIAB->ciapra & CIAF_COMDTR) == 0u;
+}
+
+
+// Put DTR back the way jlpSerialOpen found it (a failed open never happened).
+static void serialDtrRestore(void) {
+ Disable();
+ AMIGA_CIAB->ciapra = (UBYTE)((AMIGA_CIAB->ciapra & (UBYTE)~CIAF_COMDTR) | gPreOpenPra);
+ AMIGA_CIAB->ciaddra = (UBYTE)((AMIGA_CIAB->ciaddra & (UBYTE)~CIAF_COMDTR) | gPreOpenDdra);
+ Enable();
+}
+
+
+// Drive DTR on or off, leaving every other port A line alone. PRA first, so the pin only becomes an
+// output once its latch already holds the new level. Disabled, because both are read-modify-writes
+// of registers whose other bits belong to interrupt-level code (the parallel port's control lines).
+static void serialDtrSet(bool on) {
+ Disable();
+ if (on) {
+ AMIGA_CIAB->ciapra = (UBYTE)(AMIGA_CIAB->ciapra & (UBYTE)~CIAF_COMDTR);
+ } else {
+ AMIGA_CIAB->ciapra = (UBYTE)(AMIGA_CIAB->ciapra | CIAF_COMDTR);
+ }
+ AMIGA_CIAB->ciaddra = (UBYTE)(AMIGA_CIAB->ciaddra | CIAF_COMDTR);
+ Enable();
+}
+
+
// Collect a completed async write so the request + staging buffer free up.
static void serialReapWrite(void) {
if (gWritePending && CheckIO((struct IORequest *)gWriteReq) != NULL) {
@@ -57,15 +98,44 @@ static void serialReapWrite(void) {
}
-static void serialTeardown(void) {
+// Release everything jlpSerialOpen acquired. keepDtr is a close (not a failed open): DTR must come out
+// of it the way it went in.
+//
+// Close must not drop DTR: that hangs up a modem and tells a BBS the caller left, which a door or a
+// terminal handing the line back must never do. serial.device drops it whenever it was off before
+// the unit's first open: every open saves CIA-B ciapra/ciaddra bits 3-7 and asserts DTR and RTS, and
+// the unit's last close writes the saved bits back (serial 37.4, OS 3.1's DEVS:serial.device, read
+// from its code and MEASURED under fs-uae). jlpSerialOpen therefore raises DTR BEFORE OpenDevice, so
+// the snapshot says on and the last close leaves it on, with no gap at all. Two cases remain, and
+// in both DTR comes out of CloseDevice different from how it went in, so it is put straight back
+// (Forbid keeps any other task from opening the unit in between): a unit another program opened
+// first, whose snapshot had DTR off, and a program that dropped DTR with jlSerialSetDtr -- a close
+// must leave a hung-up line down, but the snapshot says on. Either way the line holds the wrong
+// level for the rest of serial.device's close, at most ~0.7 ms on a 7 MHz A500: far below the 50 ms
+// a Hayes modem's S25 needs before it hangs up, and a line already hung up only rises briefly.
+// While another opener still holds the unit this close is not the last and the lines are
+// untouched. A failed open puts DTR back as it found it -- that open never happened.
+static void serialTeardown(bool keepDtr) {
+ bool dtrWasOn;
+
if (gWritePending) {
AbortIO((struct IORequest *)gWriteReq);
WaitIO((struct IORequest *)gWriteReq);
gWritePending = false;
}
if (gReadReq != NULL && gReadReq->IOSer.io_Device != NULL) {
+ Forbid();
+ dtrWasOn = serialDtrIsOn();
CloseDevice((struct IORequest *)gReadReq); // shared open; closes for both
+ if (keepDtr && serialDtrIsOn() != dtrWasOn) {
+ serialDtrSet(dtrWasOn);
+ }
+ Permit();
}
+ if (!keepDtr && gDtrRaised) {
+ serialDtrRestore();
+ }
+ gDtrRaised = false;
if (gWriteReq != NULL) {
DeleteIORequest((struct IORequest *)gWriteReq);
gWriteReq = NULL;
@@ -93,8 +163,9 @@ uint16_t jlpSerialAvailable(void) {
}
+// Leaves DTR as it was: see serialTeardown.
void jlpSerialClose(void) {
- serialTeardown();
+ serialTeardown(true);
gOpen = false;
}
@@ -133,7 +204,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gReadReq == NULL) {
gAmigaSerialFailStage = 2u;
- serialTeardown();
+ serialTeardown(false);
return false;
}
@@ -158,6 +229,11 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
// 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;
+ // DTR goes up before the open, so serial.device's snapshot records it on (see serialTeardown).
+ gPreOpenPra = (UBYTE)(AMIGA_CIAB->ciapra & CIAF_COMDTR);
+ gPreOpenDdra = (UBYTE)(AMIGA_CIAB->ciaddra & CIAF_COMDTR);
+ gDtrRaised = true;
+ serialDtrSet(true);
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
@@ -172,7 +248,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
// 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();
+ serialTeardown(false);
return false;
}
gReadReq->io_Baud = config->baud;
@@ -190,7 +266,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
if (DoIO((struct IORequest *)gReadReq) != 0) {
gAmigaSerialFailStage = 4u;
gAmigaSerialFailErr = gReadReq->IOSer.io_Error;
- serialTeardown();
+ serialTeardown(false);
return false;
}
@@ -198,7 +274,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gWriteReq == NULL) {
gAmigaSerialFailStage = 5u;
- serialTeardown();
+ serialTeardown(false);
return false;
}
*gWriteReq = *gReadReq;
@@ -236,6 +312,16 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
}
+// DTR on demand (jlSerialSetDtr, how a program hangs up): CIA-B PRA bit 7. serial.device only touches
+// DTR when the unit is opened and closed, so the level set here holds until close, which keeps it.
+void jlpSerialSetDtr(bool on) {
+ if (!gOpen) {
+ return;
+ }
+ serialDtrSet(on);
+}
+
+
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
uint16_t i;
diff --git a/src/atarist/audio.c b/src/atarist/audio.c
index df46f42..35dde40 100644
--- a/src/atarist/audio.c
+++ b/src/atarist/audio.c
@@ -230,15 +230,15 @@ static long uninstallTimerA(void) {
gOldTimerAVec = NULL;
}
// Channel A belongs to the PSG again. Replay voice 0's latched state,
- // which both clears the DAC's residual level (so the handoff back to TOS
- // leaves no DC on the speaker) and restores a note the music player set
- // while the pump owned the channel. Without the replay voice 0 would stay
- // dead until its next note EVENT, because src/core/music.c only reprograms
- // a voice when one arrives -- a held note would never come back.
- gYmVoice = 0u;
- gYmFreqHz = gVoiceAFreqHz;
- gYmAtten = gVoiceAAtten;
- return applyYmVoice();
+ // which both clears the DAC's residual level (so the handoff back to TOS
+ // leaves no DC on the speaker) and restores a note the music player set
+ // while the pump owned the channel. Without the replay voice 0 would stay
+ // dead until its next note EVENT, because src/core/music.c only reprograms
+ // a voice when one arrives -- a held note would never come back.
+ gYmVoice = 0u;
+ gYmFreqHz = gVoiceAFreqHz;
+ gYmAtten = gVoiceAAtten;
+ return applyYmVoice();
}
@@ -528,9 +528,9 @@ void jlpAudioCriticalExit(void) {
void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
- // Same Supexec-with-stashed-args shape as jlpAudioVoice; noise
- // inherits the MOD-collision caveat documented there (the PWM
- // pump's installTimerA force-silences the whole PSG on start).
+ // Same Supexec-with-stashed-args shape as jlpAudioVoice. Noise plays
+ // through channel C, so it keeps playing while the PWM pump owns
+ // channel A (installTimerA takes channel A only).
gYmNoisePitch = pitch;
gYmNoiseAtten = atten;
Supexec(applyYmNoise);
diff --git a/src/atarist/serial.c b/src/atarist/serial.c
index 43705a4..8fcee94 100644
--- a/src/atarist/serial.c
+++ b/src/atarist/serial.c
@@ -37,6 +37,12 @@
#define ST_RSCONF_INQUIRE ((int16_t)-2)
#define ST_BAUD_CODE_MAX 15
+// DTR is YM2149 port A bit 4, set and cleared through XBIOS Ongibit/Offgibit. The line driver inverts
+// it: the bit SET is DTR OFF, so Ongibit(ST_GI_DTR_OFF) hangs up and Offgibit(ST_GI_DTR_ON_MASK) --
+// every bit but 4 kept -- raises it.
+#define ST_GI_DTR_OFF 0x10
+#define ST_GI_DTR_ON_MASK 0xEF
+
// ----- Module state -----
@@ -197,6 +203,10 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
baudCode = serialBaudCode(config->baud);
ucr = serialUcr(config);
+ // Raise DTR, as the open does on every port: TOS only sets it at boot, so a line an earlier
+ // jlSerialSetDtr(false) hung up would otherwise stay down.
+ Offgibit(ST_GI_DTR_ON_MASK);
+
// 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
@@ -233,6 +243,18 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
}
+// DTR on demand (jlSerialSetDtr, how a program hangs up). Neither Rsconf nor anything else in EmuTOS
+// touches port A bit 4 after boot (Hatari psg_write + xbios trace), and close does not either, so a
+// dropped DTR stays dropped.
+void jlpSerialSetDtr(bool on) {
+ if (on) {
+ Offgibit(ST_GI_DTR_ON_MASK);
+ } else {
+ Ongibit(ST_GI_DTR_OFF);
+ }
+}
+
+
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c
index 0f7cc51..e4e362e 100644
--- a/src/core/assetLoad.c
+++ b/src/core/assetLoad.c
@@ -100,19 +100,17 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1];
#elif defined(JOEYLIB_PLATFORM_BLANK)
#define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops)
#elif defined(JOEYLIB_PLATFORM_X68000)
-// Chunky 4bpp while the port is on the generic backend, so it shares DOS's
-// target byte -- the payload is bytes, so the endian difference does not
-// matter. This gets its own target the moment the storage model is decided
-// (a text-plane port would bake Amiga-shaped tiles and want target 1).
-#define EXPECTED_TILE_TARGET 3u
+// Plane-major like the Amiga (src/x68000/x68kTile.h reads jlTileT.pixels as
+// 4 planes x 8 rows), baked by assetbake --target x68000.
+#define EXPECTED_TILE_TARGET 5u
#else
#error "Unknown platform for asset loader"
#endif
// Per-platform target byte for .spc files (pre-compiled sprite code, so
-// it is per MACHINE CODE, not per pixel format: the X68000 shares DOS's
-// chunky tile byte above but runs 68k planar sprite routines, and must
-// never accept an x86 bank). Mirrors SPC_TARGET in tools/spritebake.
+// it is per MACHINE CODE, not per pixel format: every port's banks carry
+// their own byte, so a bank baked for any other build is refused). Mirrors
+// SPC_TARGET in tools/spritebake.
#if defined(JOEYLIB_PLATFORM_AMIGA)
#define EXPECTED_SPC_TARGET 1u
#elif defined(JOEYLIB_PLATFORM_ATARIST)
diff --git a/src/core/port.h b/src/core/port.h
index 056358c..81ab8e1 100644
--- a/src/core/port.h
+++ b/src/core/port.h
@@ -83,7 +83,6 @@ extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, ui
extern void iigsTilePasteGlyphMark(const uint8_t *bits, uint16_t bx, uint16_t by, uint16_t fgColor, uint16_t bgColor);
extern void iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void iigsCopyRectRows (uint8_t *dst, uint16_t srcLo, uint16_t srcBank, uint16_t rowBytes, uint16_t rows);
-extern void iigsQuitGS (void);
extern void iigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
extern void iigsFillRectNoMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp);
@@ -294,6 +293,13 @@ void iigsMemCopy(void *dst, const void *src, uint32_t bytes);
#define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n))
// The one Memory Manager owner ID every IIgs allocation uses (hal.c).
uint16_t iigsMemUserId(void);
+// Interrupt-latency bound of the long interrupts-off paths (hal.c): a
+// client that cannot wait ~17 ms for its interrupt holds the tight bound
+// while it needs it. Counted, so clients nest. gIigsIrqTight is nonzero
+// while any client holds it, for the C paths that bound themselves.
+extern uint16_t gIigsIrqTight;
+void iigsIrqLatencyRelax(void);
+void iigsIrqLatencyTighten(void);
// SFX streams go to NTPstreamsound as unsigned bytes with 0x00 forbidden
// (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands
@@ -1070,6 +1076,16 @@ void jlpGenericSerialFlush(void);
#define jlpSerialFlush() jlpGenericSerialFlush()
#endif
#endif
+// Application-driven DTR (jlSerialSetDtr), how a program hangs up. The generic stub does nothing, like
+// the RTS one below: a port with no controllable DTR line has nothing to drop.
+void jlpGenericSerialSetDtr(bool on);
+#if !defined(jlpSerialSetDtr)
+ #if defined(JL_HAS_SERIAL_SET_DTR)
+ void jlpSerialSetDtr(bool on);
+ #else
+ #define jlpSerialSetDtr(_o) jlpGenericSerialSetDtr((_o))
+ #endif
+#endif
// Application-driven RTS (jlSerialSetRts). The generic stub does nothing, which is the honest answer
// for a port whose RTS line is not controllable - a caller drives this unconditionally.
void jlpGenericSerialSetRts(bool on);
diff --git a/src/core/serial.c b/src/core/serial.c
index b024307..85795c7 100644
--- a/src/core/serial.c
+++ b/src/core/serial.c
@@ -7,8 +7,15 @@
// Single open port at a time (module-static gate), matching the add-on's
// stated contract. Config is normalized + range-checked here so every per-port
// HAL can assume a sane jlSerialConfigT.
+//
+// A port the program leaves open is closed at exit by an atexit handler the
+// first successful open registers: DOS, the X68000 and the IIgs point a
+// hardware interrupt vector into the program while a port is open, and the
+// program's memory is about to be freed. Closing leaves DTR as it was, so a
+// connection stays up after the program ends.
#include
+#include
#include "joey/serial.h"
#include "port.h"
@@ -25,7 +32,13 @@
// ----- Module state -----
-static bool gSerialOpen = false;
+static bool gSerialOpen = false;
+static bool gSerialAtExitOn = false;
+
+
+// ----- Prototypes -----
+
+static void serialAtExit(void);
// ----- Public API (alphabetical) -----
@@ -79,6 +92,9 @@ bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
}
gSerialOpen = jlpSerialOpen(device, &cfg);
+ if (gSerialOpen && !gSerialAtExitOn) {
+ gSerialAtExitOn = atexit(serialAtExit) == 0;
+ }
return gSerialOpen;
}
@@ -109,6 +125,14 @@ int16_t jlSerialReadByte(void) {
}
+void jlSerialSetDtr(bool on) {
+ if (!gSerialOpen) {
+ return;
+ }
+ jlpSerialSetDtr(on);
+}
+
+
void jlSerialSetRts(bool on) {
if (!gSerialOpen) {
return;
@@ -128,3 +152,11 @@ uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) {
bool jlSerialWriteByte(uint8_t b) {
return jlSerialWrite(&b, 1u) == 1u;
}
+
+
+// ----- Internal -----
+
+// The atexit handler (see the header comment): closes a port left open.
+static void serialAtExit(void) {
+ jlSerialClose();
+}
diff --git a/src/dos/serial.c b/src/dos/serial.c
index b1ab6a4..e6b70cc 100644
--- a/src/dos/serial.c
+++ b/src/dos/serial.c
@@ -83,6 +83,7 @@ static _go32_dpmi_seginfo gNewIrqVec;
static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq);
static int picVectorForIrq(uint8_t irq);
+static void serialMcrBit(uint8_t bit, bool on);
static bool serialResolvePort(jlSerialDeviceE device, const jlSerialConfigT *cfg, uint16_t *outBase, uint8_t *outIrq);
static void serialRxIsr(void);
@@ -149,6 +150,28 @@ static void serialRxIsr(void) {
}
+// ----- Modem control -----
+
+// Set or clear one MCR bit -- DTR or RTS for jlSerialSetDtr/jlSerialSetRts, OUT2 at close --
+// read-modify-write so every other bit keeps whatever the chip holds: above all OUT2, which physically
+// gates this UART's interrupt to the PIC (clearing it along with a line would silence the receiver
+// entirely), and DTR, which close must leave as it is.
+static void serialMcrBit(uint8_t bit, bool on) {
+ uint8_t mcr;
+
+ if (gComBase == 0u) {
+ return;
+ }
+ mcr = (uint8_t)inportb((int)(gComBase + UART_MCR));
+ if (on) {
+ mcr = (uint8_t)(mcr | bit);
+ } else {
+ mcr = (uint8_t)(mcr & (uint8_t)~bit);
+ }
+ outportb((int)(gComBase + UART_MCR), mcr);
+}
+
+
// ----- Port resolution -----
// DEFAULT/MODEM/PRINTER -> COM1; SLOT -> COM (1..4). The canonical COM
@@ -179,6 +202,10 @@ uint16_t jlpSerialAvailable(void) {
}
+// Close hands the IRQ back but NOT the line: only OUT2 (the UART's IRQ gate to the PIC) is cleared,
+// and DTR and RTS stay exactly as they are. Dropping DTR hangs up a modem and tells a BBS the caller
+// left, which a door or a terminal handing the line back must never do -- the same reason a FOSSIL
+// driver's deinit (INT 14h function 05h) leaves DTR alone.
void jlpSerialClose(void) {
if (!gOpen) {
return;
@@ -188,7 +215,7 @@ void jlpSerialClose(void) {
picMaskIrq(gComIrq);
outportb((int)(gComBase + UART_IER), 0); // disable UART interrupts
- outportb((int)(gComBase + UART_MCR), 0); // drop DTR/RTS/OUT2
+ serialMcrBit(MCR_OUT2, false); // IRQ gate off; DTR and RTS stay as they are
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gOldIrqVec);
_go32_dpmi_free_iret_wrapper(&gNewIrqVec);
gIrqHooked = false;
@@ -208,11 +235,14 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
uint8_t irq;
uint16_t divisor;
uint8_t lcr;
+ uint8_t mcrBefore;
int vector;
if (gOpen || !serialResolvePort(device, config, &base, &irq)) {
return false;
}
+ // A failed open puts the modem lines back the way it found them.
+ mcrBefore = (uint8_t)inportb((int)(base + UART_MCR));
divisor = (uint16_t)(UART_BAUD_BASE / config->baud);
if (divisor == 0u) {
@@ -251,7 +281,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gNewIrqVec.pm_offset = (unsigned long)serialRxIsr;
gNewIrqVec.pm_selector = _go32_my_cs();
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
- outportb((int)(base + UART_MCR), 0);
+ outportb((int)(base + UART_MCR), mcrBefore);
return false;
}
// Page-lock the ISR + every object it touches (fault at IRQ time hangs).
@@ -278,23 +308,6 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
}
-// RTS on demand (jlSerialSetRts): MCR bit 1. DTR and OUT2 are preserved - OUT2 physically gates this
-// UART's interrupt to the PIC, so clearing it while dropping RTS would silence the receiver entirely,
-// which is the opposite of what a flow-control pause is for.
-void jlpSerialSetRts(bool on) {
- uint8_t mcr;
-
- if (gComBase == 0u) {
- return;
- }
- mcr = (uint8_t)(MCR_DTR | MCR_OUT2);
- if (on) {
- mcr = (uint8_t)(mcr | MCR_RTS);
- }
- outportb((int)(gComBase + UART_MCR), mcr);
-}
-
-
void jlpSerialPoll(void) {
// The RX IRQ fills the ring in the background; nothing to pump.
}
@@ -313,6 +326,18 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
}
+// DTR on demand (jlSerialSetDtr, how a program hangs up): MCR bit 0.
+void jlpSerialSetDtr(bool on) {
+ serialMcrBit(MCR_DTR, on);
+}
+
+
+// RTS on demand (jlSerialSetRts): MCR bit 1.
+void jlpSerialSetRts(bool on) {
+ serialMcrBit(MCR_RTS, on);
+}
+
+
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
diff --git a/src/generic/genericSerial.c b/src/generic/genericSerial.c
index ba9f6d0..2429752 100644
--- a/src/generic/genericSerial.c
+++ b/src/generic/genericSerial.c
@@ -34,6 +34,12 @@ void jlpGenericSerialPoll(void) {
}
+// A port with no controllable DTR line: nothing to raise or drop, same reasoning as the RTS stub below.
+void jlpGenericSerialSetDtr(bool on) {
+ (void)on;
+}
+
+
// A port with no controllable RTS line: doing nothing is the honest answer, and it lets a caller drive
// jlSerialSetRts unconditionally instead of guarding every call site with a platform test.
void jlpGenericSerialSetRts(bool on) {
diff --git a/src/iigs/audio_full.c b/src/iigs/audio_full.c
index 2f573bb..96bf811 100644
--- a/src/iigs/audio_full.c
+++ b/src/iigs/audio_full.c
@@ -49,6 +49,13 @@ typedef void *Pointer;
#define NTP_ENTRY_PLAY 3
#define NTP_ENTRY_STOP 6
#define NTP_ENTRY_STREAMSOUND 24
+// The word JoeyLib's build adds right after the jump table (make/support/
+// iigs/ntpIrqWindow.py). Nonzero bounds NTP's two long interrupts-off
+// stretches: NTPstreamsound takes interrupts between the two 256-byte
+// halves of its first DOC fill (~2.4 ms masked otherwise), and the DOC
+// interrupt refills one streaming oscillator per interrupt instead of
+// every one that is due (~1 ms apiece).
+#define NTP_IRQ_WINDOW 27
// The silent module handed to NTPprepare when no song is playing (see
// ntpArm). One track, one never-triggered streamed instrument, one
@@ -778,10 +785,17 @@ static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint
// the machine down; guarding on gNTPPlaying instead would be safe but
// would silence every SFX in an application that plays no music, which
// is all of them today.
+//
+// While hal.c's interrupt-latency bound is tight (gIigsIrqTight, e.g. a
+// serial port is open), NTP is asked to bound itself (NTP_IRQ_WINDOW), so
+// no stretch of the call, and no refill interrupt, masks for more than
+// ~1.4 ms. The word is set before every trigger, so it follows the bound
+// within one sound.
static void sfxTrigger(uint32_t structAddr) {
if (!gNTPArmed) {
return;
}
+ *(volatile uint16_t *)(gNTPBase + NTP_IRQ_WINDOW) = gIigsIrqTight;
ntpCall(gNTPBase + NTP_ENTRY_STREAMSOUND,
(uint16_t)(structAddr & 0xFFFFu),
(uint16_t)((structAddr >> 16) & 0xFFu),
diff --git a/src/iigs/hal.c b/src/iigs/hal.c
index dee4119..dee52d4 100644
--- a/src/iigs/hal.c
+++ b/src/iigs/hal.c
@@ -134,8 +134,6 @@ 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);
@@ -183,6 +181,21 @@ extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1
// in jlpInit so the visible bezel matches the cleared SHR background.
#define BORDER_COLOR_MASK 0xF0
+// Interrupt windows of the two long interrupts-off paths in joeyDraw.s
+// (see gIigsPresentChunkRows). Loose is what they always did and costs
+// nothing; tight fits the Z8530 SCC's 3-byte receive FIFO at 9600 baud
+// (~3.1 ms): 4 present rows ~1.8 ms, 12 clear blocks ~1.4 ms.
+#define IRQ_PRESENT_ROWS_LOOSE 40u
+#define IRQ_PRESENT_ROWS_TIGHT 4u
+#define IRQ_CLEAR_BLOCKS_LOOSE 200u
+#define IRQ_CLEAR_BLOCKS_TIGHT 12u
+
+// Video beam ticks a frame (VERTCNT/HORIZCNT: 65 positions a line), for
+// the tight joystick read's beam stamps (iigsJoyPass).
+#define BEAM_TICKS_PER_LINE 65u
+#define BEAM_LINES_NTSC 262u
+#define BEAM_LINES_PAL 312u
+
// ----- Module state -----
static uint8_t gPreviousNewVideo = 0;
@@ -190,6 +203,25 @@ static uint8_t gPreviousBorder = 0;
static uint8_t gPreviousShadow = 0;
static bool gModeSet = false;
+// Interrupt-latency bound, read by joeyDraw.s (so not static, and BSS
+// like every other C global the asm reads DBR-relative; jlpInit sets
+// the loose values). The present's chunked PEI slam re-enables
+// interrupts every gIigsPresentChunkRows rows (~0.42 ms a row), the
+// stage fast clear every gIigsClearChunkBlocks 160-byte blocks
+// (~0.12 ms a block). A client whose interrupt cannot wait ~17 ms holds
+// the tight bound while it needs it; the holds are counted, so clients
+// nest. Tight costs a full-screen present ~5% (83.1 -> 87.2 ms).
+// gIigsIrqTight tells the C paths that bound themselves (input.c's
+// joystick read, audio_full.c's NinjaTrackerPlus entries) which way to
+// go. All three follow gIrqTightHolds, through irqLatencyApply alone.
+uint16_t gIigsPresentChunkRows;
+uint16_t gIigsClearChunkBlocks;
+uint16_t gIigsIrqTight;
+static uint16_t gIrqTightHolds = 0u;
+
+// Beam ticks a frame for the joystick asm's frame wrap (jlpInit).
+uint16_t gIigsBeamFrameTicks;
+
// SCB / palette upload skipping is now driven by gStageScbDirty /
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp-
// against-cached-copy approach was costing ~7 ms / frame.
@@ -315,6 +347,7 @@ void jlpBigFree(void *p) {
static void claimStagePixels(void);
+static void irqLatencyApply(bool tight);
bool jlpInit(const jlConfigT *config) {
uint16_t tick;
@@ -353,8 +386,10 @@ bool jlpInit(const jlConfigT *config) {
// bank $E1 is unreliable from jlpInit's calling context, so we
// don't try it here.
iigsInitRowLut();
- gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u;
- gModeSet = true;
+ irqLatencyApply(gIrqTightHolds != 0u);
+ gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u;
+ gIigsBeamFrameTicks = (uint16_t)(((gFrameHz == 50u) ? BEAM_LINES_PAL : BEAM_LINES_NTSC) * BEAM_TICKS_PER_LINE);
+ gModeSet = true;
return true;
}
@@ -396,25 +431,28 @@ 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();
}
-// GS/OS QuitGS, issued from the pure-constant asm routine in
-// joeyDraw.s (iigsQuitGS). The first cut built the call stub in a C
-// BSS array and hit the cross-bank DBR trap on the 10-segment STAXI
-// link: the byte stores went DB-relative into bank $08 while the
-// `jsl gQuitStub` resolved to the array's true bank $10 -- the JSL
-// executed zeros and BRK'd. The asm routine has no relocations; its
-// parameter block is the fixed scratch word at $E1:9DCC (inside the
-// survives-present $E1:9DC8-$9DFD window), written here before the
-// call. QuitGS pCount = 0 = plain quit to the parent (Finder).
-#define IIGS_QUIT_PARM_LO ((volatile uint8_t *)0xE19DCCL)
-#define IIGS_QUIT_PARM_HI ((volatile uint8_t *)0xE19DCDL)
-#define IIGS_QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L)
+// Release one hold on the tight interrupt-latency bound; the last one out
+// puts the loose bound back.
+void iigsIrqLatencyRelax(void) {
+ if (gIrqTightHolds == 0u) {
+ return;
+ }
+ gIrqTightHolds--;
+ irqLatencyApply(gIrqTightHolds != 0u);
+}
+
+
+// Take a hold on the tight interrupt-latency bound (see
+// gIigsPresentChunkRows).
+void iigsIrqLatencyTighten(void) {
+ gIrqTightHolds++;
+ irqLatencyApply(true);
+}
// jlMemCopy: MVN moves of up to a bank at a time. MVN's X and Y wrap
@@ -476,21 +514,11 @@ void jlpShutdown(void) {
*IIGS_SHADOW_REG = gPreviousShadow;
gModeSet = false;
}
- // Quit to GS/OS HERE instead of returning: the llvm-mos runtime's
- // post-main exit path is broken (a php/rtl thunk at segment offset
- // $0072 mis-balances the stack and the machine COPs to the monitor
- // at $00:0002 -- reproduced identically for KEYS and STAXI, see
- // claudeDocs/LLVM816-ASKS.md #6). jlShutdown is the last thing every example
- // calls, so ending the process here is equivalent and safe; on
- // IIgs jlShutdown therefore does not return. If QuitGS itself
- // errors, fall through to the old (crashing) path rather than
- // hang. The mailbox marker lets the MAME harness verify the call
- // was reached ($AA) or returned ($EE).
- *IIGS_QUIT_PARM_LO = 0u; // QuitGS pCount = 0
- *IIGS_QUIT_PARM_HI = 0u;
- *IIGS_QUIT_MAILBOX = 0xAAu;
- iigsQuitGS();
- *IIGS_QUIT_MAILBOX = 0xEEu;
+ // Returns like every port's: main's return then goes through the
+ // runtime's exit(), which runs the atexit handlers (an open serial
+ // port's close among them) before GS/OS QUIT. This used to QuitGS
+ // itself because that runtime exit path crashed into the monitor
+ // (LLVM816-ASKS #6, fixed 2026-09-28).
}
@@ -559,6 +587,15 @@ static void claimStagePixels(void) {
}
+// Point the chunked-SEI asm, and the C paths that bound themselves, at
+// the tight or the loose interrupt window.
+static void irqLatencyApply(bool tight) {
+ gIigsPresentChunkRows = tight ? IRQ_PRESENT_ROWS_TIGHT : IRQ_PRESENT_ROWS_LOOSE;
+ gIigsClearChunkBlocks = tight ? IRQ_CLEAR_BLOCKS_TIGHT : IRQ_CLEAR_BLOCKS_LOOSE;
+ gIigsIrqTight = tight ? 1u : 0u;
+}
+
+
uint8_t *jlpStageAllocPixels(void) {
return IIGS_STAGE_PIXELS;
}
@@ -642,10 +679,7 @@ void jlpWaitVBL(void) {
tick = iigsGetTickWord();
while (iigsGetTickWord() == 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();
+ // spin until the heartbeat advances
}
}
diff --git a/src/iigs/input.c b/src/iigs/input.c
index 8a8c7f7..e876f25 100644
--- a/src/iigs/input.c
+++ b/src/iigs/input.c
@@ -55,15 +55,10 @@
#define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L)
#define IIGS_KMSTATUS ((volatile uint8_t *)0x00C027L)
-// Joystick / paddle softswitches.
+// Joystick buttons (the paddles themselves are read by the asm in joeyDraw.s).
#define IIGS_BTN0 ((volatile uint8_t *)0x00C061L)
#define IIGS_BTN1 ((volatile uint8_t *)0x00C062L)
-#define IIGS_PADDLE0 ((volatile uint8_t *)0x00C064L)
-#define IIGS_PADDLE1 ((volatile uint8_t *)0x00C065L)
-#define IIGS_PTRIG ((volatile uint8_t *)0x00C070L)
#define IIGS_BUTTON_BIT 0x80
-#define IIGS_PADDLE_BUSY 0x80
-#define PADDLE_TIMEOUT 256
#define PADDLE_LO_THRESHOLD 64
#define PADDLE_HI_THRESHOLD 192
@@ -113,12 +108,13 @@
// ----- Prototypes -----
-static void buildAsciiTable(void);
-static void pollJoystick(void);
-static void pollMouse(void);
-static void readModifierKeys(void);
-static int8_t signExtend7(uint8_t raw);
-static int8_t thresholdPaddle(uint8_t v);
+static void buildAsciiTable(void);
+static void pollJoystick(void);
+static uint32_t pollJoystickTight(void);
+static void pollMouse(void);
+static void readModifierKeys(void);
+static int8_t signExtend7(uint8_t raw);
+static int8_t thresholdPaddle(uint8_t v);
// ----- Module state -----
@@ -235,8 +231,8 @@ static int8_t thresholdPaddle(uint8_t v) {
// approximates the paddle's 0..255 position (the Apple firmware
// PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to
// charge at full deflection; if NO joystick is wired up, the BUSY bit
-// stays set forever and the busy-wait runs the full PADDLE_TIMEOUT
-// every frame -- ~3 ms wasted per frame on a stick that isn't there.
+// stays set forever and the asm's settle wait times out every frame --
+// ~1 ms wasted per frame on a stick that isn't there.
//
// After JOY_DISCONNECT_THRESHOLD consecutive timeouts we latch the
// stick as absent and stop polling entirely. The app calls
@@ -282,6 +278,53 @@ void jlpJoystickReset(jlJoystickE js) {
// py = (ret >> 16) & 0xFF (JoyY 0..255)
extern uint32_t iigsPollJoystickInner(void);
+// Tight-bound paddle read (pollJoystickTight), used while a client holds
+// hal.c's tight interrupt-latency bound (gIigsIrqTight; the serial
+// add-on does while a port is open). The classic read masks interrupts
+// for its whole count, ~2.8 ms at full deflection; the tight read is
+// built from passes of iigsJoyPass (joeyDraw.s), none of which masks for
+// more than JOY_PASS_COUNTS counts (~1.6 ms with its beam stamp):
+// LOW pass trigger and count at once, on the classic read's beat, so
+// an axis below the limit reads exactly what the classic
+// read gives; an axis still timing at the limit is OPEN.
+// JOY_PASS_FIRST stops it at the first axis to time out.
+// HIGH pass trigger, let interrupts run for JOY_PASS_HIGH_START
+// counts' worth of video-beam time, then count to the end,
+// adding the classic samples the beam says came before
+// (gIigsJoyStart), so the value can differ from the classic
+// read's by a count. An axis that counts 0 timed out before
+// the count began: only a low pass can read it.
+// The last reading picks the first pass: a stick with both axes low or
+// centred costs one low pass, one past the low pass's reach a high pass
+// first, and then, for an axis that was already out when the high pass
+// began counting (a corner), a low pass that stops at that axis. Each
+// case costs about what the classic read does. An axis the passes cannot
+// pin down within JOY_PASS_ATTEMPTS (an interrupt ran long right where a
+// high pass had to start) keeps its last value.
+#define JOY_COUNT_RANGE 256u // an 8-bit paddle count
+#define JOY_PASS_COUNTS 144u // one pass's masked count: a multiple of 3, ~1.55 ms
+#define JOY_PASS_HIGH_START (JOY_COUNT_RANGE - JOY_PASS_COUNTS) // counts a high pass leaves unmasked
+#define JOY_PASS_ATTEMPTS 4u
+// iigsJoyPass flags and mode (joeyDraw.s JOY_PASS_*).
+#define JOY_PASS_NO_STICK 0x01u
+#define JOY_PASS_X_OPEN 0x02u
+#define JOY_PASS_Y_OPEN 0x04u
+#define JOY_PASS_FIRST 0x0100u
+#define JOY_AXIS_X 0u
+#define JOY_AXIS_Y 1u
+#define JOY_AXES 2u
+#define JOY_PADDLE_MAX 255u
+#define JOY_PADDLE_CENTER 128u
+#define JOY_RESOLVED 0x03u // the classic read's "both axes read"
+// The classic read's "no stick" result: resolved 0, both axes 128.
+#define JOY_RESULT_NO_STICK 0x00808000UL
+
+extern uint32_t iigsJoyPass(uint16_t limitAndMode, uint16_t highStart);
+extern uint16_t gIigsJoyStart;
+
+// The last tight reading, for the pass order and the hold.
+static uint8_t gJoyTightLast[JOY_AXES] = { JOY_PADDLE_CENTER, JOY_PADDLE_CENTER };
+
static void pollJoystick(void) {
uint32_t result;
uint8_t px;
@@ -310,7 +353,13 @@ static void pollJoystick(void) {
}
// Asm read at 1 MHz -- result returned packed in registers (A:X).
- result = iigsPollJoystickInner();
+ // While the interrupt-latency bound is tight, in passes that each
+ // mask interrupts for ~1.6 ms at most (pollJoystickTight).
+ if (gIigsIrqTight != 0u) {
+ result = pollJoystickTight();
+ } else {
+ result = iigsPollJoystickInner();
+ }
resolvedFlags = (uint8_t)(result & 0xFFu);
px = (uint8_t)((result >> 8) & 0xFFu);
py = (uint8_t)((result >> 16) & 0xFFu);
@@ -371,6 +420,84 @@ static void pollJoystick(void) {
}
+// The paddles as iigsPollJoystickInner reports them, read in passes that
+// never mask interrupts for more than one pass (see JOY_PASS_COUNTS).
+static uint32_t pollJoystickTight(void) {
+ uint32_t pass;
+ uint32_t result;
+ uint8_t *passBytes;
+ uint8_t *resultBytes;
+ uint8_t value[JOY_AXES];
+ bool known[JOY_AXES];
+ bool needHigh[JOY_AXES];
+ uint16_t flags;
+ uint16_t attempt;
+ uint16_t axis;
+ uint16_t count;
+ bool high;
+ bool first;
+ bool needed;
+
+ // Byte aliases, not shifts: a variable or 32-bit shift is a libcall here.
+ passBytes = (uint8_t *)&pass;
+ resultBytes = (uint8_t *)&result;
+ known[JOY_AXIS_X] = false;
+ known[JOY_AXIS_Y] = false;
+ needHigh[JOY_AXIS_X] = false;
+ needHigh[JOY_AXIS_Y] = false;
+ high = gJoyTightLast[JOY_AXIS_X] > JOY_PASS_COUNTS || gJoyTightLast[JOY_AXIS_Y] > JOY_PASS_COUNTS;
+ first = false;
+ for (attempt = 0u; attempt < JOY_PASS_ATTEMPTS; attempt++) {
+ pass = iigsJoyPass(first ? (JOY_PASS_COUNTS | JOY_PASS_FIRST) : JOY_PASS_COUNTS, high ? JOY_PASS_HIGH_START : 0u);
+ flags = (uint16_t)(passBytes[2] | ((uint16_t)passBytes[3] << 8));
+ if ((flags & JOY_PASS_NO_STICK) != 0u) {
+ return JOY_RESULT_NO_STICK;
+ }
+ for (axis = 0u; axis < JOY_AXES; axis++) {
+ if (known[axis]) {
+ continue;
+ }
+ count = passBytes[axis];
+ if (!high) {
+ if ((flags & (JOY_PASS_X_OPEN << axis)) == 0u) {
+ value[axis] = (uint8_t)count;
+ known[axis] = true;
+ } else if (!first) {
+ needHigh[axis] = true;
+ }
+ } else if (count != 0u) {
+ count = (uint16_t)(count + gIigsJoyStart);
+ if ((flags & (JOY_PASS_X_OPEN << axis)) != 0u || count > JOY_PADDLE_MAX) {
+ count = JOY_PADDLE_MAX;
+ }
+ value[axis] = (uint8_t)count;
+ known[axis] = true;
+ }
+ }
+ if (known[JOY_AXIS_X] && known[JOY_AXIS_Y]) {
+ break;
+ }
+ // Next: a high pass again only for an axis a low pass left timing
+ // (an interrupt ran long where the last high pass had to start). A
+ // single axis a high pass missed is the lower one, so the low pass
+ // that follows can stop at it.
+ needed = (!known[JOY_AXIS_X] && needHigh[JOY_AXIS_X]) || (!known[JOY_AXIS_Y] && needHigh[JOY_AXIS_Y]);
+ first = high && !needed && known[JOY_AXIS_X] != known[JOY_AXIS_Y];
+ high = needed;
+ }
+ for (axis = 0u; axis < JOY_AXES; axis++) {
+ if (known[axis]) {
+ gJoyTightLast[axis] = value[axis];
+ }
+ }
+ resultBytes[0] = JOY_RESOLVED;
+ resultBytes[1] = gJoyTightLast[JOY_AXIS_X];
+ resultBytes[2] = gJoyTightLast[JOY_AXIS_Y];
+ resultBytes[3] = 0u;
+ return result;
+}
+
+
// Drain one X+Y delta pair from the ADB mouse FIFO. $C027 bit 1 tells
// us which coordinate the next $C024 read will return; we honor that
// rather than assuming an order, so we stay in sync even if a stray
@@ -433,12 +560,6 @@ 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;
@@ -457,14 +578,6 @@ 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/joeyDraw.s b/src/iigs/joeyDraw.s
index 6774315..bb42758 100644
--- a/src/iigs/joeyDraw.s
+++ b/src/iigs/joeyDraw.s
@@ -175,6 +175,12 @@ clrExit:
; inhibited by jlpInit. ~1.5x faster.
;
; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI.
+; The hold is chunked like the present's: every gIigsClearChunkBlocks
+; blocks (hal.c; ~0.12 ms a block) the real stack and bank-0 writes come
+; back for a CLI/SEI pair, so a pending interrupt waits at most one chunk.
+; The loose bound (200) is the old single ~23 ms hold; the tight one
+; (12, ~1.4 ms) fits the Z8530's 3-byte receive FIFO at 9600 baud and
+; costs ~0.2 ms a clear.
; SAFE ONLY POST-jlpInit. Soft switches use LONG addressing to bank $E0
; ($E0C005), NOT `sta $C005`: under the GS/OS Loader DBR=$08, a DBR-rel
; `sta $C0xx` hits bank-$08 RAM and the switch silently never toggles, so
@@ -202,6 +208,7 @@ iigsSurfaceClearFastInner:
tcs ; SP = top of $01 stage region
lda sclrFillCap ; A = fillWord = the pushed value
ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes
+ ldy gIigsClearChunkBlocks ; blocks until the next interrupt window
clrFastBlock:
pha
pha
@@ -284,7 +291,34 @@ clrFastBlock:
pha
pha
dex
+ beq clrFastDone
+ dey
bne clrFastBlock
+; Interrupt window. AUXWRITE goes off BEFORE anything is stored: a
+; DBR-relative store to bank 0 would otherwise land in bank $01.
+ sep #0x20
+ .a8
+ sta 0xE0C004 ; AUXWRITE off
+ rep #0x20
+ .a16
+ tsc
+ sta sclrSlamSp ; where the slam resumes
+ lda sclrSavedSp
+ tcs ; the caller's stack for any interrupt
+ cli
+ nop ; a pending IRQ is taken here
+ sei
+ lda sclrSlamSp
+ tcs
+ sep #0x20
+ .a8
+ sta 0xE0C005 ; AUXWRITE on
+ rep #0x20
+ .a16
+ lda sclrFillCap
+ ldy gIigsClearChunkBlocks
+ brl clrFastBlock
+clrFastDone:
lda sclrSavedSp
tcs ; restore original SP
sep #0x20
@@ -314,6 +348,11 @@ sclrFillCap:
sclrSavedSp:
.zero 2
+ .section .bss.sclrSlamSp,"aw"
+ .globl sclrSlamSp
+sclrSlamSp:
+ .zero 2
+
; Flood-fill seed/match scratch (asm-only data; no C definition exists).
; iigsFloodWalkAndScansInner writes the seed match result and the left/
; right span extents here for the C wrapper to read back.
@@ -4661,6 +4700,13 @@ iigsBlitStageToShr:
; PLAN.md W2). The old rule both UNDER-promoted left-anchored
; mid-width bands and OVER-promoted right-anchored ones.
PEI_RUN_MIN = 8
+; Interrupt-latency bound: gIigsPresentChunkRows (hal.c) rows are consumed
+; per SEI chunk, ~0.42 ms a full-width row. Loose (40, ~17 ms) is free;
+; tight (4, ~1.8 ms) fits the Z8530 SCC's 3-byte receive FIFO at 9600 baud
+; (~3.1 ms; MEASURED zero loss under MAME, whose FIFO keeps only 2, where
+; 6 rows already lost bytes; 40 rows lost ~42% of a 9600 stream). MEASURED
+; tight cost: a full-screen present 83.1 -> 87.2 ms (each extra chunk
+; re-pays the run setup, ~90 us); clean rows are unaffected.
php
phb
@@ -4706,7 +4752,7 @@ PEI_RUN_MIN = 8
ldx #0 ; X = absolute row counter
; ---------------------------------------------------------------
-; Chunked-SEI scan. Contract UNCHANGED: sei per chunk, <= 40 rows
+; Chunked-SEI scan. Contract: sei per chunk, <= gIigsPresentChunkRows rows
; consumed per chunk, cli at chunk end so the latched VBL IRQ catches
; up (this is what keeps the GetTick bench clock honest). Runs are
; clipped at the chunk budget, so no SEI hold got longer -- every row
@@ -4805,7 +4851,7 @@ peiRowSkipGroup:
peiRowBounds:
cpx #200
bcs peiChunkDone
- cpy #40
+ cpy gIigsPresentChunkRows
bcc peiRowLoop
peiChunkDone:
; Per-chunk teardown is now JUST the CLI: runs restore SP and switches
@@ -4841,7 +4887,7 @@ peiRunGather:
iny
cpx #200
bcs peiRunGathered
- cpy #40
+ cpy gIigsPresentChunkRows
bcs peiRunGathered ; chunk budget clips the run
lda gStageMinWord,x
inc a
@@ -4875,7 +4921,7 @@ peiRunGathered: ; X/Y = scan resume values
txa
sec
sbc gPeiRunFirst
- sta gPeiRunLen ; rows in this run (1..40)
+ sta gPeiRunLen ; rows in this run (1..gIigsPresentChunkRows)
; STAGE B: computed entry. spOffset = 2*runMax + 1, parked
; at $E1:9DFE (the ONLY datum the switches-on loop loads);
; the burst enters the PEI table at word runMax, pushing
@@ -5048,7 +5094,7 @@ peiRunDone:
lda gPeiOrigSp
tcs ; caller stack back
; telemetry (W2 Stage 0): gPresentCopiedBytes +=
- ; runLen * bytesPerRow. Repeated add (runLen <= 40),
+ ; runLen * bytesPerRow. Repeated add (runLen <= gIigsPresentChunkRows),
; ~13 cyc per slammed row -- accepted for verifiability.
; DBR is the caller's globals bank (slam never touches
; DBR), so the absolute counter access is safe.
@@ -5112,7 +5158,7 @@ peiMvnGather:
iny
cpx #200
bcs peiMvnRunGo
- cpy #40
+ cpy gIigsPresentChunkRows
bcs peiMvnRunGo
lda gStageMinWord,x
cmp gPeiBandMin
@@ -5791,6 +5837,407 @@ gjNoJoy:
rtl ; uint32_t in A:X
+; ====================================================================
+; iigsJoyPass(uint16_t limitAndMode, uint16_t highStart)
+;
+; One pass of the tight-bound paddle read (input.c pollJoystickTight),
+; used while a client holds hal.c's tight interrupt-latency bound. The
+; classic read above masks interrupts for its whole count, ~2.8 ms at
+; full deflection, and the Z8530 SCC's receive FIFO lasts ~3.1 ms at
+; 9600 baud (~2.3 ms under MAME). A pass masks for at most its limit in
+; counts plus a beam stamp, and it counts with the classic dual/solo loop
+; on the classic 11-cycle beat, so a count means the same thing in both.
+;
+; highStart == 0 LOW pass: trigger and count at once. The trigger-
+; to-first-sample sequence matches the classic read's
+; cycle for cycle, so an axis that times out inside
+; the limit reads exactly what the classic read gives.
+; With JOY_PASS_FIRST it stops at the first axis to
+; time out, leaving the other OPEN.
+; highStart != 0 HIGH pass: trigger, let interrupts run for
+; highStart counts' worth of beam ticks, then count.
+; gIigsJoyStart says how many of the classic read's
+; samples came before the pass's first, rounded; an
+; axis that timed out before that sample counts 0, as
+; in the classic loop.
+;
+; Beam ticks: VERTCNT/HORIZCNT as line * 65 + position (0..64), i.e.
+; 1 MHz cycles on any IIgs (65 to a line), wrapped at gIigsBeamFrameTicks
+; (hal.c). A classic count is 11 of them, and so is one here.
+;
+; ABI: limitAndMode in A: the limit in counts in the low byte (a multiple
+; of 3, 3..255: the dual loop only tests for the wrap every third count),
+; JOY_PASS_FIRST in the high byte; highStart @4,s on JSL entry. Returns
+; uint32_t in A:X: A = px | (py << 8), the axes' counts (1-based like the
+; classic read's), X = JOY_PASS_* flags (input.c mirrors them).
+; Interrupts are re-enabled for the settle and the high pass's delay
+; whatever the caller had.
+; ====================================================================
+JOY_PASS_NO_STICK = 0x01 ; a paddle never timed out: no stick
+JOY_PASS_X_OPEN = 0x02 ; X still timing at the limit
+JOY_PASS_Y_OPEN = 0x04 ; Y still timing at the limit
+JOY_PASS_FIRST = 0x0100 ; low pass: stop at the first axis out
+JOY_TICKS_PER_COUNT = 11 ; the classic loop's 11 cycles at 1 MHz
+; Beam ticks a high pass's first sample comes after its count stamp,
+; measured the way the classic read counts, from cycle counts (a 2.8 MHz
+; cycle is 5/14 of a tick, an I/O access ~1.5): 52 from the count stamp
+; to the first sample (all at 1 MHz), less 18.8 from the trigger stamp
+; to the trigger and 15.1 from the trigger to the classic first sample.
+JOY_START_TICKS = 18
+; Settle tries, interrupts running: the previous pass can leave an axis
+; timing for up to ~1.3 ms, and a try is ~3.5 us at 2.8 MHz.
+JOY_SETTLE_TRIES = 0x0800
+
+; JOY_DUAL_LOOP: the classic read's dual counter, instruction for
+; instruction: both paddles sampled every ~11 cycles (10, 10, 13) while
+; both still time, to `toSolo` when one stops. Falls through when X wraps
+; to 0, i.e. at the pass's limit. In: M=8, X=8, DP = $C000, 1 MHz.
+.macro JOY_DUAL_LOOP toSolo
+1:
+ lda 0x64
+ and 0x65
+ bpl \toSolo
+ inx
+ lda 0x64
+ and 0x65
+ bpl \toSolo
+ inx
+ lda 0x64
+ and 0x65
+ bpl \toSolo
+ inx
+ bne 1b
+.endm
+
+.section .text.iigsJoyPass,"ax"
+.globl iigsJoyPass
+iigsJoyPass:
+ php
+ rep #0x30
+ .a16
+ .i16
+ phd
+ pha
+ and #JOY_PASS_FIRST
+ sta joyMode
+ pla
+ and #0x00FF
+ eor #0x00FF
+ inc a
+ sta joyX0 ; 256 - limit: X wraps to 0 at the limit
+ lda 7,s ; highStart (+3: php, phd)
+ sta joyDelay
+ asl a
+ sta joyDelayTicks
+ asl a
+ asl a ; * 8
+ clc
+ adc joyDelayTicks ; * 10
+ adc 7,s ; * 11: the delay in beam ticks
+ sta joyDelayTicks
+ stz joyFlags
+ stz joyPx
+ stz joyPy
+ pea 0xC000
+ pld ; DP = $C000: paddle, beam and speed I/O
+; Settle: both paddles timed out (a previous pass can leave one timing).
+ ldy #JOY_SETTLE_TRIES
+ sep #0x20
+ .a8
+jpSettle:
+ lda 0x64
+ ora 0x65
+ bpl jpSettled
+ dey
+ bne jpSettle
+ lda #JOY_PASS_NO_STICK
+ sta joyFlags
+ brl jpDone
+jpSettled:
+ sei
+ rep #0x20
+ .a16
+ lda joyDelay
+ beq jpLow
+ brl jpHigh
+jpLow:
+ lda joyMode
+ beq jpLowBoth
+ brl jpLowFirst
+; LOW pass: from the trigger on, the classic read's exact sequence.
+jpLowBoth:
+ sep #0x30
+ .a8
+ .i8
+ bit 0x70 ; PTRIG: start X,Y timers
+ lsr 0x36 ; force 1 MHz (C = CYAREG bit 0, kept for the rol)
+ ldx joyX0 ; 4 + 2 + 2 cycles: the classic ldx #1 / xba / xba
+ nop
+ nop
+jpDualXY0:
+ JOY_DUAL_LOOP jpToSolo
+ lda #JOY_PASS_X_OPEN | JOY_PASS_Y_OPEN
+ bra jpOpen
+jpSameXY:
+ dex
+ txy
+ bra jpGotXY
+jpSoloX:
+ bit 0x64
+ bmi jpSoloXOk
+ dey
+ bra jpGotXY
+jpSoloXOk:
+ inx
+ bne jpSoloX
+ dey ; Y timed out first: the classic finish's dey
+ lda #JOY_PASS_X_OPEN
+ bra jpOpen
+jpSoloY:
+ bit 0x65
+ bmi jpSoloYOk
+ dex
+ bra jpGotXY
+jpToSolo:
+ txy
+ bit 0x64
+ bmi jpSoloXOk
+ bit 0x65
+ bpl jpSameXY
+jpSoloYOk:
+ iny
+ bne jpSoloY
+ dex ; X timed out first: the classic finish's dex
+ lda #JOY_PASS_Y_OPEN
+jpOpen:
+ tsb joyFlags
+jpGotXY:
+ rol 0x36 ; restore CPU speed (C untouched by the loop)
+ txa
+ sec
+ sbc joyX0
+ inc a
+ sta joyPx ; count - X0 + 1: 1-based like the classic read
+ tya
+ sec
+ sbc joyX0
+ inc a
+ sta joyPy
+ bra jpDone
+
+; LOW pass that stops at the first axis to time out: the same trigger
+; sequence and dual counter, then the classic's own decisions about which
+; axis stopped (and its finishing dex/dey) without counting on.
+jpLowFirst:
+ sep #0x30
+ .a8
+ .i8
+ bit 0x70 ; PTRIG
+ lsr 0x36 ; force 1 MHz (C = CYAREG bit 0)
+ ldx joyX0
+ nop
+ nop
+ JOY_DUAL_LOOP jpFirstOut
+ lda #JOY_PASS_X_OPEN | JOY_PASS_Y_OPEN
+ bra jpOpen
+jpFirstOut:
+ txy
+ bit 0x64
+ bmi jpFirstY ; X still timing: Y stopped first
+ bit 0x65
+ bpl jpSameXY ; both at once
+ dex ; X stopped first
+ lda #JOY_PASS_Y_OPEN
+ bra jpOpen
+jpFirstY:
+ dey
+ lda #JOY_PASS_X_OPEN
+ bra jpOpen
+
+; HIGH pass: trigger, interrupts on for highStart counts of beam time, then count.
+jpHigh:
+ .a16
+ .i16
+ jsr joyStamp ; the trigger's stamp
+ sta joyStampTrig
+ sep #0x20
+ .a8
+ bit 0x70 ; PTRIG
+ rep #0x20
+ .a16
+ lda joyStampTrig
+ jsr joyLinear
+ sta joyLinTrig
+ cli
+jpDelay:
+ jsr joyStamp
+ jsr joyElapsed
+ cmp joyDelayTicks
+ bcc jpDelay
+ sei
+ sep #0x30
+ .a8
+ .i8
+ lsr 0x36 ; force 1 MHz (C = CYAREG bit 0)
+; The count's start stamp, taken at 1 MHz right before the loop: MAME
+; re-times a whole timeslice when the CPU clock changes, so a stamp taken
+; before the switch sits up to ~0.1 ms off the samples after it.
+ php ; keep C for the rol
+ jsr joyStamp
+ .a16
+ sta joyStampEnd
+ plp
+ .a8
+ .i8
+ ldx joyX0
+ brl jpDualXY0 ; an axis already timed out counts 0
+
+jpDone:
+ rep #0x30
+ .a16
+ .i16
+ pld
+ plp ; the caller's I, M=16, X=16
+ .a16
+ .i16
+; A high pass's start in classic counts, (span + JOY_START_TICKS) / 11
+; rounded, worked out with interrupts back on: 16 shift-subtract steps.
+ lda joyDelay
+ beq jpPack
+ lda joyStampEnd
+ jsr joyElapsed
+ clc
+ adc #JOY_START_TICKS + JOY_TICKS_PER_COUNT / 2
+ sta gIigsJoyStart ; dividend, becomes the quotient
+ lda #0 ; remainder
+ ldx #16
+jpDivide:
+ asl gIigsJoyStart
+ rol a
+ cmp #JOY_TICKS_PER_COUNT
+ bcc jpDivideNext
+ sbc #JOY_TICKS_PER_COUNT
+ inc gIigsJoyStart
+jpDivideNext:
+ dex
+ bne jpDivide
+jpPack:
+ lda joyPy
+ xba
+ ora joyPx
+ ldx joyFlags
+ rtl
+
+; joyStamp: the raw beam position, HORIZCNT:VERTCNT (VERTCNT in the low
+; byte), read twice so a VERTCNT step between the two bytes cannot tear
+; it. In: DP = $C000, any M. Out: A16. X, Y untouched.
+joyStamp:
+ rep #0x20
+ .a16
+ lda 0x2E ; VERTCNT, then HORIZCNT a cycle later
+ pha
+ lda 0x2E
+ sep #0x20
+ .a8
+ cmp 1,s ; VERTCNT the same both times?
+ rep #0x20
+ .a16
+ beq 1f
+ sta 1,s ; it stepped under the first read: take the second
+1:
+ pla
+ rts
+
+; joyLinear: a raw beam position as ticks, line * 65 + position, where
+; the line is VERTCNT * 2 + HORIZCNT bit 7 and the position is 0 for
+; HORIZCNT $00 and 1..64 for $40..$7F. In/out A16; X, Y untouched.
+joyLinear:
+ .a16
+ sta joyLinRaw
+ xba ; low byte = HORIZCNT
+ and #0x007F
+ bit #0x0040
+ beq 1f
+ and #0x003F
+ inc a
+1:
+ sta joyLinPos
+ lda joyLinRaw
+ and #0x00FF
+ asl a ; VERTCNT * 2
+ bit joyLinRaw ; N = HORIZCNT bit 7: the line's low bit
+ bpl 2f
+ inc a
+2:
+ sta joyLinRaw ; the line
+ asl a
+ asl a
+ asl a
+ asl a
+ asl a
+ asl a ; line * 64
+ clc
+ adc joyLinRaw ; line * 65
+ adc joyLinPos
+ rts
+
+; joyElapsed: ticks from joyLinTrig to a raw beam position, across a
+; frame wrap. In/out A16; X, Y untouched.
+joyElapsed:
+ .a16
+ jsr joyLinear
+ sec
+ sbc joyLinTrig
+ bcs 1f
+ clc
+ adc gIigsBeamFrameTicks ; the beam wrapped into the next frame
+1:
+ rts
+
+; iigsJoyPass scratch (asm-only data) and the high pass's start output,
+; read by input.c.
+ .section .bss.joyX0,"aw"
+joyX0:
+ .zero 2
+ .section .bss.joyDelay,"aw"
+joyDelay:
+ .zero 2
+ .section .bss.joyMode,"aw"
+joyMode:
+ .zero 2
+ .section .bss.joyDelayTicks,"aw"
+joyDelayTicks:
+ .zero 2
+ .section .bss.joyFlags,"aw"
+joyFlags:
+ .zero 2
+ .section .bss.joyPx,"aw"
+joyPx:
+ .zero 2
+ .section .bss.joyPy,"aw"
+joyPy:
+ .zero 2
+ .section .bss.joyStampTrig,"aw"
+joyStampTrig:
+ .zero 2
+ .section .bss.joyStampEnd,"aw"
+joyStampEnd:
+ .zero 2
+ .section .bss.joyLinTrig,"aw"
+joyLinTrig:
+ .zero 2
+ .section .bss.joyLinRaw,"aw"
+joyLinRaw:
+ .zero 2
+ .section .bss.joyLinPos,"aw"
+joyLinPos:
+ .zero 2
+ .section .bss.gIigsJoyStart,"aw"
+ .globl gIigsJoyStart
+gIigsJoyStart:
+ .zero 2
+
+
; ====================================================================
; iigsInputSnapshot(void)
;
@@ -6912,23 +7359,3 @@ wsDoubledByte:
.globl wsHasTrail
wsHasTrail:
.zero 2
-
-
-; ---------------------------------------------------------------------
-; iigsQuitGS -- issue GS/OS QuitGS ($2029) via the inline convention
-; and never return on success. Pure constants on purpose: the first
-; workaround built this stub in a C BSS array and hit the cross-bank
-; DBR trap (byte stores went DB-relative to bank $08 while the jsl
-; resolved to the array's true bank $10 -- executed zeros, BRK). The
-; parameter block lives at the FIXED scratch address $E1:9DCC (inside
-; the $E1:9DC8-$9DFD survives-present window; present parks $9DFE) and
-; the C caller writes its pCount=0 word before calling, so this
-; routine contains no relocations at all. If QuitGS errors, we rtl
-; and the caller falls back to the (broken) runtime exit path.
- .section .text.iigsQuitGS,"ax"
- .globl iigsQuitGS
-iigsQuitGS:
- jsl 0xE100A8
- .word 0x2029
- .long 0x00E19DCC
- rtl
diff --git a/src/iigs/serial.c b/src/iigs/serial.c
index 09ab3e6..fe88834 100644
--- a/src/iigs/serial.c
+++ b/src/iigs/serial.c
@@ -1,4 +1,4 @@
-// Apple IIgs serial HAL -- direct, polled register access.
+// Apple IIgs serial HAL -- direct register access, interrupt-driven receive.
//
// Three endpoints behind one API, selected at open:
// JL_SERIAL_MODEM -- modem port = Zilog 8530 SCC channel B
@@ -14,22 +14,46 @@
// 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
-// latency, and it does not fight the GS/OS heartbeat (JoeyLib's stated policy
-// is to avoid ISRs that do). RX is drained on jlSerialPoll() into a bank-0
-// ring; TX is a bounded poll on the transmit-empty status bit. The app owns
-// the port exclusively between jlSerialOpen and jlSerialClose.
+// input.c / the video + DOC pokes -- no GS/OS driver and no ROM serial
+// firmware. RECEIVE IS INTERRUPT-DRIVEN (serialIrq.s): the open port's
+// receive interrupt reaches a handler through the ROM interrupt manager --
+// the SCC through the Misc Tool Set's serial vector, a Super Serial Card
+// through its "other interrupt" vector -- and the handler moves every byte
+// into the ring the moment it lands. Nothing outside this add-on has to pump
+// the port: the Z8530 holds 3 received bytes (~3 ms at 9600 baud) and the 6551
+// one (~1 ms), while a full-screen present takes ~74 ms. TX is a bounded poll
+// on the transmit-empty status bit. The app owns the port exclusively between
+// jlSerialOpen and jlSerialClose, and MUST close it before it exits: until
+// then a system interrupt vector points into the program.
+//
+// The one thing an interrupt cannot beat is code that masks interrupts, and a
+// byte that arrives then waits for the CLI. While a port is open this add-on
+// holds the HAL's tight interrupt-latency bound (iigsIrqLatencyTighten), which
+// cuts the present's chunked SEI (see iigsBlitStageToShr), the stage fast
+// clear, the joystick read and the NinjaTrackerPlus effect calls into holds
+// of at most ~1.7 ms. GS/OS disk I/O and jlAudioPlayMod's instrument upload
+// are not JoeyLib's to cut.
//
// SCC access uses the register-pointer protocol: to reach WRn/RRn, first write
// n to the channel's command register, then write (WRn) or read (RRn) it; RR0
-// (status) and WR0 need no pointer step. The two writes must stay adjacent
-// (the pointer is not re-entrant), which they do here -- no serial ISR runs.
+// (status) and WR0 need no pointer step. The pointer is ONE register for the
+// whole chip, and the ROM's interrupt entry moves it on every interrupt (it
+// reads RR3 to find AppleTalk's and the serial vector's work), so a pointer
+// write and its access must never be split by an interrupt: sccWrite masks
+// them, and the handlers run masked anyway.
+//
+// A Super Serial Card only raises its interrupt with its SW2-6 switch ON (MAME
+// models the switch, default off, like the card). With it off, nothing reaches
+// the handler and the port still works, drained by jlSerialPoll and
+// jlSerialWrite -- but loses whatever arrives while the program is elsewhere.
//
// CAVEAT: the SSC 6551 window offset (+8 within the slot's 16-byte I/O space,
// the classic Apple Super Serial Card decode) and the slot's Control-Panel
// "Your Card" setting should be confirmed against the target hardware/emulator
// before relying on the SSC path.
+#include
+
#include "joey/serial.h"
#include "port.h"
@@ -38,8 +62,7 @@
#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)
+#define SCC_DATA_OFFSET 2 // a channel's data register sits 2 above its command register
// 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
@@ -48,130 +71,170 @@
#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
-#define SCC_RR0_TX_EMPTY 0x04
+// The rest of WR9, written along with the reset command: master interrupt
+// enable + no vector. WR9 is chip-wide and write-only, so this rewrites the
+// interrupt-control bits for BOTH channels -- with the value every IIgs driver
+// of the chip uses: the ROM serial firmware's EnableInt writes $0A, AppleTalk
+// resets its channel with $8A (Bank FF serialdrvr / Bank FD lap_includes). So
+// AppleTalk's interrupts on the other channel stay on, and ours can happen.
+#define SCC_WR9_MIE_NV 0x0Au
-// RR1 bit 5: RX overrun - the 3-byte hardware FIFO was full when another byte finished, and that byte
-// is GONE. Latched until an Error Reset (WR0 command 0x30), so it survives to the next pump pass.
-#define SCC_RR1_RX_OVRN 0x20
-#define SCC_WR0_ERR_RESET 0x30
+// WR1 receive-interrupt mode: interrupt on every character (and on a special
+// receive condition). 0 = this channel never interrupts.
+#define SCC_WR1_RX_INT_ALL 0x10u
+#define SCC_WR1_NO_INTS 0x00u
-// 6551 status bits.
-#define ACIA_ST_RX_FULL 0x08
-#define ACIA_ST_TX_EMPTY 0x10
-#define ACIA_ST_RX_OVRN 0x04
+// RR3 interrupt-pending bits (ext/status, Tx, Rx) of each channel. The ROM's
+// interrupt entry only calls the serial vector for bits that are also in
+// SERFLAG, and dismisses any others untouched -- so a channel's bits must be
+// there before it may interrupt, or its level-triggered IRQ would re-enter the
+// ROM forever. The ROM firmware's EnableInt claims all three the same way
+// (MAME also reports a special receive condition on the ext/status bit).
+#define SCC_RR3_CHAN_A 0x38u
+#define SCC_RR3_CHAN_B 0x07u
+
+// RR0 transmit buffer empty.
+#define SCC_RR0_TX_EMPTY 0x04u
+
+// WR5 control-line and transmitter bits (the Tx bits-per-char field, bits 6-5, comes from the config).
+#define SCC_WR5_DTR 0x80u
+#define SCC_WR5_TX_EN 0x08u
+#define SCC_WR5_RTS 0x02u
// SCC baud time constant: BRG output = PCLK(3.6864MHz) / (2*(TC+2)) with the
// /16 clock mode, so baud = 115200 / (TC+2) -> TC = 115200/baud - 2.
#define SCC_TC_NUM 115200L
-// SSC 6551 lives at the high half of slot s's I/O window: $C080 + s*16 + 8.
-#define SSC_BASE(_slot) ((volatile uint8_t *)(0x00C088L + (long)(_slot) * 16L))
+// ----- 6551 (Super Serial Card) -----
-// Bank-0 RX ring (power of two).
+// SSC 6551 lives at the high half of slot s's I/O window: $C080 + s*16 + 8
+// (data +0, status +1, command +2, control +3).
+#define SSC_BASE(_slot) ((volatile uint8_t *)(0x00C088L + (long)(_slot) * 16L))
+#define ACIA_STATUS 1
+#define ACIA_COMMAND 2
+#define ACIA_CONTROL 3
+
+// Command register: bit 0 DTR -- which on this chip also enables the receiver and its interrupt, so
+// dropping DTR stops the card receiving -- bit 1 receiver
+// interrupt DISABLE, bits 3-2 transmitter control (%10 = RTS asserted, transmit
+// interrupt off; %00 = RTS dropped and transmitter off), bits 7-5 parity.
+#define ACIA_CMD_DTR 0x01u
+#define ACIA_CMD_RX_IRQ_OFF 0x02u
+#define ACIA_CMD_TX_MASK 0x0Cu
+#define ACIA_CMD_RTS_ON 0x08u
+#define ACIA_CMD_PARITY_ODD 0x20u
+#define ACIA_CMD_PARITY_EVEN 0x60u
+
+#define ACIA_ST_TX_EMPTY 0x10u
+
+// ----- Interrupt plumbing -----
+
+// Misc Tool Set vector reference numbers (GetVector/SetVector): the ROM
+// interrupt manager's hooks for SCC interrupts AppleTalk refused ($E1/0024)
+// and for interrupts no built-in source claimed, i.e. slot cards ($E1/005C).
+#define IIGS_VECTOR_SERIAL 0x0009u
+#define IIGS_VECTOR_OTHER 0x0017u
+
+// SERFLAG: the RR3 bits the ROM's interrupt entry hands to the serial vector
+// (Bank FF monitor_bram.intr071, INTHNDLR: `AND SERFLAG / JSL IRQ_SERIAL`).
+#define IIGS_SERFLAG ((volatile uint8_t *)0xE10104L)
+
+// RX ring (power of two).
#define RX_RING_SIZE 512u
#define RX_RING_MASK (RX_RING_SIZE - 1u)
+// What is open, for the handlers. serialIrq.s spells the same values as
+// SER_KIND_SCC / SER_KIND_ACIA.
+typedef enum {
+ IIGS_SER_CLOSED = 0,
+ IIGS_SER_SCC = 1,
+ IIGS_SER_ACIA = 2
+} IigsSerKindE;
-// ----- Module state -----
-static uint8_t gRxRing[RX_RING_SIZE];
+// ----- State shared with serialIrq.s (read there with long addressing) -----
-// Flow counters for the RetroNet RXSTAT diagnostic (read via extern): bytes moved SCC->ring and
+// The receive ring. One producer -- the interrupt handler, or the same drain
+// run by the mainline with interrupts masked -- stores the byte and then
+// publishes gIigsRxTail with a single 16-bit store; the mainline consumer only
+// ever moves gIigsRxHead.
+uint8_t gIigsRxRing[RX_RING_SIZE];
+const uint16_t gIigsRxRingMask = RX_RING_MASK;
+volatile uint16_t gIigsRxHead = 0u;
+volatile uint16_t gIigsRxTail = 0u;
+
+uint16_t gIigsSerKind = IIGS_SER_CLOSED; // IigsSerKindE of the open port
+uint16_t gIigsSerPort = 0u; // SCC command register / 6551 data register (I/O address)
+uint8_t gIigsSerIpMask = 0u; // the open SCC channel's RR3 bits; 0 for the 6551
+uint32_t gIigsSerChain = 0ul; // the vector's previous owner, minus one (the handlers RTL to it)
+
+// Flow counters for the RetroNet RXSTAT diagnostic (read via extern): bytes moved chip->ring and
// ring->caller since open. Cheap enough to keep unconditionally.
uint16_t gIigsRxPumped = 0u;
uint16_t gIigsRxRead = 0u;
// Receive-loss counters, same read-via-extern contract. Overruns = loss EVENTS the hardware latched
-// (SCC RR1 overrun / 6551 status bit 2) - one latch observation may stand for SEVERAL destroyed bytes,
-// so this is a loss indicator, not a byte count. Before it existed a pump-discipline gap was invisible
-// except as a frame CRC drop and a retransmit stall. Dropped = bytes the SOFT ring refused because it
-// was full (read from the chip, then discarded); saturating, like the overrun count.
+// (SCC RR1 overrun, checked after every byte / 6551 status bit 2) - one latch observation may stand for
+// SEVERAL destroyed bytes, so this is a loss indicator, not a byte count; MAME's z80scc never surfaces
+// the SCC flag, so there it stays 0. Dropped = bytes the SOFT ring refused because it was full (read
+// from the chip, then discarded). Both saturate.
uint16_t gIigsRxOverruns = 0u;
uint16_t gIigsRxDropped = 0u;
-static uint16_t gRxHead;
-static uint16_t gRxTail;
-static bool gOpen;
-static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA
-static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg
-static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so jlpSerialSetRts
+
+
+// ----- Module state -----
+
+static volatile uint8_t *gReg; // SCC command register / 6551 command register
+static volatile uint8_t *gData; // data register (both)
+static uint8_t gTxReadyBit; // transmit-empty bit of the status iigsSerialService returns
+static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so serialSetLine
// can flip ONE bit without guessing the rest back: WR5 is
// write-only on this chip, and rebuilding it from scratch would
// silently drop the data-bit and Tx-enable settings
-static volatile uint8_t *gData; // data register (both)
+static uint8_t gAciaCmd; // the 6551's command register, same reason
+static uint16_t gVector; // the Misc Tool Set vector the handler sits on
+static uint32_t gVectorOld; // ...and what it held before, restored at close
+static uint8_t gSerflagOld; // our channel's SERFLAG bits before open
// ----- Prototypes -----
-extern uint8_t jlpIoRead8(const volatile uint8_t *p);
-static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg);
-static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg);
-static bool serialTxReady(void);
-static uint8_t sccRead(volatile uint8_t *ctrl, uint8_t reg);
-static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val);
-
-
-// ----- Guaranteed 8-bit I/O read -----
-//
-// llvm816 widens a volatile uint8_t LOAD through a variable long pointer to a 16-bit `lda [dp],y`
-// (m=0), so every such register read also performs a phantom bus read of address+1 - and the SCC's
-// four registers are adjacent, so a data read on one channel phantom-pops the OTHER channel's data
-// FIFO ($C03A reads $C03B: channel A, the AppleTalk port). Constant-address loads are sep-wrapped
-// correctly; only the pointer form is affected, and STORES are always sep-wrapped. Filed in
-// llvm816/LLVM816-ASKS.md; until the toolchain honours volatile width, every serial register READ
-// goes through this hand-asm helper - a global function so no call site can be inlined back into
-// the widened form. ABI from a compiled probe: pointer lo16 in A, bank in X; return in A (high
-// byte cleared), X zeroed; $e0-$e3 are call-clobbered imaginary-register scratch, D points at the
-// imaginary page on entry (the same assumption every compiled function makes).
-__asm__(
- ".section .text.jlpIoRead8,\"ax\",@progbits\n"
- ".globl jlpIoRead8\n"
- "jlpIoRead8:\n"
- "\trep #$30\n" // known width state: 16-bit A and index
- "\tsta $e0\n" // pointer low 16 -> $e0-$e1
- "\ttxa\n"
- "\tsta $e2\n" // bank -> $e2 ($e3 catches the junk high byte - scratch)
- "\tldy #0\n"
- "\tsep #$20\n" // 8-bit accumulator: the actual fix
- "\tlda [$e0],y\n" // ONE byte on the bus - no phantom companion read
- "\trep #$30\n"
- "\tand #$ff\n" // the hidden B accumulator held junk; clear it for the return
- "\tldx #0\n"
- "\trtl\n"
-);
+extern uint16_t iigsIrqOff(void);
+extern void iigsIrqRestore(uint16_t p);
+extern uint32_t iigsSerialIsrAddress(uint16_t kind);
+extern uint16_t iigsSerialService(void);
+static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val);
+static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg);
+static void serialInitScc(volatile uint8_t *ctrl, uint8_t reset, const jlSerialConfigT *cfg);
+static void serialIrqInstall(uint16_t kind, volatile uint8_t *port, uint8_t rr3Bits, uint16_t vector);
+static void serialIrqRemove(void);
+static void serialOpenScc(volatile uint8_t *ctrl, uint8_t reset, uint8_t rr3Bits, const jlSerialConfigT *cfg);
+static void serialSetLine(uint8_t sccBit, uint8_t aciaMask, uint8_t aciaOn, bool on);
// ----- SCC helpers -----
-// Register-pointer protocol: point at WRn (if not WR0), then write the value.
+// Register-pointer protocol: point at WRn (if not WR0), then write the value -- with interrupts
+// masked, because the ROM's interrupt entry moves the chip-wide pointer (see the file header).
static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val) {
+ uint16_t p;
+
+ p = iigsIrqOff();
if (reg != 0u) {
*ctrl = reg;
}
*ctrl = val;
+ iigsIrqRestore(p);
}
-// Point at RRn, then read it. Same adjacency rule as sccWrite: nothing may touch the SCC between the
-// pointer write and the read (safe here - WR1=0 means this channel never interrupts, and the HAL is
-// single-threaded polled).
-static uint8_t sccRead(volatile uint8_t *ctrl, uint8_t reg) {
- if (reg != 0u) {
- *ctrl = reg;
- }
- return jlpIoRead8(ctrl);
-}
-
-
-static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg) {
+// Program a channel and start it, with its interrupts still off.
+static void serialInitScc(volatile uint8_t *ctrl, uint8_t reset, const jlSerialConfigT *cfg) {
uint8_t wr4;
uint8_t wr3;
uint8_t wr5;
int32_t tc;
- (void)data;
-
// WR4: /16 clock, stop bits, parity.
wr4 = 0x40u; // /16 clock rate
wr4 |= (cfg->stopBits == 2u) ? 0x0Cu : 0x04u; // 2 or 1 stop bit
@@ -203,36 +266,62 @@ static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_
// 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
+ // The same write sets the chip-wide interrupt control (SCC_WR9_MIE_NV); a channel reset leaves
+ // this channel's WR1 interrupt enables clear, and it stays quiet until serialOpenScc has the
+ // handler in place.
+ sccWrite(ctrl, 9u, (uint8_t)(reset | SCC_WR9_MIE_NV));
+ sccWrite(ctrl, 1u, SCC_WR1_NO_INTS); // 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
+ sccWrite(ctrl, 5u, (uint8_t)(wr5 | SCC_WR5_DTR | SCC_WR5_RTS)); // Tx bits + DTR + RTS, not yet enabled
sccWrite(ctrl, 11u, 0x50u); // Rx & Tx clock = baud rate generator
sccWrite(ctrl, 12u, (uint8_t)(tc & 0xFFu)); // time constant low
sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK
sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable
- gWr5 = (uint8_t)(wr5 | 0x8Au);
+ gWr5 = (uint8_t)(wr5 | SCC_WR5_DTR | SCC_WR5_TX_EN | SCC_WR5_RTS);
sccWrite(ctrl, 5u, gWr5); // Tx enable + DTR + RTS
}
+static void serialOpenScc(volatile uint8_t *ctrl, uint8_t reset, uint8_t rr3Bits, const jlSerialConfigT *cfg) {
+ serialInitScc(ctrl, reset, cfg);
+ gReg = ctrl;
+ gData = ctrl + SCC_DATA_OFFSET;
+ gTxReadyBit = SCC_RR0_TX_EMPTY;
+ serialIrqInstall(IIGS_SER_SCC, ctrl, rr3Bits, IIGS_VECTOR_SERIAL);
+ sccWrite(ctrl, 1u, SCC_WR1_RX_INT_ALL); // handler in: let the channel interrupt
+}
+
+
+// Raise or drop one control line of the open port, changing nothing else. SCC: `sccBit` of WR5,
+// applied to the retained copy (WR5 is write-only, and rebuilding it would silently drop the data-bit
+// and Tx-enable settings) and written with interrupts masked by sccWrite. 6551: the command
+// register's `aciaMask` field becomes `aciaOn` when on and 0 when off; parity and the receiver
+// interrupt setting ride along unchanged.
+static void serialSetLine(uint8_t sccBit, uint8_t aciaMask, uint8_t aciaOn, bool on) {
+ if (gIigsSerKind == IIGS_SER_SCC) {
+ gWr5 = on ? (uint8_t)(gWr5 | sccBit) : (uint8_t)(gWr5 & (uint8_t)~sccBit);
+ sccWrite(gReg, 5u, gWr5);
+ return;
+ }
+ gAciaCmd = (uint8_t)(gAciaCmd & (uint8_t)~aciaMask);
+ if (on) {
+ gAciaCmd = (uint8_t)(gAciaCmd | aciaOn);
+ }
+ *gReg = gAciaCmd;
+}
+
+
// ----- 6551 (Super Serial Card) helper -----
+// Reset and frame the ACIA and work out its command byte; the command register (which starts the
+// receiver and its interrupt) is written by jlpSerialOpen once the handler is in place.
static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) {
uint8_t control;
- uint8_t command;
uint8_t baudCode;
// 6551 internal baud rate codes (bits 3-0 of the control register).
@@ -262,227 +351,172 @@ static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) {
control |= 0x80u;
}
- // Command: DTR on (bit0) | rx IRQ disabled (bit1, polled) | RTS low, TX IRQ
- // off (bits 3-2 = 10) | parity.
- command = 0x0Bu;
+ // Command: DTR on, receiver interrupt ENABLED, RTS asserted with the transmit interrupt off, parity.
+ gAciaCmd = (uint8_t)(ACIA_CMD_DTR | ACIA_CMD_RTS_ON);
if (cfg->parity == JL_SERIAL_PARITY_ODD) {
- command |= 0x20u; // parity enable, odd
+ gAciaCmd |= ACIA_CMD_PARITY_ODD;
} else if (cfg->parity == JL_SERIAL_PARITY_EVEN) {
- command |= 0x60u; // parity enable, even
+ gAciaCmd |= ACIA_CMD_PARITY_EVEN;
}
- base[1] = 0x00u; // write to status register = software reset
- base[3] = control; // control register
- base[2] = command; // command register
+ base[ACIA_STATUS] = 0x00u; // write to status register = programmed reset (DTR off: no interrupts)
+ base[ACIA_CONTROL] = control;
}
-// ----- Status polling -----
+// ----- Interrupt install / remove -----
-static bool serialTxReady(void) {
- if (gIsScc) {
- return (jlpIoRead8(gStatus) & SCC_RR0_TX_EMPTY) != 0u;
- }
- return (jlpIoRead8(gStatus) & ACIA_ST_TX_EMPTY) != 0u;
+// Point `vector` at the handler for `kind` and publish the port to it. For the SCC, rr3Bits go into
+// SERFLAG so the ROM routes the channel's interrupts to the vector at all. Masked throughout: the
+// vector, SERFLAG and the handler's view of the port change together.
+static void serialIrqInstall(uint16_t kind, volatile uint8_t *port, uint8_t rr3Bits, uint16_t vector) {
+ uint16_t p;
+
+ p = iigsIrqOff();
+ gVector = vector;
+ gVectorOld = (uint32_t)GetVector(vector);
+ gIigsSerChain = gVectorOld - 1ul;
+ gIigsSerPort = (uint16_t)(uint32_t)port;
+ gIigsSerIpMask = rr3Bits;
+ gSerflagOld = (uint8_t)(*IIGS_SERFLAG & rr3Bits);
+ *IIGS_SERFLAG = (uint8_t)(*IIGS_SERFLAG | rr3Bits);
+ gIigsSerKind = kind;
+ SetVector(vector, (void *)iigsSerialIsrAddress(kind));
+ iigsIrqRestore(p);
+ iigsIrqLatencyTighten();
+}
+
+
+// Undo serialIrqInstall. The caller has already stopped the chip interrupting and retired any
+// request it raised before that.
+static void serialIrqRemove(void) {
+ uint16_t p;
+
+ p = iigsIrqOff();
+ gIigsSerKind = IIGS_SER_CLOSED;
+ *IIGS_SERFLAG = (uint8_t)((*IIGS_SERFLAG & (uint8_t)~gIigsSerIpMask) | gSerflagOld);
+ gIigsSerIpMask = 0u;
+ SetVector(gVector, (void *)gVectorOld);
+ iigsIrqRestore(p);
+ iigsIrqLatencyRelax();
}
// ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) {
- return (uint16_t)((gRxTail - gRxHead) & RX_RING_MASK);
+ return (uint16_t)((gIigsRxTail - gIigsRxHead) & RX_RING_MASK);
}
+// The core calls this whether or not a port is open.
void jlpSerialClose(void) {
- gOpen = false;
- // Leave the chip configured; a polled port needs no teardown, and the app
- // owns it until re-opened.
+ uint16_t p;
+
+ if (gIigsSerKind == IIGS_SER_CLOSED) {
+ return;
+ }
+ p = iigsIrqOff();
+ if (gIigsSerKind == IIGS_SER_SCC) {
+ sccWrite(gReg, 1u, SCC_WR1_NO_INTS);
+ } else {
+ gAciaCmd = (uint8_t)(gAciaCmd | ACIA_CMD_RX_IRQ_OFF);
+ *gReg = gAciaCmd;
+ }
+ // A request raised before the line went quiet is retired by taking its byte; left pending, a
+ // level-triggered IRQ whose bits are no longer claimed would re-enter the ROM forever.
+ (void)iigsSerialService();
+ serialIrqRemove();
+ iigsIrqRestore(p);
+ // Otherwise the chip stays configured (the transmitter keeps running) until the next open.
}
void jlpSerialFlush(void) {
- gRxHead = gRxTail;
+ gIigsRxHead = gIigsRxTail;
}
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
- if (gOpen) {
- return false;
- }
- gRxHead = 0u;
- gRxTail = 0u;
+ volatile uint8_t *base;
+
+ gIigsRxHead = 0u;
+ gIigsRxTail = 0u;
switch (device) {
case JL_SERIAL_PRINTER:
- gIsScc = true;
- gStatus = SCC_A_CTRL;
- gData = SCC_A_DATA;
- serialInitScc(SCC_A_CTRL, SCC_A_DATA, SCC_WR9_RESET_A, config);
+ serialOpenScc(SCC_A_CTRL, SCC_WR9_RESET_A, SCC_RR3_CHAN_A, config);
break;
- case JL_SERIAL_SLOT: {
- volatile uint8_t *base;
-
+ case JL_SERIAL_SLOT:
if (config->unit < 1u || config->unit > 7u) {
return false;
}
- base = SSC_BASE(config->unit);
- gIsScc = false;
- gStatus = &base[1];
- gData = &base[0];
+ base = SSC_BASE(config->unit);
+ gReg = &base[ACIA_COMMAND];
+ gData = base;
+ gTxReadyBit = ACIA_ST_TX_EMPTY;
serialInitAcia(base, config);
+ serialIrqInstall(IIGS_SER_ACIA, base, 0u, IIGS_VECTOR_OTHER);
+ *gReg = gAciaCmd; // handler in: start the receiver and its interrupt
break;
- }
case JL_SERIAL_MODEM:
case JL_SERIAL_DEFAULT:
default:
- gIsScc = true;
- gStatus = SCC_B_CTRL;
- gData = SCC_B_DATA;
- serialInitScc(SCC_B_CTRL, SCC_B_DATA, SCC_WR9_RESET_B, config);
+ serialOpenScc(SCC_B_CTRL, SCC_WR9_RESET_B, SCC_RR3_CHAN_B, config);
break;
}
-
- gOpen = true;
return true;
}
-// 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();
-}
-
-
-// RTS on demand (jlSerialSetRts). SCC: WR5 bit 1, applied to the retained copy so the Tx enable, DTR and
-// data-bit fields survive. 6551: the command register's bits 3:2 - %10 asserts RTS with the receiver
-// interrupt disabled (this HAL polls), %00 deasserts it AND gates the transmitter off, which is why the
-// deassert is only ever a pause.
-void jlpSerialSetRts(bool on) {
- if (!gOpen) {
- return;
- }
- if (gIsScc) {
- gWr5 = on ? (uint8_t)(gWr5 | 0x02u) : (uint8_t)(gWr5 & (uint8_t)~0x02u);
- sccWrite(gStatus, 5u, gWr5);
- return;
- }
- gStatus[1] = on ? 0x09u : 0x01u; // 6551 command: DTR on, RTS asserted / deasserted
-}
-
-
+// The interrupt keeps the ring filled; this only catches what it could not -- a Super Serial Card
+// whose interrupt switch is off.
void jlpSerialPoll(void) {
- uint16_t tail;
- uint16_t guard;
-
- if (!gOpen) {
- return;
- }
- // Drain the hardware into the ring under a bound (like input.c's FIFO
- // drain). Local read-modify-write of the tail index dodges the `inc abs`
- // DBR trap that file-scope `++` lowers to on the 65816.
- tail = gRxTail;
- guard = RX_RING_SIZE;
- // Counter updates below use the guarded local read-modify-write from input.c's joystick counter -
- // NOT because of the spelling (clang canonicalizes `x++` and `x = x + 1` identically) but because
- // the saturation compare keeps the loaded value LIVE, which is what stops the backend fusing the
- // update into a DBR-relative `inc abs`. Saturating is also the right behaviour for a loss counter.
- while (guard != 0u) {
- uint16_t next;
- uint8_t st;
- uint8_t b;
-
- st = jlpIoRead8(gStatus); // RR0 (SCC) or 6551 status
- if (gIsScc) {
- if ((st & SCC_RR0_RX_AVAIL) == 0u) {
- break;
- }
- } else {
- // The 6551 latches overrun ONLY while RDRF is still set, and the DATA read below retires
- // both bits together - so the overrun must be sampled HERE, from the same status read that
- // gates the byte. (A post-loop check can never see it: the drain already cleared it.)
- if ((st & ACIA_ST_RX_OVRN) != 0u) {
- uint16_t ovr = gIigsRxOverruns;
-
- if (ovr < 0xFFFFu) {
- gIigsRxOverruns = (uint16_t)(ovr + 1u);
- }
- }
- if ((st & ACIA_ST_RX_FULL) == 0u) {
- break;
- }
- }
- next = (uint16_t)((tail + 1u) & RX_RING_MASK);
- b = jlpIoRead8(gData);
- if (next != gRxHead) {
- gRxRing[tail] = b;
- tail = next;
- gIigsRxPumped++;
- } else {
- uint16_t drop = gIigsRxDropped;
-
- if (drop < 0xFFFFu) {
- gIigsRxDropped = (uint16_t)(drop + 1u);
- }
- }
- guard--;
- }
- gRxTail = tail;
- // Harvest the SCC's own loss record: real silicon latches RX overrun in RR1 until an Error Reset,
- // so one check per pump names a deaf-window loss even though the pump never saw the byte.
- //
- // DEFAULT OFF - REAL HARDWARE ONLY, on two measured grounds: (1) under MAME the counter is INERT
- // (its z80scc never surfaces the flag into RR1), so enabling it under emulation buys nothing; and
- // (2) with it enabled the MAME 0.264 session goes RX-deaf after HELLO - 3 of 4 runs, INCLUDING one
- // through jlpIoRead8's true 8-bit reads, so the phantom companion read is NOT the mechanism (one
- // green run said otherwise and the next red run retracted it - the two-greens-prove-nothing rule).
- // Whatever the per-pass WR0-pointer/RR1 dance disturbs in MAME's SCC remains unpinned; it is not
- // worth more rig time for a diagnostic that cannot fire there. JL_IIGS_RR1_HARVEST enables it
- // for silicon validation.
-#ifdef JL_IIGS_RR1_HARVEST
- if (gIsScc) {
- if ((sccRead(gStatus, 1u) & SCC_RR1_RX_OVRN) != 0u) {
- uint16_t ovr = gIigsRxOverruns;
-
- if (ovr < 0xFFFFu) {
- gIigsRxOverruns = (uint16_t)(ovr + 1u);
- }
- sccWrite(gStatus, 0u, SCC_WR0_ERR_RESET);
- }
- }
-#endif
+ (void)iigsSerialService();
}
uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
uint16_t n;
uint16_t head;
+ uint16_t tail;
- head = gRxHead;
- for (n = 0u; n < max && head != gRxTail; n++) {
- buf[n] = gRxRing[head];
+ head = gIigsRxHead;
+ tail = gIigsRxTail; // one snapshot: the handler may append while we copy
+ for (n = 0u; n < max && head != tail; n++) {
+ buf[n] = gIigsRxRing[head];
head = (uint16_t)((head + 1u) & RX_RING_MASK);
}
- gRxHead = head;
+ gIigsRxHead = head;
gIigsRxRead = (uint16_t)(gIigsRxRead + n);
return n;
}
+// DTR on demand (jlSerialSetDtr, how a program hangs up). SCC: WR5 bit 7. 6551: command bit 0, which
+// is also the receiver's enable -- with DTR dropped the card neither receives nor interrupts until DTR
+// is raised again. Close keeps whatever this left, so a dropped DTR stays dropped.
+void jlpSerialSetDtr(bool on) {
+ serialSetLine(SCC_WR5_DTR, ACIA_CMD_DTR, ACIA_CMD_DTR, on);
+}
+
+
+// RTS on demand (jlSerialSetRts). SCC: WR5 bit 1. 6551: the command register's bits 3:2 - %10 asserts
+// RTS, %00 deasserts it AND gates the transmitter off, which is why the deassert is only ever a pause.
+void jlpSerialSetRts(bool on) {
+ serialSetLine(SCC_WR5_RTS, ACIA_CMD_TX_MASK, ACIA_CMD_RTS_ON, on);
+}
+
+
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
for (n = 0u; n < len; n++) {
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();
+ // The status read goes through iigsSerialService, which also drains the receiver: a 6551
+ // status read clears its interrupt request, so whoever reads it must take the byte.
+ while ((iigsSerialService() & gTxReadyBit) == 0u) {
guard++;
if (guard == 0u) { // wrapped 65536 spins -- link stalled
return n;
@@ -490,6 +524,5 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
}
*gData = buf[n];
}
- jlpSerialPoll();
return n;
}
diff --git a/src/iigs/serialIrq.s b/src/iigs/serialIrq.s
new file mode 100644
index 0000000..5e0fbd0
--- /dev/null
+++ b/src/iigs/serialIrq.s
@@ -0,0 +1,364 @@
+; serialIrq.s - interrupt-driven receive for the IIgs serial add-on (serial.c).
+;
+; GAS syntax for the llvm-mos w65816 toolchain (conventions: peislam.s,
+; joeyDraw.s). Everything is ONE section, so the handlers, the shared drain
+; routines and the C-callable helpers reach each other with 16-bit jsr/rts.
+;
+; The ROM's interrupt manager (Bank FF monitor_bram.intr071 INTHNDLR) hands
+; each source to a Misc Tool Set vector, which serial.c points here with
+; GetVector/SetVector:
+;
+; $0009 serial ($E1/0024) -> iigsSccIsr. Called straight from INTHNDLR's
+; fast path, for an SCC interrupt AppleTalk refused and whose RR3 bits
+; are in SERFLAG ($E1/0104), with M=X=8, DBR=$E1, D=0 and
+; A = RR3 & SERFLAG -- and BEFORE the ROM has switched to its own
+; stack or normalised STATEREG, so this runs on the interrupted
+; program's stack with whatever RAMRD/RAMWRT it had set (the ROM's
+; own serial handler, serialdrvr SERPINT, saves and clears them).
+; $0017 other ($E1/005C) -> iigsSscIsr. Called last, for an interrupt no
+; built-in source claimed (a slot card), after the ROM has saved the
+; machine and normalised it: M=X=8, DBR=0, D=0, main memory.
+;
+; Both return carry clear for "serviced" and M=X=8 (the ROM ROR/PLAs a byte
+; right after the call). Anything that is not ours goes on to the vector's
+; previous owner by an RTL to gIigsSerChain (its address minus one), with the
+; registers as the ROM left them.
+;
+; Receive ring protocol, single producer / single consumer: the producer
+; (ringPut, only ever run by an interrupt handler or under SEI) stores the
+; byte, then publishes gIigsRxTail with one 16-bit store; the mainline only
+; ever moves gIigsRxHead. C globals are reached with long addressing, so the
+; data bank does not matter; the I/O is DBR-relative through Y, and every path
+; sets DBR to a bank where $C0xx is I/O ($E1 here, or $00 in iigsSscIsr where
+; the ROM has I/O shadowing on).
+;
+; Port layout (gIigsSerPort, from serial.c):
+; SCC Y = channel command register ($C038 B / $C039 A); data at 2,y.
+; 6551 Y = data register ($C0n8); status at 1,y, command at 2,y.
+
+ .section .text.iigsSerialIrq,"ax"
+ .globl iigsSccIsr
+ .globl iigsSscIsr
+ .globl iigsSerialService
+ .globl iigsSerialIsrAddress
+ .globl iigsIrqOff
+ .globl iigsIrqRestore
+
+SER_KIND_SCC = 1 ; gIigsSerKind values (IigsSerKindE in serial.c)
+SER_KIND_ACIA = 2
+STATEREG = 0xE0C068
+SERFLAG = 0xE10104
+SCC_A_CTRL = 0xC039 ; RR3 exists on channel A only
+SCC_RR0_RX = 0x01
+SCC_RR1_ERRORS = 0x70 ; framing | overrun | parity
+SCC_RR1_OVERRUN = 0x20
+SCC_WR0_ERR_RST = 0x30
+SCC_WR0_EXT_RST = 0x10
+ACIA_ST_IRQ = 0x80
+ACIA_ST_RDRF = 0x08
+ACIA_ST_OVERRUN = 0x04
+STATE_MAIN_MEM = 0x8F ; clears PAGE2, RAMRD, RAMWRT (SERPINT's mask)
+
+; ----------------------------------------------------------------
+; SCC receive interrupt (serial vector).
+; ----------------------------------------------------------------
+iigsSccIsr:
+ .a8
+ .i8
+ pha ; RR3 & SERFLAG, handed on if we chain
+ lda >STATEREG
+ pha
+ and #STATE_MAIN_MEM
+ sta >STATEREG
+ lda #3
+ sta SCC_A_CTRL ; point at RR3 (DBR = $E1)
+ lda SCC_A_CTRL
+ and >gIigsSerIpMask
+ beq sccIsrPass ; none of ours pending
+ rep #0x30
+ .a16
+ .i16
+ lda >gIigsSerPort
+ tay
+ sep #0x20
+ .a8
+ jsr sccDrain
+ lda #3
+ sta SCC_A_CTRL
+ lda SCC_A_CTRL
+ and >SERFLAG
+ ora >gIigsSerIpMask
+ eor >gIigsSerIpMask ; = pending bits the previous owner asked for
+ bne sccIsrPass
+ pla
+ sta >STATEREG
+ pla
+ sep #0x30
+ .i8
+ clc
+ rtl
+sccIsrPass:
+ pla
+ sta >STATEREG
+ pla
+ sep #0x30
+ .i8
+ bra chain
+
+; ----------------------------------------------------------------
+; 6551 receive interrupt (other-interrupt vector).
+; ----------------------------------------------------------------
+iigsSscIsr:
+ .a8
+ .i8
+ phb
+ pea 0xE1E1
+ plb
+ plb ; DBR = $E1: $C0xx is I/O whatever the shadow state
+ rep #0x30
+ .a16
+ .i16
+ lda >gIigsSerKind
+ cmp #SER_KIND_ACIA
+ bne sscIsrPass
+ lda >gIigsSerPort
+ tay
+ sep #0x20
+ .a8
+ lda 1,y ; status; the read also clears the IRQ bit
+ bpl sscIsrPass ; IRQ bit clear: another card's interrupt
+ jsr sscDrain
+ plb
+ sep #0x30
+ .i8
+ clc
+ rtl
+sscIsrPass:
+ plb
+ sep #0x30
+ .a8
+ .i8
+
+; Hand the interrupt to the vector's previous owner: push its address - 1 and
+; RTL there. A is parked in B, which 8-bit loads leave alone.
+chain:
+ xba
+ lda >gIigsSerChain+2
+ pha
+ lda >gIigsSerChain+1
+ pha
+ lda >gIigsSerChain
+ pha
+ xba
+ rtl
+
+; ----------------------------------------------------------------
+; uint16_t iigsSerialService(void)
+;
+; The mainline's view of the open port: under SEI, drain whatever the chip
+; holds into the ring (the same drain the handlers run) and return the status
+; register -- SCC RR0 after the drain, 6551 status before it -- for the
+; transmit-ready test. A 6551 status read clears its IRQ bit, so it may only
+; ever happen here or in the handler, both of which then take the byte.
+; Returns 0 when no port is open.
+; ----------------------------------------------------------------
+iigsSerialService:
+ php
+ sei
+ phb
+ pea 0xE1E1
+ plb
+ plb
+ rep #0x30
+ .a16
+ .i16
+ lda >gIigsSerPort
+ tay
+ lda >gIigsSerKind
+ cmp #SER_KIND_SCC
+ beq svcScc
+ cmp #SER_KIND_ACIA
+ beq svcAcia
+ lda #0
+ bra svcDone
+svcScc:
+ sep #0x20
+ .a8
+ jsr sccDrain
+ lda 0,y ; RR0 again: transmit-buffer-empty is bit 2
+ bra svcWiden
+svcAcia:
+ sep #0x20
+ .a8
+ lda 1,y
+ pha
+ jsr sscDrain
+ pla
+svcWiden:
+ rep #0x20
+ .a16
+ and #0x00FF
+svcDone:
+ plb
+ plp
+ rtl
+
+; ----------------------------------------------------------------
+; Drains. M=8, X=16, Y = port (see the header), DBR = an I/O bank.
+; Clobber A and X; preserve Y.
+; ----------------------------------------------------------------
+sccDrain:
+ .a8
+ .i16
+ lda 0,y ; RR0 (the pointer rests at 0)
+ and #SCC_RR0_RX
+ beq sccDrainDone
+ lda 2,y ; the byte
+ jsr ringPut
+ lda #1
+ sta 0,y ; point at RR1: did that byte carry an error?
+ lda 0,y
+ and #SCC_RR1_ERRORS
+ beq sccDrain
+ and #SCC_RR1_OVERRUN
+ beq sccErrReset
+ jsr countOverrun
+sccErrReset:
+ lda #SCC_WR0_ERR_RST ; unlatch the error (and unlock the FIFO)
+ sta 0,y
+ lda #SCC_WR0_EXT_RST ; MAME raises the special condition on the
+ sta 0,y ; ext/status IP, which only this clears
+ bra sccDrain
+sccDrainDone:
+ rts
+
+; A = a 6551 status byte just read.
+sscDrain:
+ .a8
+ .i16
+ bit #ACIA_ST_RDRF
+ beq sscDrainDone
+ bit #ACIA_ST_OVERRUN ; the byte before this one was lost
+ beq sscTake
+ jsr countOverrun
+sscTake:
+ lda 0,y ; the byte; clears RDRF and the error bits
+ jsr ringPut
+ lda 1,y
+ bra sscDrain
+sscDrainDone:
+ rts
+
+; A (8-bit) = received byte. Store it, then publish the tail; a full ring
+; drops it and counts the drop. Clobbers X.
+ringPut:
+ .a8
+ .i16
+ pha ; the byte
+ rep #0x20
+ .a16
+ lda >gIigsRxTail
+ tax ; X = slot to fill
+ inc a
+ and >gIigsRxRingMask
+ cmp >gIigsRxHead
+ beq ringPutFull
+ pha ; the next tail; the byte is now at 3,s
+ sep #0x20
+ .a8
+ lda 3,s
+ sta >gIigsRxRing,x ; store BEFORE publishing
+ rep #0x20
+ .a16
+ pla
+ sta >gIigsRxTail
+ lda >gIigsRxPumped
+ inc a
+ sta >gIigsRxPumped
+ bra ringPutDone
+ringPutFull:
+ lda >gIigsRxDropped
+ inc a
+ beq ringPutDone ; saturated at 0xFFFF
+ sta >gIigsRxDropped
+ringPutDone:
+ sep #0x20
+ .a8
+ pla
+ rts
+
+; Saturating gIigsRxOverruns++. M=8 in and out; clobbers A.
+countOverrun:
+ .a8
+ rep #0x20
+ .a16
+ lda >gIigsRxOverruns
+ inc a
+ beq countOverrunDone
+ sta >gIigsRxOverruns
+countOverrunDone:
+ sep #0x20
+ .a8
+ rts
+
+; ----------------------------------------------------------------
+; uint32_t iigsSerialIsrAddress(uint16_t kind)
+;
+; The 24-bit address of the handler for `kind`, for SetVector: the offset is
+; a same-section immediate (the loader bank-aligns segments, so it equals the
+; runtime offset) and the bank is K. Returned in A (low 16) : X (bank).
+; ----------------------------------------------------------------
+iigsSerialIsrAddress:
+ .a16
+ .i16
+ cmp #SER_KIND_ACIA
+ beq isrAddrAcia
+ lda #iigsSccIsr
+ bra isrAddrBank
+isrAddrAcia:
+ lda #iigsSscIsr
+isrAddrBank:
+ pha
+ phk
+ sep #0x20
+ .a8
+ pla
+ rep #0x20
+ .a16
+ and #0x00FF
+ tax
+ pla
+ rtl
+
+; ----------------------------------------------------------------
+; uint16_t iigsIrqOff(void) -- SEI; returns the caller's P
+; void iigsIrqRestore(uint16_t p) -- puts that P (and its I bit) back
+;
+; For the mainline's two-access SCC register-pointer sequences: the ROM's
+; interrupt entry reads RR3 through the same chip-wide pointer on EVERY
+; interrupt, so an unmasked pointer write can land as data in the wrong
+; register.
+; ----------------------------------------------------------------
+iigsIrqOff:
+ .a16
+ .i16
+ php
+ sei
+ sep #0x20
+ .a8
+ pla
+ rep #0x20
+ .a16
+ and #0x00FF
+ rtl
+
+iigsIrqRestore:
+ .a16
+ .i16
+ sep #0x20
+ .a8
+ pha
+ plp ; M/X back to the caller's 16/16
+ .a16
+ .i16
+ rtl
diff --git a/src/x68000/hal.c b/src/x68000/hal.c
index 537282e..5c60441 100644
--- a/src/x68000/hal.c
+++ b/src/x68000/hal.c
@@ -197,11 +197,6 @@ static void spreadSetPalBase(uint8_t palBase);
static void uploadGfxPalette(const jlSurfaceT *src);
#endif
-// serial.c: drain the tiny (~64 B) RSDRV receive ring into the port's soft ring. Called from the
-// long-running paths below (present / VBL wait / input poll) so mid-frame serial bytes are never
-// lost while the HAL is busy -- see the soft-ring note in serial.c.
-extern void x68kSerialPump(void);
-
static void vdispHandler(void);
static void vdispPoll(void);
@@ -735,7 +730,6 @@ void jlpPresent(const jlSurfaceT *src) {
uint16_t plane;
uint16_t firstByte;
uint16_t rowBytes;
- uint16_t blitRows = 0;
// Four rows of dirty band read as ONE long. A clean row's minWord is the
// 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so
@@ -768,7 +762,6 @@ void jlpPresent(const jlSurfaceT *src) {
applyScbBands();
gStageScbDirty = false;
}
- x68kSerialPump();
for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) {
if (minGroups[g] == 0xFFFFFFFFul) {
continue;
@@ -778,12 +771,6 @@ void jlpPresent(const jlSurfaceT *src) {
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
continue;
}
- // Same pump cadence as the graphics path: a blitted row is line
- // time, so keep the 64-byte RSDRV ring from overflowing.
- if ((blitRows & 1u) == 0u) {
- x68kSerialPump();
- }
- blitRows++;
// Bands are 16-bit stage words = 4 pixels = 2 bytes of a plane.
firstByte = (uint16_t)(gStageMinWord[y] >> 1);
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
@@ -816,7 +803,6 @@ void jlpPresent(const jlSurfaceT *src) {
const uint8_t *sp;
uint16_t b;
uint8_t palBase;
- uint16_t blitRows = 0;
if (src == NULL) {
return;
@@ -837,18 +823,10 @@ void jlpPresent(const jlSurfaceT *src) {
return;
}
sp = (const uint8_t *)&gSpreadPair[0][0][0];
- x68kSerialPump(); // the palette upload + spread-table build above are already line time
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
continue;
}
- // A blitted row costs ~0.5-0.7 ms, so pump every second one: that keeps the gap near ~1 ms,
- // inside the ~2.8 ms the 64-byte RSDRV ring covers at the port's fastest rate (230400).
- // Clean rows are a flag test and skip the pump, so an idle present stays trap-free.
- if ((blitRows & 1u) == 0u) {
- x68kSerialPump();
- }
- blitRows++;
// Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane.
firstByte = (uint16_t)(gStageMinWord[y] >> 1);
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
@@ -1015,9 +993,6 @@ void jlpInputPoll(void) {
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
- // ring covers at the port's fastest rate, but one group's trap never does.
- x68kSerialPump();
groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF);
}
// Write both states each poll: the bitmap is live key-DOWN data,
@@ -1148,11 +1123,6 @@ void jlpWaitVBL(void) {
guard = 0ul;
while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) {
vdispPoll();
- // Every 64 spins ~= a few hundred microseconds: comfortably inside the ~2.8 ms the 64-byte
- // RSDRV ring covers at 230400. Trap cost is free here -- this is a blocking wait anyway.
- if ((guard & 63ul) == 0ul) {
- x68kSerialPump();
- }
guard++;
}
}
diff --git a/src/x68000/serial.c b/src/x68000/serial.c
index 0d1777d..9e7ef14 100644
--- a/src/x68000/serial.c
+++ b/src/x68000/serial.c
@@ -1,46 +1,89 @@
-// Sharp X68000 serial HAL: RS-232C via the IOCS polled API.
+// Sharp X68000 serial HAL: RS-232C on SCC channel A.
//
-// Structurally the Atari ST HAL (src/atarist/serial.c): ride the OS's polled
-// interface, hijack no vector, buffer nothing ourselves. The IOCS calls map
-// one-for-one onto the ST's XBIOS ones --
+// Transmit goes through IOCS, whose calls map one-for-one onto the ST HAL's
+// XBIOS ones (src/atarist/serial.c) --
// _iocs_set232c <- Rsconf (baud + framing, one packed word)
// _iocs_isns232c <- Bconstat (input status)
// _iocs_inp232c <- Bconin (read one byte, BLOCKS if empty)
// _iocs_osns232c <- Bcostat (transmitter ready)
// _iocs_out232c <- Bconout (write one byte)
+// Receive is our own: the channel A RX interrupt feeds a soft ring (see the
+// takeover section below), and when that cannot be installed the polled
+// pump drains RSDRV.SYS's ring into the same soft ring instead.
//
// The machine has one RS-232C line (SCC channel A); channel B is the mouse and
// is not reachable here, so jlSerialDeviceE is accepted and ignored exactly as
// on the ST.
+#include
#include
#include "port.h"
-// LINE CONFIGURATION IS DELIBERATELY NOT APPLIED YET.
+// ----- Line configuration --------------------------------------------------
//
-// _iocs_set232c takes a single packed mode word, and the packing could not be
-// confirmed. What IS confirmed, from XEiJ's RS232CTerminal.java (which reads
-// the live mode word out of the RSDRV.SYS work area):
-// bit 7 RTS/CTS flow control
-// bit 9 XON/XOFF flow control
-// and its baud-rate table is
-// 0=75 1=150 2=300 3=600 4=1200 5=2400 6=4800 7=9600 8=19200 9=31250
-// 10=38400 11=50000 12=57600 13=76800 14=115200 15=230400
-// i.e. the rate is a FOUR-bit field, so it cannot sit at bits 0-2 with parity
-// at bit 3. The positions of the data/stop/parity fields remain unknown.
+// _iocs_set232c's mode word, from the IOCS call reference (reproduced on Data
+// Crystal's X68k/IOCS page):
+// bits 15-14 stop bits 1 = 1, 2 = 1.5, 0 or 3 = 2
+// bits 13-12 parity 1 = odd, 3 = even, 0 or 2 = none
+// bits 11-10 data bits 0 = 5 or fewer, 1 = 6, 2 = 7, 3 = 8
+// bit 9 XON/XOFF
+// bit 8 SI/SO (does not work in ROM IOCS; never set)
+// bit 7 RTS/CTS (RSDRV.SYS only, below)
+// bits 6-0 rate index 0..7 = 75 150 300 600 1200 2400 4800 9600
+// It returns the mode it replaced, which jlpSerialClose puts back.
//
-// Writing a guessed mode word is worse than writing none: a wrong framing
-// setting still transmits, it just corrupts every byte, which is exactly the
-// failure this port hit first time out (every received byte came back as 0x00
-// or 0x80 -- only the MSB surviving).
-//
-// So jlpSerialOpen inherits whatever RSDRV.SYS configured at boot. The Human68k
-// "RS-232C DRIVER for X68000 version 2.02" banner appears during startup and
-// leaves the port at its default, which pairs with MAME's null_modem default
-// of 9600 8N1. Honour jlSerialConfigT properly once the layout is confirmed
-// against hardware documentation.
+// Two implementations answer it, and they differ above 9600 (both read from
+// their code). The ROM's (IPL-ROM 1.0) takes indexes 0..7 and turns anything
+// else -- bit 7 included -- into 1200. Human68k's RS-232C driver RSDRV.SYS
+// (1.13 on the Human68k 3.02 system disk, whose CONFIG.SYS loads it) replaces
+// the RS-232C IOCS calls, takes indexes 0..9 -- 8 = 19200, 9 = 38400 (time
+// constant 2, really 39062 bps, 1.7% fast) -- and reads bit 7 as RTS/CTS,
+// which overrides XON/XOFF: its _OSNS232C reports "not ready" while CTS is
+// off, and its receive side drops RTS as its buffer fills. So the rates above
+// 9600 and RTS/CTS are used only when RSDRV is detected (x68kRsdrvPresent);
+// without it a faster request clamps to 9600 and RTS/CTS falls back to no
+// handshaking. With this HAL's receive interrupt installed (below) RSDRV's
+// receive side never runs, so RTS stays up and the 4 KB soft ring is the
+// receive buffer; transmit still waits for CTS.
+#define X68K_MODE_STOP_1 0x4000u
+#define X68K_MODE_STOP_2 0xC000u
+#define X68K_MODE_PARITY_ODD 0x1000u
+#define X68K_MODE_PARITY_EVEN 0x3000u
+#define X68K_MODE_DATA_SHIFT 10
+#define X68K_MODE_DATA_MASK 0x03u
+#define X68K_MODE_XONXOFF 0x0200u
+#define X68K_MODE_RTSCTS 0x0080u
+#define X68K_DATA_BITS_MIN 5u
+#define X68K_DATA_BITS_MAX 8u
+#define X68K_RATE_COUNT_ROM 8u
+#define X68K_RATE_COUNT_RSDRV 10u
+
+static const uint16_t kX68kRates[X68K_RATE_COUNT_RSDRV] = { 75u, 150u, 300u, 600u, 1200u, 2400u, 4800u, 9600u, 19200u, 38400u };
+
+// RSDRV.SYS detection. The driver adds an extended RS-232C IOCS call, $F1
+// (d2 = function | port << 8), whose function $37 answers $0201 and touches
+// nothing (read from its code). The ROM leaves $F1 unassigned: its vector
+// holds the call number in the top byte over the ROM's error handler
+// ($F1FF1B1E on IPL-ROM 1.0), and calling that is an error, so the call is
+// made only when the vector points into RAM. DOS _INTVCG reads the vector
+// from user mode; IOCS call n is its vector $100 + n.
+#define X68K_IOCS_RS232C_EXT 0xF1
+#define X68K_INTVC_IOCS_BASE 0x100
+#define X68K_RSDRV_IDENT_FN 0x37
+#define X68K_RSDRV_IDENT 0x0201
+#define X68K_RAM_END 0x00C00000ul
+
+// WR5 as _SET232C writes it -- the ROM and RSDRV alike: DTR, Tx enable and
+// RTS, plus the Tx bits-per-char code in bits 6-5 -- so jlpSerialSetDtr can
+// change DTR alone (WR5 is write-only). The data-bits code of the mode word
+// (0 = 5 .. 3 = 8) becomes the Z8530's (5 = 0, 6 = 2, 7 = 1, 8 = 3) by
+// swapping 6 and 7.
+#define X68K_WR5_BASE 0x8Au
+#define X68K_WR5_DTR 0x80u
+#define X68K_WR5_BITS_SHIFT 5
+#define X68K_SCC_WR5 5u
// Bounded transmitter-ready spin, so a stalled link (no CTS) reports a partial
// write instead of hanging the frame. Same guard the ST HAL uses.
@@ -48,24 +91,18 @@
// ----- Soft receive ring -----------------------------------------------------
//
-// RSDRV.SYS's own receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a
-// 203-byte frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the
-// signature of a 64-entry ring filling while the client was busy and discarding until drained. Every
-// other JoeyLib target rides a big interrupt-fed ring (DOS hardware IRQ ring, ST XBIOS IOREC, and the
-// 8-bit clients' IRQ rings), so only this port could lose mid-frame bytes whenever the caller went
-// deaf for longer than ~66 ms at 9600 baud -- and jlpPresent's full-stage blit, jlpWaitVBL's V-DISP
-// spin, and jlpInputPoll's IOCS trap storm all can. The fix: drain the IOCS ring into this soft ring
-// from INSIDE those long-running paths (x68kSerialPump below), so the 64-byte hardware-side window
-// only ever has to cover the gap between pumps, never a whole present. No ISR is involved -- the
-// pump is a plain call -- so none of the vdispst-style vector hazards apply.
+// The IOCS receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a 203-byte
+// frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the signature
+// of a 64-entry ring filling while the client was busy and discarding until drained. So this port
+// takes the receive interrupt over (below) and fills a soft ring of its own, the way DOS rides a
+// hardware IRQ ring and the ST TOS's IOREC: the program can go deaf for a whole present without
+// losing a byte. Only when the takeover cannot be installed does x68kSerialPump drain the IOCS
+// ring instead, from the add-on's own entry points -- then the app must call jlSerialPoll often.
//
-// SIZED FOR THE FASTEST LINE, not just the 9600 default: this port inherits RSDRV.SYS's boot rate
-// (the mode word is deliberately not written), and RSDRV's table reaches 230400, where the 64-byte
-// IOCS ring covers only ~2.8 ms -- so every pump site keeps its interval near 1 ms (present: every
-// 2 blitted rows; input poll: every bitsns group; VBL wait: every 64 spins). 4 KB here holds ~178 ms
-// of full-rate 230400 traffic, longer than the worst deaf window (a >100 ms all-dirty present), so
-// even an unpaced burst -- the server's flowWait timeout degrade streams a whole screen without acks
-// -- fits while the loop is busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes.
+// 4 KB holds over four seconds at 9600 (jlpSerialOpen's ceiling, see the mode-word note above),
+// longer than the worst deaf window (a >100 ms all-dirty present), so even an unpaced burst -- the
+// server's flowWait timeout degrade streams a whole screen without acks -- fits while the loop is
+// busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes.
#define X68K_RX_RING_CAP 4096u
// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer
@@ -92,43 +129,46 @@ volatile uint16_t gX68kRxTail = 0u; // push side
extern void x68kSccRxIsr(void);
extern void x68kSccSpIsr(void);
-static uint16_t x68kIrqMask(void);
-static void x68kIrqRestore(uint16_t sr);
static void sccIsrInstall(void);
static void sccIsrUninstall(void);
+static bool x68kInSupervisor(void);
+static uint16_t x68kIrqMask(void);
+static void x68kIrqRestore(uint16_t sr);
+static bool x68kRsdrvPresent(void);
+static uint16_t x68kSerialMode(const jlSerialConfigT *config, bool rsdrv);
+static void x68kSerialPump(void);
+static int32_t x68kSuperEnter(void);
+static void x68kSuperLeave(int32_t ssp);
+static uint8_t x68kWr5ForMode(uint16_t mode);
+static void x68kWr5Write(uint8_t wr5);
static bool gSccIsrInstalled = false;
// Has the app opened the port at all? Without this the polled pump traps into
// IOCS on every call, and jlpPresent calls it once every two blitted rows --
// about fifty _ISNS232C traps per frame for a game that never touched serial.
static bool gSerialOpened = false;
+// The mode _iocs_set232c replaced at open, put back at close.
+static int gPrevMode = 0;
+// Channel A's WR5 as last written (see X68K_WR5_BASE), for jlpSerialSetDtr.
+static uint8_t gX68kWr5 = 0u;
static uint8_t gSccRxVec = 0u;
static uint32_t gSccOldRx = 0ul;
static uint32_t gSccOldSp = 0ul;
-static uint16_t x68kIrqMask(void) {
- uint16_t sr;
-
- __asm__ volatile("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "cc");
- return sr;
-}
-
-
-static void x68kIrqRestore(uint16_t sr) {
- __asm__ volatile("move.w %0,%%sr" : : "d"(sr) : "cc");
-}
-
-
static void sccIsrInstall(void) {
uint16_t sr;
uint8_t base;
- if (gSccIsrInstalled) {
+ // Only a program already in supervisor mode (every jlInit app) takes the
+ // interrupt over. One in user mode (SERTEST) stays on the polled pump,
+ // which needs no privilege and races nothing -- and since there is then
+ // no ISR, jlpSerialWrite needs no interrupt mask either.
+ if (gSccIsrInstalled || !x68kInSupervisor()) {
return;
}
// RR2 read is a two-access pointer sequence on the shared SCC, so no interrupt (the channel B
- // mouse) may interleave. We run supervisor (jlpInit's _dos_super), so the SR mask is legal.
+ // mouse) may interleave.
sr = x68kIrqMask();
*X68K_SCC_A_CTRL = 2u;
base = *X68K_SCC_A_CTRL;
@@ -151,24 +191,108 @@ static void sccIsrInstall(void) {
static void sccIsrUninstall(void) {
uint16_t sr;
+ int32_t ssp;
if (!gSccIsrInstalled) {
return;
}
+ // Supervisor was a given at install time, but the app may have left it
+ // since (jlShutdown before jlSerialClose), so make sure of it here.
+ ssp = x68kSuperEnter();
sr = x68kIrqMask();
*X68K_VECTOR(gSccRxVec) = gSccOldRx;
*X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp;
x68kIrqRestore(sr);
+ x68kSuperLeave(ssp);
gSccIsrInstalled = false;
}
-// Drain everything the IOCS ring holds into the soft ring. Called from the HAL's own long-running
-// paths (present / VBL wait / input poll) and from every serial entry point. With the SCC ISR
-// installed this is a cheap no-op guard (RSDRV's ring never fills); without it, it is the polled
-// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead
-// of RSDRV's ~64 bytes.
-void x68kSerialPump(void) {
+// Installing and removing the RX ISR touches the vector table, the SCC and
+// the SR, all supervisor-only. Inside a JoeyLib app jlInit has already
+// switched to supervisor, but the add-on must not depend on that: SERTEST
+// opens the port without jlInit. _dos_super(0) switches and returns the old
+// stack pointer, or a negative value when the CPU already was supervisor;
+// hand that value to x68kSuperLeave, which only switches back if this did.
+static bool x68kInSupervisor(void) {
+ int32_t ssp;
+
+ ssp = x68kSuperEnter();
+ x68kSuperLeave(ssp);
+ return ssp < 0;
+}
+
+
+static uint16_t x68kIrqMask(void) {
+ uint16_t sr;
+
+ __asm__ volatile("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "cc");
+ return sr;
+}
+
+
+static void x68kIrqRestore(uint16_t sr) {
+ __asm__ volatile("move.w %0,%%sr" : : "d"(sr) : "cc");
+}
+
+
+// Is RSDRV.SYS loaded? See X68K_IOCS_RS232C_EXT for the test.
+static bool x68kRsdrvPresent(void) {
+ register int32_t ret __asm__("d0");
+ register int32_t fn __asm__("d2");
+
+ if ((uint32_t)_dos_intvcg(X68K_INTVC_IOCS_BASE + X68K_IOCS_RS232C_EXT) >= X68K_RAM_END) {
+ return false;
+ }
+ ret = X68K_IOCS_RS232C_EXT;
+ fn = X68K_RSDRV_IDENT_FN;
+ __asm__ volatile("trap #15" : "+d"(ret) : "d"(fn) : "d1", "a0", "a1", "cc", "memory");
+ return ret == X68K_RSDRV_IDENT;
+}
+
+
+// The _iocs_set232c mode word for a JoeyLib line configuration (see the table
+// above): the rate snaps down to the nearest one the IOCS in charge supports,
+// and RTS/CTS becomes no handshaking without RSDRV.SYS.
+static uint16_t x68kSerialMode(const jlSerialConfigT *config, bool rsdrv) {
+ uint16_t mode;
+ uint8_t rate;
+ uint8_t rateCount;
+ uint8_t dataBits;
+
+ rateCount = rsdrv ? X68K_RATE_COUNT_RSDRV : X68K_RATE_COUNT_ROM;
+ rate = 0u;
+ while (rate + 1u < rateCount && kX68kRates[rate + 1u] <= config->baud) {
+ rate++;
+ }
+ dataBits = config->dataBits;
+ if (dataBits < X68K_DATA_BITS_MIN) {
+ dataBits = X68K_DATA_BITS_MIN;
+ }
+ if (dataBits > X68K_DATA_BITS_MAX) {
+ dataBits = X68K_DATA_BITS_MAX;
+ }
+ mode = (uint16_t)(rate | ((uint16_t)(dataBits - X68K_DATA_BITS_MIN) << X68K_MODE_DATA_SHIFT));
+ mode |= (config->stopBits >= 2u) ? X68K_MODE_STOP_2 : X68K_MODE_STOP_1;
+ if (config->parity == JL_SERIAL_PARITY_ODD) {
+ mode |= X68K_MODE_PARITY_ODD;
+ } else if (config->parity == JL_SERIAL_PARITY_EVEN) {
+ mode |= X68K_MODE_PARITY_EVEN;
+ }
+ if (config->flow == JL_SERIAL_FLOW_XONXOFF) {
+ mode |= X68K_MODE_XONXOFF;
+ }
+ if (rsdrv && config->flow == JL_SERIAL_FLOW_RTSCTS) {
+ mode |= X68K_MODE_RTSCTS;
+ }
+ return mode;
+}
+
+
+// Drain everything the IOCS ring holds into the soft ring. Called from every serial entry point.
+// With the SCC ISR installed this is a cheap no-op guard (the IOCS ring never fills); without it, it
+// is the polled fallback that keeps the port working.
+static void x68kSerialPump(void) {
if (gSccIsrInstalled || !gSerialOpened) {
return;
}
@@ -184,6 +308,48 @@ void x68kSerialPump(void) {
}
+static int32_t x68kSuperEnter(void) {
+ return (int32_t)_dos_super(0);
+}
+
+
+static void x68kSuperLeave(int32_t ssp) {
+ if (ssp >= 0) {
+ (void)_dos_super((int)ssp);
+ }
+}
+
+
+// WR5 as _SET232C writes it for `mode` (see X68K_WR5_BASE).
+static uint8_t x68kWr5ForMode(uint16_t mode) {
+ uint8_t code;
+
+ code = (uint8_t)((mode >> X68K_MODE_DATA_SHIFT) & X68K_MODE_DATA_MASK);
+ if (code == 1u || code == 2u) {
+ code = (uint8_t)(code ^ X68K_MODE_DATA_MASK);
+ }
+ return (uint8_t)(X68K_WR5_BASE | (uint8_t)(code << X68K_WR5_BITS_SHIFT));
+}
+
+
+// Write channel A's WR5 and remember it. The pointer write and the data write
+// are two accesses to the chip-wide register pointer, so no interrupt (our RX
+// ISR, the channel B mouse) may come between them -- see jlpSerialWrite. The
+// SCC is supervisor-only, and the app may have left supervisor mode.
+static void x68kWr5Write(uint8_t wr5) {
+ int32_t ssp;
+ uint16_t sr;
+
+ gX68kWr5 = wr5;
+ ssp = x68kSuperEnter();
+ sr = x68kIrqMask();
+ *X68K_SCC_A_CTRL = X68K_SCC_WR5;
+ *X68K_SCC_A_CTRL = wr5;
+ x68kIrqRestore(sr);
+ x68kSuperLeave(ssp);
+}
+
+
// ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) {
@@ -192,8 +358,21 @@ uint16_t jlpSerialAvailable(void) {
}
+// Close leaves DTR as it is. _SET232C raises it whenever it runs, so a line the
+// program hung up (jlSerialSetDtr) is dropped again straight after the restore:
+// it is up only for the rest of that call, 0.17 ms under MAME with RSDRV.SYS.
void jlpSerialClose(void) {
+ bool dtrOn;
+
+ if (!gSerialOpened) {
+ return;
+ }
+ dtrOn = (gX68kWr5 & X68K_WR5_DTR) != 0u;
sccIsrUninstall(); // hand channel A RX back to RSDRV before the vectors dangle
+ (void)_iocs_set232c(gPrevMode);
+ if (!dtrOn) {
+ x68kWr5Write((uint8_t)(x68kWr5ForMode((uint16_t)gPrevMode) & (uint8_t)~X68K_WR5_DTR));
+ }
gSerialOpened = false;
}
@@ -207,16 +386,15 @@ void jlpSerialFlush(void) {
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
+ uint16_t mode;
+
(void)device; // one RS-232C line; channel B is the mouse.
if (config == NULL) {
return false;
}
- // Line config is accepted but NOT applied -- see the mode-word note above. The
- // port keeps whatever RSDRV.SYS set at boot. (An RTS/CTS enable via an
- // _iocs_set232c(-1) read-modify-write was tried and REVERTED: the -1-as-query
- // idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the
- // RSDRV work area instead -- so the write could not be trusted.)
- (void)config;
+ mode = x68kSerialMode(config, x68kRsdrvPresent());
+ gPrevMode = _iocs_set232c((int)mode);
+ gX68kWr5 = x68kWr5ForMode(mode); // _SET232C just wrote this, DTR on
gSerialOpened = true;
sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong
return true;
@@ -242,6 +420,22 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
}
+// DTR on demand (jlSerialSetDtr, how a program hangs up): WR5 bit 7, written
+// directly -- neither the ROM nor RSDRV.SYS has an IOCS call for it that keeps
+// the data bits. Whatever runs _SET232C next (the next open, another program)
+// raises DTR again; close keeps a dropped one down.
+void jlpSerialSetDtr(bool on) {
+ if (!gSerialOpened) {
+ return;
+ }
+ if (on) {
+ x68kWr5Write((uint8_t)(gX68kWr5 | X68K_WR5_DTR));
+ } else {
+ x68kWr5Write((uint8_t)(gX68kWr5 & (uint8_t)~X68K_WR5_DTR));
+ }
+}
+
+
// The Z8530's register pointer is ONE register for the whole chip, shared by
// both channels, and selecting a register is a separate access from using it.
// So any two-access sequence has to be atomic or the two accesses belong to
@@ -271,12 +465,18 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t sr;
int ready;
+ // The masks keep our RX ISR out of the IOCS calls' SCC accesses; with no
+ // ISR installed (the polled path) there is nothing to keep out.
for (n = 0u; n < len; n++) {
guard = 0ul;
for (;;) {
- sr = x68kIrqMask();
- ready = _iocs_osns232c();
- x68kIrqRestore(sr);
+ if (gSccIsrInstalled) {
+ sr = x68kIrqMask();
+ ready = _iocs_osns232c();
+ x68kIrqRestore(sr);
+ } else {
+ ready = _iocs_osns232c();
+ }
if (ready != 0) {
break;
}
@@ -285,9 +485,13 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
return n;
}
}
- sr = x68kIrqMask();
- _iocs_out232c((int)buf[n]);
- x68kIrqRestore(sr);
+ if (gSccIsrInstalled) {
+ sr = x68kIrqMask();
+ _iocs_out232c((int)buf[n]);
+ x68kIrqRestore(sr);
+ } else {
+ _iocs_out232c((int)buf[n]);
+ }
}
return n;
}
diff --git a/src/x68000/x68kTile.h b/src/x68000/x68kTile.h
index 2971b58..ee77f80 100644
--- a/src/x68000/x68kTile.h
+++ b/src/x68000/x68kTile.h
@@ -1,15 +1,14 @@
-// Sharp X68000 planar tile ops -- adapted verbatim from src/amiga/x68kTile.h.
+// Sharp X68000 planar tile ops -- adapted verbatim from src/amiga/amigaTile.h.
// The surface layout is identical (40 bytes per row per plane), so an 8x8 tile
-// is 8 rows x 4 planes x 1 byte on both machines and the .tbk blob format is
-// byte-compatible; only the type names differ.
-// Amiga tile ops as always-inline functions (NATIVE-PERF Phase 2
-// R4a). Bodies moved verbatim from src/amiga/hal.c so the public
-// wrappers in src/core/tile.c inline them, deleting the second full
-// cdecl call layer (~130-190 cycles/op on the 68000). The jlpTile*
-// dispatch names are macro-aliased to these in port.h, which also
-// #includes this header at its tail -- AFTER the jlpGenericTile*
-// prototypes the portData == NULL fallbacks call. Do not include this
-// header directly; it comes with port.h on the Amiga.
+// is 8 rows x 4 planes x 1 byte on both machines and the .tbk payload is
+// byte-compatible (assetbake --target x68000 writes the Amiga layout under its
+// own target byte); only the type names differ.
+// Tile ops as always-inline functions, so the public wrappers in
+// src/core/tile.c inline them, deleting the second full cdecl call layer
+// (~130-190 cycles/op on the 68000). The jlpTile* dispatch names are
+// macro-aliased to these in port.h, which also #includes this header at its
+// tail -- AFTER the jlpGenericTile* prototypes the portData == NULL fallbacks
+// call. Do not include this header directly; it comes with port.h.
#ifndef JOEY_X68K_TILE_H
#define JOEY_X68K_TILE_H
diff --git a/tests/host/serialHost.c b/tests/host/serialHost.c
index a0695da..d4f8240 100644
--- a/tests/host/serialHost.c
+++ b/tests/host/serialHost.c
@@ -10,6 +10,7 @@
// final tally; exits nonzero if any check failed, so it doubles as a CI gate.
#include
+#include
#include
#include "joey/serial.h"
@@ -27,6 +28,10 @@ static uint16_t gFifoTail;
static bool gMockOpen;
static jlSerialConfigT gMockLastCfg; // config the core handed the HAL
static bool gMockOpenResult = true; // test can force open failure
+static bool gMockRts = true; // last RTS level the core handed the HAL
+static uint16_t gMockRtsCalls = 0u;
+static bool gMockDtr = true; // last DTR level the core handed the HAL
+static uint16_t gMockDtrCalls = 0u;
// ----- Mock HAL (satisfies the jlpSerial* dispatch under -DJL_HAS_SERIAL_*) -----
@@ -72,6 +77,18 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
}
+void jlpSerialSetDtr(bool on) {
+ gMockDtr = on;
+ gMockDtrCalls++;
+}
+
+
+void jlpSerialSetRts(bool on) {
+ gMockRts = on;
+ gMockRtsCalls++;
+}
+
+
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
@@ -103,6 +120,19 @@ static void check(const char *name, bool ok) {
}
+// Registered before the first jlSerialOpen, so it runs after the serial add-on's
+// own atexit handler (last registered, first run): the port main leaves open
+// must be closed by then.
+static void checkClosedAtExit(void) {
+ if (gMockOpen) {
+ printf(" FAIL atexit: a port left open was not closed\n");
+ fflush(stdout);
+ _Exit(1);
+ }
+ printf(" ok atexit: a port left open was closed\n");
+}
+
+
static jlSerialConfigT makeConfig(uint32_t baud, uint8_t dataBits, uint8_t stopBits) {
jlSerialConfigT cfg;
@@ -122,6 +152,7 @@ int main(void) {
int16_t b;
printf("serial-host: core gate/normalize/loopback\n");
+ atexit(checkClosedAtExit);
// 1. Every call is inert before a successful open.
check("closed: available == 0", jlSerialAvailable() == 0u);
@@ -129,6 +160,10 @@ int main(void) {
check("closed: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u);
check("closed: readByte == -1", jlSerialReadByte() == -1);
check("closed: writeByte == false", jlSerialWriteByte('x') == false);
+ jlSerialSetRts(false);
+ check("closed: setRts never reaches the HAL", gMockRtsCalls == 0u);
+ jlSerialSetDtr(false);
+ check("closed: setDtr never reaches the HAL", gMockDtrCalls == 0u);
// 2. NULL config and out-of-range framing are rejected, and a rejected
// open must leave the port closed (a later valid open still works).
@@ -144,6 +179,15 @@ int main(void) {
// 3. Zero fields normalize to library defaults before reaching the HAL.
cfg = makeConfig(0u, 0u, 0u);
check("open: zero-field config accepted", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == true);
+ jlSerialSetRts(false);
+ check("setRts: false reaches the HAL", gMockRtsCalls == 1u && !gMockRts);
+ jlSerialSetRts(true);
+ check("setRts: true reaches the HAL", gMockRtsCalls == 2u && gMockRts);
+ jlSerialSetDtr(false);
+ check("setDtr: false reaches the HAL", gMockDtrCalls == 1u && !gMockDtr);
+ jlSerialSetDtr(true);
+ check("setDtr: true reaches the HAL", gMockDtrCalls == 2u && gMockDtr);
+ check("setDtr: RTS untouched", gMockRtsCalls == 2u && gMockRts);
check("normalize: baud -> 9600", gMockLastCfg.baud == 9600u);
check("normalize: dataBits -> 8", gMockLastCfg.dataBits == 8u);
check("normalize: stopBits -> 1", gMockLastCfg.stopBits == 1u);
@@ -172,8 +216,14 @@ int main(void) {
jlSerialFlush();
check("flush: available == 0", jlSerialAvailable() == 0u);
- // 8. Close re-gates the API; a fresh open works afterward.
+ // 8. Close re-gates the API; a fresh open works afterward. A hang-up
+ // (DTR dropped before the close) is the HAL's to keep: the core must
+ // not touch DTR on the way out.
+ jlSerialSetDtr(false);
jlSerialClose();
+ check("close: core leaves DTR alone", gMockDtrCalls == 3u && !gMockDtr);
+ jlSerialSetDtr(true);
+ check("closed again: setDtr never reaches the HAL", gMockDtrCalls == 3u && !gMockDtr);
check("closed again: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u);
cfg = makeConfig(2400u, 8u, 1u);
check("reopen after close", jlSerialOpen(JL_SERIAL_PRINTER, &cfg) == true);
@@ -187,6 +237,10 @@ int main(void) {
check("open: closed after HAL failure", jlSerialWriteByte('x') == false);
gMockOpenResult = true;
+ // 10. A port left open is closed at exit (checked by checkClosedAtExit).
+ cfg = makeConfig(9600u, 8u, 1u);
+ check("left open for the exit check", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == true);
+
printf("serial-host: %d passed, %d failed\n", gPass, gFail);
return (gFail == 0) ? 0 : 1;
}
diff --git a/toolchains/env.sh b/toolchains/env.sh
index 7e04ad3..315b6cd 100644
--- a/toolchains/env.sh
+++ b/toolchains/env.sh
@@ -7,6 +7,7 @@ export AMIGA_AR='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-ami
export AMIGA_LD='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-ld'
export AMIGA_AS='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/vasmm68k_mot'
export AMIGA_NDK='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/m68k-amigaos/sys-include'
+export AMIGA_SERIAL_DEVICE='/home/scott/claude/joeylib/toolchains/firmware/amiga/Devs/serial.device'
export CADIUS='/home/scott/claude/joeylib/toolchains/opt/cadius/bin/cadius'
export DOS_CWSDPMI='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin'
export DOS_CWSDSTUB='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin/CWSDSTUB.EXE'
diff --git a/tools/adfTool.py b/tools/adfTool.py
new file mode 100755
index 0000000..b4c0117
--- /dev/null
+++ b/tools/adfTool.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""adfTool.py - read files out of an Amiga OFS/FFS disk image (.adf).
+
+ adfTool.py list IMAGE every file, one path per line
+ adfTool.py extract IMAGE PATH OUT one file (PATH like Devs/serial.device)
+ adfTool.py extractall IMAGE OUTDIR every file, keeping the drawer tree
+
+Read-only. Used by the installer's `import` (a Workbench disk image is how
+most people own AmigaOS files such as Devs/serial.device) and by the Amiga
+gates. Handles DD (880 KB) and HD (1.76 MB) images; names match case-
+insensitively, as AmigaDOS does.
+"""
+
+import os
+import struct
+import sys
+
+BLOCK_SIZE = 512
+HASH_SLOTS = 72 # hash-table entries in a 512-byte header block
+TYPE_HEADER = 2 # primary type of a root / drawer / file header
+SEC_TYPE_ROOT = 1
+SEC_TYPE_USERDIR = 2
+SEC_TYPE_FILE = -3
+OFS_DATA_HEADER = 24 # OFS data blocks start with a 24-byte header
+
+
+class AdfImage:
+ def __init__(self, path):
+ with open(path, "rb") as f:
+ self.image = f.read()
+ if len(self.image) % BLOCK_SIZE != 0 or self.image[0:3] != b"DOS":
+ raise ValueError("%s is not an AmigaDOS disk image" % path)
+ self.isFfs = (self.image[3] & 1) != 0
+ self.rootBlock = (len(self.image) // BLOCK_SIZE) // 2
+
+ def block(self, number):
+ return self.image[number * BLOCK_SIZE:(number + 1) * BLOCK_SIZE]
+
+ @staticmethod
+ def long(data, offset):
+ # Negative offsets count from the end of the block, as the
+ # AmigaDOS structure definitions do.
+ if offset < 0:
+ offset += BLOCK_SIZE
+ return struct.unpack(">l", data[offset:offset + 4])[0]
+
+ @staticmethod
+ def name(data):
+ length = data[BLOCK_SIZE - 80]
+ return data[BLOCK_SIZE - 79:BLOCK_SIZE - 79 + length].decode("latin-1")
+
+ def entries(self, dirBlock):
+ # (name, header block, secondary type) for everything in a drawer.
+ data = self.block(dirBlock)
+ found = []
+ for slot in range(HASH_SLOTS):
+ number = self.long(data, 24 + 4 * slot)
+ while number:
+ header = self.block(number)
+ if self.long(header, 0) == TYPE_HEADER:
+ found.append((self.name(header), number, self.long(header, -4)))
+ number = self.long(header, -16)
+ return sorted(found, key=lambda e: e[0].upper())
+
+ def walk(self, dirBlock=None, prefix=""):
+ # Yield (path, header block) for every file under a drawer.
+ if dirBlock is None:
+ dirBlock = self.rootBlock
+ for entryName, number, secType in self.entries(dirBlock):
+ path = prefix + entryName
+ if secType == SEC_TYPE_USERDIR:
+ yield from self.walk(number, path + "/")
+ elif secType == SEC_TYPE_FILE:
+ yield path, number
+
+ def find(self, path):
+ current = self.rootBlock
+ for part in path.split("/"):
+ match = [n for entryName, n, _ in self.entries(current) if entryName.upper() == part.upper()]
+ if not match:
+ raise KeyError("not found in image: %s" % path)
+ current = match[0]
+ return current
+
+ def read(self, headerBlock):
+ header = self.block(headerBlock)
+ size = self.long(header, -188)
+ data = bytearray()
+ chunk = header
+ while True:
+ count = self.long(chunk, 8)
+ # Data-block pointers fill the table from the END (slot 71 first).
+ pointers = [self.long(chunk, 24 + 4 * i) for i in range(HASH_SLOTS)][::-1][:count]
+ for pointer in pointers:
+ blockData = self.block(pointer)
+ if self.isFfs:
+ data += blockData
+ else:
+ data += blockData[OFS_DATA_HEADER:OFS_DATA_HEADER + self.long(blockData, 12)]
+ extension = self.long(chunk, -8)
+ if not extension:
+ break
+ chunk = self.block(extension)
+ return bytes(data[:size])
+
+
+def main(argv):
+ if len(argv) < 3 or argv[1] not in ("list", "extract", "extractall"):
+ sys.stderr.write(__doc__)
+ return 2
+ try:
+ image = AdfImage(argv[2])
+ if argv[1] == "list":
+ for path, _ in image.walk():
+ print(path)
+ elif argv[1] == "extract":
+ if len(argv) != 5:
+ sys.stderr.write(__doc__)
+ return 2
+ with open(argv[4], "wb") as f:
+ f.write(image.read(image.find(argv[3])))
+ else:
+ if len(argv) != 4:
+ sys.stderr.write(__doc__)
+ return 2
+ for path, number in image.walk():
+ out = os.path.join(argv[3], *path.split("/"))
+ os.makedirs(os.path.dirname(out), exist_ok=True)
+ with open(out, "wb") as f:
+ f.write(image.read(number))
+ except (OSError, ValueError, KeyError) as error:
+ sys.stderr.write("adfTool.py: %s\n" % error)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/tools/assetbake/assetbake.py b/tools/assetbake/assetbake.py
index bd43233..53f5e5a 100755
--- a/tools/assetbake/assetbake.py
+++ b/tools/assetbake/assetbake.py
@@ -6,8 +6,8 @@
# Each target's runtime can use the baked bytes directly:
# * Tile banks (.tbk) are per-target planar (or chunky for DOS/IIgs).
# The Phase 11 walker reads sprite tile data as chunky 4bpp, but
-# tile-paste is platform-private -- Amiga is plane-major, ST is
-# row-major-with-planes-per-row, DOS / IIgs are chunky 4bpp.
+# tile-paste is platform-private -- Amiga and X68000 are plane-major,
+# ST is row-major-with-planes-per-row, DOS / IIgs are chunky 4bpp.
# * Sprite cels (.spr) are cross-target chunky 4bpp. The sprite walker
# reads chunky and does c2p inline at draw time, so the same blob
# works on every platform.
@@ -27,7 +27,8 @@
# --------------------------------------------------------------------
# offset bytes field
# 0 4 magic "JTB1"
-# 4 1 target (1=amiga, 2=atarist, 3=dos, 4=iigs)
+# 4 1 target (1=amiga, 2=atarist, 3=dos, 4=iigs,
+# 5=x68000)
# 5 1 hasPalette (0 or 1)
# 6 2 tileCount (LE16)
# 8 4 reserved (zero)
@@ -38,6 +39,8 @@
# [r0_p0 r0_p1 r0_p2 r0_p3][r1_p0..]
# dos : chunky 4bpp [r0_b0..3][r1_b0..3]...
# iigs : chunky 4bpp (same as dos)
+# x68000 : plane-major (same as amiga; its own byte so
+# each bank names the build it was made for)
#
# --------------------------------------------------------------------
# .spr format
@@ -70,12 +73,14 @@ TARGET_AMIGA = 1
TARGET_ATARIST = 2
TARGET_DOS = 3
TARGET_IIGS = 4
+TARGET_X68000 = 5
TARGET_NAMES = {
"amiga": TARGET_AMIGA,
"atarist": TARGET_ATARIST,
"dos": TARGET_DOS,
"iigs": TARGET_IIGS,
+ "x68000": TARGET_X68000,
}
TILE_PX = 8 # 8x8 tile
@@ -134,7 +139,7 @@ def encodeTileChunky(grid, ox, oy):
def encodeTilePlaneMajor(grid, ox, oy):
- # Amiga: 4 planes * 8 rows, one byte per row per plane.
+ # Amiga and X68000: 4 planes * 8 rows, one byte per row per plane.
# Plane 0 bytes 0..7, plane 1 bytes 8..15, etc.
out = bytearray()
for k in range(4):
@@ -167,6 +172,7 @@ TILE_ENCODERS = {
TARGET_ATARIST: encodeTileRowPlanesPerRow,
TARGET_DOS: encodeTileChunky,
TARGET_IIGS: encodeTileChunky,
+ TARGET_X68000: encodeTilePlaneMajor,
}