From 70f67d232de77125d941722e951569b2eaf4f90e Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 28 Sep 2026 21:03:46 -0500 Subject: [PATCH] Lots of work, but mostly serial related. --- LICENSES.md | 11 +- Makefile | 12 +- README.md | 22 +- docs/ApiSerial.adoc | 134 ++++- docs/Assets.adoc | 8 +- docs/GettingStarted.adoc | 17 +- examples/serial/serial.c | 19 +- examples/serloss/serloss.c | 408 +++++++++++++ examples/spacetaxi/stMarshalIigs.s | 354 ----------- examples/spacetaxi/stSim.c | 25 - include/joey/platform.h | 10 + include/joey/serial.h | 48 +- installer/components/amigaSerial.sh | 34 ++ installer/components/iigsFirmware.sh | 22 +- installer/files.sha1 | 1 + installer/lib/bundle.sh | 15 +- .../patches/mame/0002-z80scc-rx-fifo.patch | 21 +- installer/patches/mame/README.md | 24 +- joey-run | 83 +-- make/iigs.mk | 35 +- make/support/iigs/ntpIrqWindow.py | 119 ++++ make/x68000.mk | 96 +-- scripts/bench-x68000.sh | 29 +- scripts/check-serial.sh | 2 +- scripts/lib/joeyEnv.sh | 42 ++ scripts/lib/serialLoopPeer.py | 132 +++++ scripts/lib/x68kDisk.sh | 70 +++ scripts/profile-x68000.sh | 33 +- .../{verify-audio-all.sh => verify-all.sh} | 51 +- scripts/verify-amiga-serial.sh | 67 +++ scripts/verify-atarist-serial.sh | 59 ++ scripts/verify-dos-serial.sh | 60 ++ scripts/verify-iigs-serial-loss.sh | 360 ++++++++++++ scripts/verify-x68000-audio.sh | 35 +- scripts/verify-x68000-golden.sh | 29 +- scripts/verify-x68000-input.sh | 25 +- scripts/verify-x68000-mouse.sh | 13 +- scripts/verify-x68000-serial.sh | 69 ++- src/amiga/serial.c | 100 +++- src/atarist/audio.c | 24 +- src/atarist/serial.c | 22 + src/core/assetLoad.c | 14 +- src/core/port.h | 18 +- src/core/serial.c | 34 +- src/dos/serial.c | 63 +- src/generic/genericSerial.c | 6 + src/iigs/audio_full.c | 14 + src/iigs/hal.c | 108 ++-- src/iigs/input.c | 171 +++++- src/iigs/joeyDraw.s | 479 ++++++++++++++- src/iigs/serial.c | 549 ++++++++++-------- src/iigs/serialIrq.s | 364 ++++++++++++ src/x68000/hal.c | 30 - src/x68000/serial.c | 358 +++++++++--- src/x68000/x68kTile.h | 21 +- tests/host/serialHost.c | 56 +- toolchains/env.sh | 1 + tools/adfTool.py | 138 +++++ tools/assetbake/assetbake.py | 14 +- 59 files changed, 3953 insertions(+), 1225 deletions(-) create mode 100644 examples/serloss/serloss.c delete mode 100644 examples/spacetaxi/stMarshalIigs.s create mode 100644 installer/components/amigaSerial.sh create mode 100755 make/support/iigs/ntpIrqWindow.py create mode 100755 scripts/lib/serialLoopPeer.py create mode 100644 scripts/lib/x68kDisk.sh rename scripts/{verify-audio-all.sh => verify-all.sh} (64%) create mode 100755 scripts/verify-amiga-serial.sh create mode 100755 scripts/verify-atarist-serial.sh create mode 100755 scripts/verify-dos-serial.sh create mode 100755 scripts/verify-iigs-serial-loss.sh create mode 100644 src/iigs/serialIrq.s create mode 100755 tools/adfTool.py 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, }