Lots of work, but mostly serial related.

This commit is contained in:
Scott Duensing 2026-09-28 21:03:46 -05:00
parent 8b9590ddb2
commit 70f67d232d
59 changed files with 3953 additions and 1225 deletions

View file

@ -215,9 +215,9 @@ the source of each before shipping anything that depends on them.
### Downloaded by the installer ### Downloaded by the installer
- **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, and `apple2gsr1`, - **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, `apple2gsr1`, ROM 01,
ROM 01): Apple Inc. proprietary firmware, from the archive.org MAME 0.240 and `a2ssc`, the Super Serial Card): Apple Inc. proprietary firmware, from
split set. Component `iigsFirmware`. the archive.org MAME 0.240 split set. Component `iigsFirmware`.
- **Apple IIgs System 6.0.4** (`System.Disk`): Apple Inc. proprietary OS, from - **Apple IIgs System 6.0.4** (`System.Disk`): Apple Inc. proprietary OS, from
the Apple II archive (<https://mirrors.apple2.org.za/>). Component the Apple II archive (<https://mirrors.apple2.org.za/>). Component
`iigsFirmware`. `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 Kickstart 3.1 ROM**: Cloanto / Amiga Corp. proprietary, sold today
(Amiga Forever). Required by the Amiga test gates; `joey-run` falls back to (Amiga Forever). Required by the Amiga test gates; `joey-run` falls back to
FS-UAE's AROS without it. Component `kickstart`. 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 - **X68000 CG (font) ROM**: not part of Sharp's free release. MAME's x68000
driver needs it. Component `x68kCgrom`. driver needs it. Component `x68kCgrom`.
- **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original - **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original

View file

@ -9,6 +9,7 @@
# make x68000 builds Sharp X68000 only # make x68000 builds Sharp X68000 only
# make docs renders the manual to build/docs (HTML + PDF) # make docs renders the manual to build/docs (HTML + PDF)
# make verify-audio runs every port's audio gate (needs emulators) # 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 # make clean removes all build outputs
# #
# Cross tools come from ./install.sh, which records their paths in # Cross tools come from ./install.sh, which records their paths in
@ -43,7 +44,7 @@ ifeq ($(HAVE_X68000),1)
ALL_TARGETS += x68000 ALL_TARGETS += x68000
endif 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 all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),) ifeq ($(ALL_TARGETS),)
@ -92,7 +93,13 @@ docs:
# non-zero on purpose, so a run that tested nothing cannot look green. # non-zero on purpose, so a run that tested nothing cannot look green.
# Takes a subset: make verify-audio PORTS="atarist amiga" # Takes a subset: make verify-audio PORTS="atarist amiga"
verify-audio: 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: clean:
rm -rf $(REPO_DIR)/build rm -rf $(REPO_DIR)/build
@ -110,5 +117,6 @@ help:
@echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)" @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-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 " docs (manual to build/docs; FORMAT=html for HTML only)"
@echo "Install toolchains first: ./install.sh install toolchains" @echo "Install toolchains first: ./install.sh install toolchains"

View file

@ -65,9 +65,10 @@ the emulators and their firmware:
./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw ./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw
``` ```
Three files are never downloaded -- the Amiga Kickstart, the X68000 font ROM Four files are never downloaded -- the Amiga Kickstart, the Amiga
and Space Taxi's C64 memory image -- so supply your copies once with `serial.device` from Workbench 3.1, the X68000 font ROM and Space Taxi's C64
`./install.sh import PATH`. The manual's Getting Started chapter 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. covers `joey-run`'s options and the private-file bundle.
@ -386,13 +387,9 @@ re-indexing on the device.
Two blob formats: Two blob formats:
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target * **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
planar layout (Amiga plane-major; Atari ST row-major-with-planes- planar layout (Amiga and X68000 plane-major; Atari ST row-major-with-
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad` planes-per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
rejects files baked for the wrong target. There is no X68000 target: rejects files baked for the wrong 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.
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite * **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky 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 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. 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 tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
``` ```
@ -411,7 +408,8 @@ per target so `make <target>` produces baked blobs under
runtime tree at `build/<target>/.../DATA/`. `make/iigs.mk` runs no runtime tree at `build/<target>/.../DATA/`. `make/iigs.mk` runs no
`assetbake.py`: its `generated/iigs` tree is produced out-of-band and the `assetbake.py`: its `generated/iigs` tree is produced out-of-band and the
fragment only bakes `.spc` from the committed `.spr`. `make/x68000.mk` 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 For runtime-extracted content (sprites peeled out of a procedural
or captured surface), use `jlSpriteCreateFromSurface` -- the or captured surface), use `jlSpriteCreateFromSurface` -- the

View file

@ -19,15 +19,44 @@ the module entirely.
One byte-stream API spans every port while the HAL hides very different 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; 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`; 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 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 background ring -- a hardware interrupt on DOS, the X68000 and the IIgs, the
`jlSerialPoll` on the IIgs -- and reads never block. Call `jlSerialPoll` once OS on Amiga and ST -- and reads never block. Call `jlSerialPoll` once per
per frame: it is a no-op where the interrupt or OS already fills the ring, and frame: it is next to a no-op where the interrupt or OS already fills the ring,
the hardware drain where it does not, so calling it every frame is always and the hardware drain where nothing does, so calling it every frame is always
correct. 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 A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call
becomes a safe no-op, exactly like a failed audio init. 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 { typedef enum {
JL_SERIAL_DEFAULT = 0, // the platform's natural RS-232 port JL_SERIAL_DEFAULT = 0, // the platform's natural RS-232 port
JL_SERIAL_MODEM, // IIgs modem port (SCC channel A) JL_SERIAL_MODEM, // IIgs modem port (SCC channel B)
JL_SERIAL_PRINTER, // IIgs printer port (SCC channel B) JL_SERIAL_PRINTER, // IIgs printer port (SCC channel A)
JL_SERIAL_SLOT // card/port selected by config->unit JL_SERIAL_SLOT // card/port selected by config->unit
} jlSerialDeviceE; } jlSerialDeviceE;
@ -64,7 +93,19 @@ typedef struct {
The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM` 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 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 `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. 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); 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. *Parameters:* none. *Returns:* nothing.
@ -107,7 +178,9 @@ Closes the port and releases its resources. Safe when not open.
void jlSerialPoll(void); 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. *Parameters:* none. *Returns:* nothing.
@ -188,6 +261,47 @@ Discards any buffered received bytes.
*Parameters:* none. *Returns:* nothing. *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 === jlSerialSetRts
[source,c] [source,c]

View file

@ -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 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 native pixel format -- plane-major on Amiga and X68000, row-major-with-planes-
ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the 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. loader refuses a file baked for a different target rather than drawing garbage.
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos` or `--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
`iigs`. `iigs` or `x68000`.
==== Baking Sprites ==== Baking Sprites

View file

@ -88,7 +88,8 @@ such as FS-UAE without a Kickstart.
| `import PATH...` | `import PATH...`
| Installs the files that are not downloaded (see <<Firmware that is not | Installs the files that are not downloaded (see <<Firmware that is not
downloaded>>) 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` | `bundle FILE`
| Packs those files, as present on this machine, into `FILE` (a `.tar.gz`) | 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, 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 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 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 Amiga `serial.device` from Workbench 3.1 (Kickstart 3.1 has none in ROM, and
levels are extracted from. Supply your copies once: 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] [source,console]
---- ----
$ ./install.sh import ~/my-roms $ ./install.sh import ~/my-roms
---- ----
`import` takes files, folders and `.zip` archives, identifies each file by its `import` takes files, folders, `.zip` archives and Amiga `.adf` disk images
checksum (the list is `installer/files.sha1`) and puts it in place. (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: 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 `./install.sh bundle joeylib-private.tar.gz` packs the ones present on

View file

@ -45,9 +45,24 @@
#define COLOR_LINK_OPEN 6 #define COLOR_LINK_OPEN 6
#define COLOR_LINK_SHUT 7 #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 #define SERIAL_BAUD 9600u
#endif
#ifndef SERIAL_DATABITS
#define SERIAL_DATABITS 8u #define SERIAL_DATABITS 8u
#endif
#ifndef SERIAL_STOPBITS
#define SERIAL_STOPBITS 1u #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 #define RX_CHUNK 32
@ -184,8 +199,8 @@ int main(void) {
serialCfg.baud = SERIAL_BAUD; serialCfg.baud = SERIAL_BAUD;
serialCfg.dataBits = SERIAL_DATABITS; serialCfg.dataBits = SERIAL_DATABITS;
serialCfg.stopBits = SERIAL_STOPBITS; serialCfg.stopBits = SERIAL_STOPBITS;
serialCfg.parity = JL_SERIAL_PARITY_NONE; serialCfg.parity = SERIAL_PARITY;
serialCfg.flow = JL_SERIAL_FLOW_NONE; serialCfg.flow = SERIAL_FLOW;
serialCfg.unit = 0u; serialCfg.unit = 0u;
linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg); linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg);
if (linkOpen) { if (linkOpen) {

408
examples/serloss/serloss.c Normal file
View file

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

View file

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

View file

@ -1489,31 +1489,7 @@ void stSimMarkChar(StSimT *sim, uint8_t ch) {
// $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC // $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC
// shows next. The title and level-intro loops flush the same way. // 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) { 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 i;
uint8_t enable = 0u; uint8_t enable = 0u;
@ -1528,7 +1504,6 @@ void stSimMarshal(StSimT *sim) {
} }
sim->frame.enableMask = enable; sim->frame.enableMask = enable;
sim->frame.multiMask = sim->multiColorMask; sim->frame.multiMask = sim->multiColorMask;
#endif
} }

View file

@ -170,6 +170,8 @@
#define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH #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). // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create // 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_WRITE
#define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH #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) // save files + disk space (CreateDir/DeleteFile/Info via dos.library)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
@ -313,6 +317,8 @@
#define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH #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) // save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
@ -361,6 +367,8 @@
#define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH #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). // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free) // save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
@ -427,6 +435,8 @@
#define JL_HAS_SERIAL_READ #define JL_HAS_SERIAL_READ
#define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_FLUSH #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 // Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what
// the portable JYM1 tracker rides, with no CPU mixing. // the portable JYM1 tracker rides, with no CPU mixing.
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT

View file

@ -7,15 +7,18 @@
// One byte-stream API spans every port; the per-port HAL hides the very // One byte-stream API spans every port; the per-port HAL hides the very
// different hardware: // different hardware:
// Apple IIgs -- direct Zilog 8530 SCC (modem/printer ports) or a 6551 // Apple IIgs -- direct Zilog 8530 SCC (modem/printer ports) or a 6551
// Super Serial Card in a slot; polled, drained on // Super Serial Card in a slot; its own RX interrupt fills
// jlSerialPoll(). // the ring (a card needs its interrupt switch, SW2-6, on).
// DOS -- 8250/16550 UART on a COM port; interrupt-driven RX ring. // DOS -- 8250/16550 UART on a COM port; interrupt-driven RX ring.
// Amiga -- serial.device (async exec IO); the OS buffers RX. // Amiga -- serial.device (async exec IO); the OS buffers RX.
// Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS 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 // 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 // ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST);
// IIgs); jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll() // jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll()
// once per game frame -- it is a no-op where the IRQ/OS already fills the // 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 // ring, and the hardware drain where it does not, so calling it every frame is
// always correct. Transmit is a bounded poll. // always correct. Transmit is a bounded poll.
@ -30,8 +33,8 @@
#include "types.h" #include "types.h"
// Which physical interface to open. The IIgs is the oddball with three: // 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_MODEM -- IIgs modem port (SCC channel B). Elsewhere == DEFAULT.
// JL_SERIAL_PRINTER -- IIgs printer port (SCC channel B). Only the IIgs has // JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has
// a second built-in port; elsewhere == DEFAULT. // a second built-in port; elsewhere == DEFAULT.
// JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs // JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs
// Super Serial Card slot (1..7); DOS COM number (1..4); // Super Serial Card slot (1..7); DOS COM number (1..4);
@ -75,12 +78,23 @@ typedef struct {
bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
// Close the open port and release its resources. Safe to call when not open. // 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); void jlSerialClose(void);
// Pump the RX/TX buffers. Call once per game frame. No-op where a hardware IRQ // Pump the RX/TX buffers. Call once per game frame. Next to a no-op where a
// or the OS already services the port (DOS/Amiga/ST); drains the hardware FIFO // hardware IRQ or the OS already services the port; drains the hardware into
// into the RX ring where it does not (IIgs). Calling it every frame is always // the RX ring where nothing else does (an IIgs Super Serial Card whose
// correct. // interrupt switch is off). Calling it every frame is always correct.
void jlSerialPoll(void); void jlSerialPoll(void);
// Bytes waiting in the RX buffer, readable now without blocking. // Bytes waiting in the RX buffer, readable now without blocking.
@ -101,6 +115,20 @@ int16_t jlSerialReadByte(void);
// Discard any buffered received bytes. // Discard any buffered received bytes.
void jlSerialFlush(void); 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. // 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 // JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying

View file

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

View file

@ -1,12 +1,13 @@
# Apple IIgs firmware and system software for MAME: # Apple IIgs firmware and system software for MAME:
# - the MAME apple2gs (ROM 03) and apple2gsr1 (ROM 01) ROM sets, from the # - the MAME apple2gs (ROM 03) and apple2gsr1 (ROM 01) ROM sets, and a2ssc
# archive.org MAME 0.240 split set (the source llvm816 also uses) # (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 # - the CFFA2 card firmware, freely redistributed by dreher.net, needed
# to boot images larger than an 800 KB floppy # to boot images larger than an 800 KB floppy
# - the GS/OS 6.0.4 System.Disk, from the Apple II archive # - the GS/OS 6.0.4 System.Disk, from the Apple II archive
# Everything lands in toolchains/firmware/iigs. # 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" compGroups="emulator:iigs"
compRequires="host" compRequires="host"
compHostDeps="unzip" compHostDeps="unzip"
@ -19,6 +20,7 @@ fwDir="$JOEY_FIRMWARE/iigs"
romSets=( romSets=(
"apple2gs.zip eddce4a9f9f6e73ffed06c5aede6fbcdaa01dda5568b81f90d865858a3d7d9bc" "apple2gs.zip eddce4a9f9f6e73ffed06c5aede6fbcdaa01dda5568b81f90d865858a3d7d9bc"
"apple2gsr1.zip 1c16fec7b9e785c4118909e409e61dcc38d0921f774b8708bae176bc5eb27307" "apple2gsr1.zip 1c16fec7b9e785c4118909e409e61dcc38d0921f774b8708bae176bc5eb27307"
"a2ssc.zip 2e46808466d578006d9e12f86af8e831aa4dffb554149e184c044a1d14ed8a20"
) )
cffaSha256="48242b9e2e87d87bfef1f1b1d64d1091bbfca182d745c1d031690fac0f80c0fe" cffaSha256="48242b9e2e87d87bfef1f1b1d64d1091bbfca182d745c1d031690fac0f80c0fe"
gsosSha256="e42a4f1f50c1aae0cc17dc555415a2fbf21dc0b7b6b3e33e0d73bf22ef97506a" gsosSha256="e42a4f1f50c1aae0cc17dc555415a2fbf21dc0b7b6b3e33e0d73bf22ef97506a"
@ -44,12 +46,22 @@ compInstall() {
compInstalled() { compInstalled() {
[ -f "$fwDir/roms/apple2gs.zip" ] && [ -f "$fwDir/roms/apple2gsr1.zip" ] && 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" ] [ -f "$fwDir/roms/a2cffa2/cffa20eec02.bin" ] && [ -f "$fwDir/gsos-system.po" ]
} }
compVerify() { 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/roms/a2cffa2/cffa20eec02.bin" | cut -d' ' -f1)" = "080ff88f19de22328e162954ee2b51ee65f9d5cd" ] &&
[ "$(sha1sum "$fwDir/gsos-system.po" | cut -d' ' -f1)" = "a57940a0b724fc7dc545236a4f9d4c6dd68f3e81" ] [ "$(sha1sum "$fwDir/gsos-system.po" | cut -d' ' -f1)" = "a57940a0b724fc7dc545236a4f9d4c6dd68f3e81" ]
} }

View file

@ -5,5 +5,6 @@
# #
# sha1 destination (repository-relative) what it is # sha1 destination (repository-relative) what it is
3b7f1493b27e212830f989f26ca76c02049f09ca toolchains/firmware/amiga/kickstart.rom Amiga Kickstart 3.1 r40.63 (A500/A600/A2000) 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 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) 5bfa4ba70431135575dab610a73dde02b8095953 stuff/spacetaxi/raw.bin Space Taxi C64 memory image (level source)

View file

@ -43,8 +43,10 @@ importOneFile() {
} }
# importPaths PATH... -- scan files, directories and .zip archives and # importPaths PATH... -- scan files, directories, .zip archives and Amiga
# install every private file found. Prints what was installed. # .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() { importPaths() {
local path local path
local file local file
@ -63,6 +65,15 @@ importPaths() {
done done
fi 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" importOneFile "$file"
;; ;;

View file

@ -1,24 +1,37 @@
--- a/src/devices/machine/z80scc.cpp --- a/src/devices/machine/z80scc.cpp
+++ b/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_uart->m_wr0_ptrbits = 0;
- m_rx_fifo_sz = (m_uart->m_variant & z80scc_device::SET_ESCC) ? 8 : 3; - 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; m_tx_fifo_sz = (m_uart->m_variant & z80scc_device::SET_ESCC) ? 4 : 1;
--- a/src/devices/machine/z80scc.h --- a/src/devices/machine/z80scc.h
+++ b/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; unsigned int m_delayed_tx_brg_change;
// receiver state // receiver state
- uint8_t m_rx_data_fifo[8]; // receive data FIFO - uint8_t m_rx_data_fifo[8]; // receive data FIFO
- uint8_t m_rx_error_fifo[8]; // receive error 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 + // 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_data_fifo[512]; // receive data FIFO
+ uint8_t m_rx_error_fifo[512]; // receive error FIFO + uint8_t m_rx_error_fifo[512]; // receive error FIFO
uint8_t m_rx_error; // current receive error uint8_t m_rx_error; // current receive error

View file

@ -7,10 +7,32 @@ drivers. The gates depend on every one of them.
| Patch | What it fixes | Needed by | | 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` | | 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 | | 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 | | 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 `<channel tag>: 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 ## Why the X68000 changes are one patch
They were first kept as three separate patches, but the upstream mouse They were first kept as three separate patches, but the upstream mouse

View file

@ -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 DIR -- the one program file inside an app folder.
folderProgram() { folderProgram() {
local file local file
@ -350,6 +303,15 @@ runAmiga() {
stageFolder "$hd" stageFolder "$hd"
mkdir -p "$hd/s" mkdir -p "$hd/s"
printf ':%s\n' "$name" > "$hd/s/startup-sequence" 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 args=(--amiga_model=A500 --fast_memory=2048
--hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB --hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB
--automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1 --automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1
@ -428,28 +390,18 @@ runDos() {
runX68000() { runX68000() {
local name local name
local xdf="$repo/tools/xdftool.py"
local stage="$work/files" local stage="$work/files"
local f
local rel
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
# shellcheck source=scripts/lib/x68kDisk.sh
source "$repo/scripts/lib/x68kDisk.sh"
name="$(basename "$program")" name="$(basename "$program")"
# Human68k boots from a trimmed copy of the system disk whose # Human68k boots from a trimmed copy of the system disk whose
# AUTOEXEC.BAT runs the program from the data disk in drive B. # AUTOEXEC.BAT runs the program from the data disk in drive B.
printf 'B:\r\nB:\\%s\r\n\x1a' "$name" > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$name" || fail "cannot build the boot disk"
cp "$X68K_DIR/HUMAN302.XDF" "$work/boot.xdf" x68kDataDisk "$work/data.xdf" || fail "cannot build the data disk"
cp "$X68K_DIR/HUMAN302.XDF" "$work/data.xdf" x68kXdf add "$work/data.xdf" "$program" "$name" >/dev/null || fail "cannot copy $name onto the data disk"
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
rm -rf "$stage" rm -rf "$stage"
mkdir -p "$stage" mkdir -p "$stage"
if [ -d "$(dirname "$program")/DATA" ]; then if [ -d "$(dirname "$program")/DATA" ]; then
@ -460,10 +412,7 @@ runX68000() {
cp -r "$dataDir/." "$stage/DATA/" cp -r "$dataDir/." "$stage/DATA/"
fi fi
if [ -d "$stage/DATA" ]; then if [ -d "$stage/DATA" ]; then
while IFS= read -r f; do x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk"
rel="${f#"$stage"/}"
python3 "$xdf" add "$work/data.xdf" "$f" "${rel^^}" >/dev/null
done < <(find "$stage/DATA" -type f | sort)
fi fi
echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop." 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" 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")" program="$(folderProgram "$program")"
elif [ ! -e "$program" ]; then elif [ ! -e "$program" ]; then
[ -n "$port" ] || fail "$program is not a file; for an example name add --port" [ -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)" [ -e "$program" ] || fail "$program is not built (make $port)"
fi fi
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")" program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"

View file

@ -62,6 +62,7 @@ KEYS_SRC := $(EXAMPLES)/keys/keys.c
SERIAL_SRC := $(EXAMPLES)/serial/serial.c SERIAL_SRC := $(EXAMPLES)/serial/serial.c
SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c
SERLOSS_SRC := $(EXAMPLES)/serloss/serloss.c
SAVE_SRC := $(EXAMPLES)/save/save.c SAVE_SRC := $(EXAMPLES)/save/save.c
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
@ -69,17 +70,19 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c
ADV_SRC := $(EXAMPLES)/adventure/adventure.c ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.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 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 AUDIO_SRC := $(EXAMPLES)/audio/audio.c
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
# binary that ships next to the program; jlpAudioInit loads "ntpplayer" from # 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_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s
NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py
NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_BIN := $(BUILD)/audio/ntpplayer.bin
IIGS_MERLIN := $(IIGS_AS) 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 # Default: build (compile-check) the library. The MAME smoke test is
# `iigs-clang-smoke`; disks and examples are `iigs-disk`. # `iigs-clang-smoke`; disks and examples are `iigs-disk`.
@ -222,6 +225,19 @@ iigs-verify: iigs-disk
iigs-verify-serial: $(BINDIR)/SCCPROBE iigs-verify-serial: $(BINDIR)/SCCPROBE
$(REPO_DIR)/scripts/verify-iigs-serial.sh $(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). # Boot-verify every launchable example on joey.2mg (per-example thresholds).
iigs-verify-all: iigs-disk iigs-verify-all: iigs-disk
$(REPO_DIR)/scripts/verify-iigs-all.sh $(REPO_DIR)/scripts/verify-iigs-all.sh
@ -259,9 +275,9 @@ iigs-verify-shrtail: $(LIB) $(NTP_BIN)
iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW
$(REPO_DIR)/scripts/verify-iigs-save.sh $(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 @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 cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@ mv $(BUILD)/audio/ntpplayer $@
@ -331,6 +347,15 @@ $(BINDIR)/SCCPROBE: $(SCCPROBE_SRC) $(LIB) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SCCPROBE.d $(INCLUDES) -o $@ $(SCCPROBE_SRC) $(LIB) $(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) $(BINDIR)/SAVE: $(SAVE_SRC) $(LIB) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB)

119
make/support/iigs/ntpIrqWindow.py Executable file
View file

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

View file

@ -1,8 +1,8 @@
# Sharp X68000 (elf2x68k / m68k-xelf-gcc) build rules. # Sharp X68000 (elf2x68k / m68k-xelf-gcc) build rules.
# #
# BRING-UP STATE. This builds libjoey.a and the serial example against the # The port draws planar into the text plane (src/x68000/halPlanar.c), shares
# generic backend -- no native primitives, no audio, no sprite codegen yet. # the Amiga's 68k planar fills, plots and sprite emitter (src/m68k), plays
# See claudeDocs/x68000_port.md for what is and is not real. # chip music on the OPM and samples through the ADPCM channel.
# #
# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), installed # 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 # 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 LDFLAGS := -lm
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c) PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
# Port-local 68000 asm, if any lands. Empty today: SCB per-band palettes are # Raw interrupt shims (timer tick, SCB raster, SCC receive): each saves every
# done in the pixel expansion (DOS-style), not with a raster ISR, so the port is # register and returns with RTE, which a C function cannot.
# pure C again.
PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s) PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
# No src/m68k sources yet: those are the planar sprite emitters and the planar # Sprite codegen is the shared 68k PLANAR emitter, unchanged. Its
# 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
# AMIGA_PLANE_STRIDE is SURFACE_WIDTH/8 = 40, which is this port's surface # 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 # stride too -- the display's 128-byte stride never reaches the emitter, which
# only ever addresses the surface. So sprites COMPILE here rather than # only ever addresses the surface. So sprites COMPILE here rather than
@ -94,11 +83,41 @@ $(LIBXMP_AR): $(LIBXMP_OBJS)
LIB := $(LIBDIR)/libjoey.a 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 PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
SERIAL_SRC := $(EXAMPLES)/serial/serial.c DRAW_SRC := $(EXAMPLES)/draw/draw.c
UBER_SRC := $(EXAMPLES)/uber/uber.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
KEYS_SRC := $(EXAMPLES)/keys/keys.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 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 .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-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 $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -143,38 +162,19 @@ $(LIB): $(LIB_OBJS)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(X68K_AR) rcs $@ $(LIB_OBJS) $(X68K_AR) rcs $@ $(LIB_OBJS)
# PATTERN is the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient .SECONDEXPANSION:
# palettes, so a mis-placed band boundary is immediately visible. $(EXAMPLE_BINS): $(BINDIR)/%.X: $$($$*_SRC) $(LIB) $(LIBXMP_AR)
$(BINDIR)/PATTERN.X: $(PATTERN_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @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 $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ cp $< $@
# 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 $@
# Space Taxi asset variables (the rules follow the binary below); make # Space Taxi asset variables (the rules follow the binary below); make
# expands prerequisites when it reads the rule, so these come first. # expands prerequisites when it reads the rule, so these come first.
STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000 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_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat $(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) 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 # 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 # own. The DATA tree next to it (levels + the precompiled cel bank) is
# what joey-run copies onto the data floppy. # 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 $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@

View file

@ -30,7 +30,7 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
TEMPLATE="$X68K_DIR/HUMAN302.XDF" source "$repo/scripts/lib/x68kDisk.sh"
FRAMES="${X68K_BENCH_FRAMES:-120000}" FRAMES="${X68K_BENCH_FRAMES:-120000}"
WALL="${X68K_BENCH_WALL:-7200}" WALL="${X68K_BENCH_WALL:-7200}"
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in # 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 # 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 # 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 # launching so the log lands there too.
# disk also means a run can never corrupt the thing that boots it. x68kBootDisk "$work/boot.xdf" 'B:' 'B:\UBER.X' || exit 1
printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" x68kDataDisk "$work/data.xdf" || exit 1
cp "$TEMPLATE" "$work/boot.xdf" x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# 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
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is # Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM # jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
@ -171,7 +157,8 @@ emu.register_frame_done(function()
end) end)
LUA 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" \ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \
-autoboot_script "$work/bench.lua" </dev/null 2>&1 | grep '^BENCH') -autoboot_script "$work/bench.lua" </dev/null 2>&1 | grep '^BENCH')
@ -189,7 +176,7 @@ if ! echo "$runLog" | grep -q 'done-screen'; then
exit 1 exit 1
fi 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 echo "bench-x68000: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
exit 1 exit 1
} }

View file

@ -18,7 +18,7 @@ cd "$repo"
"$cc" -DJOEYLIB_PLATFORM_BLANK \ "$cc" -DJOEYLIB_PLATFORM_BLANK \
-DJL_HAS_SERIAL_OPEN -DJL_HAS_SERIAL_CLOSE -DJL_HAS_SERIAL_POLL \ -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_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 \ -Iinclude -Iinclude/joey -Isrc/core -Wall -Wextra \
src/core/serial.c tests/host/serialHost.c \ src/core/serial.c tests/host/serialHost.c \
-o "$work/serialHost" -o "$work/serialHost"

View file

@ -38,6 +38,48 @@ joeyNeedFile() {
JOEY_DOSBOX_ISOLATE=(--noprimaryconf --nolocalconf) 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. # joeyFindWindow DISPLAY PID -- print the X window owned by process PID.
# More robust than matching titles, which differ between emulator builds. # More robust than matching titles, which differ between emulator builds.
joeyFindWindow() { joeyFindWindow() {

132
scripts/lib/serialLoopPeer.py Executable file
View file

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

70
scripts/lib/x68kDisk.sh Normal file
View file

@ -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/<path>, 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" "$@"
}

View file

@ -37,10 +37,9 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom 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 elf=$bin.elf
x68=$X68K_DIR
template=$x68/HUMAN302.XDF
joeyNeed X68K_ROOT toolchain:x68000 joeyNeed X68K_ROOT toolchain:x68000
export PATH="$X68K_ROOT/bin:$PATH" 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) work=$(mktemp -d -t joey-x68prof.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1
cp "$template" "$work/boot.xdf" x68kDataDisk "$work/data.xdf" || exit 1
cp "$template" "$work/data.xdf" x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null # The program's DATA tree sits next to it: its own folder's for Space Taxi,
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do # the shared bin/DATA for the rest.
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true if [[ -d $(dirname "$bin")/DATA ]]; then
done x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1
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)
fi fi
cat > "$work/prof.lua" <<LUA cat > "$work/prof.lua" <<LUA
@ -223,7 +210,7 @@ end)
LUA LUA
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 3600 "$MAME" x68000 \ ( cd "$work" && SDL_AUDIODRIVER=dummy timeout 3600 "$MAME" x68000 \
-bios ipl10 -rompath "$x68/roms" \ -bios ipl10 -rompath "$X68K_DIR/roms" \
-flop1 boot.xdf -flop2 data.xdf \ -flop1 boot.xdf -flop2 data.xdf \
-video none -sound none -nothrottle -skip_gameinfo \ -video none -sound none -nothrottle -skip_gameinfo \
-autoboot_script prof.lua </dev/null >mame.log 2>&1 ) || true -autoboot_script prof.lua </dev/null >mame.log 2>&1 ) || true

View file

@ -1,15 +1,25 @@
#!/usr/bin/env bash #!/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) # iigs DOC wavetable + sound IRQ vector (register tap)
# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) # x68000 YM2151 noise + PPI port C + ADPCM rate (register tap)
# atarist YM2149 write trace, with the pc (register tap) # atarist YM2149 write trace, with the pc (register tap)
# amiga recorded sound, four loudness phases (Paula has no tap) # amiga recorded sound, four loudness phases (Paula has no tap)
# dos recorded sound, four loudness phases (SB 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 # 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 # 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 # 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 # roll-up. Gate stdout/stderr is echoed as it happens, so a failure shows
# its diagnosis here and not just a verdict. # its diagnosis here and not just a verdict.
# #
# Usage: scripts/verify-audio-all.sh [port ...] # Usage: scripts/verify-all.sh audio|serial [port ...]
# with no arguments, all five. Otherwise any of: # with no ports, all of that kind's. Otherwise any of:
# iigs x68000 atarist amiga dos # iigs x68000 atarist amiga dos (and host, for serial)
# Env: AUDIO_ALL_LOG also append the full transcript to this file # Env: VERIFY_ALL_LOG also append the full transcript to this file
# Requires: whatever each selected gate requires; missing ones SKIP. # Requires: whatever each selected gate requires; missing ones SKIP.
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 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} ports=${*:-$ALL_PORTS}
for p in $ports; do for p in $ports; do
@ -48,12 +64,11 @@ for p in $ports; do
done done
gateFor() { # $1 = port -> script + args gateFor() { # $1 = port -> script + args
case $1 in case $kind:$1 in
iigs) echo "scripts/verify-iigs-audio.sh staxi" ;; audio:iigs) echo "scripts/verify-iigs-audio.sh staxi" ;;
x68000) echo "scripts/verify-x68000-audio.sh" ;; audio:*) echo "scripts/verify-$1-audio.sh" ;;
atarist) echo "scripts/verify-atarist-audio.sh" ;; serial:host) echo "scripts/check-serial.sh" ;;
amiga) echo "scripts/verify-amiga-audio.sh" ;; serial:*) echo "scripts/verify-$1-serial.sh" ;;
dos) echo "scripts/verify-dos-audio.sh" ;;
esac esac
} }
@ -67,8 +82,8 @@ for port in $ports; do
echo echo
echo "=== $label: $port ($gate) ===" echo "=== $label: $port ($gate) ==="
start=$SECONDS start=$SECONDS
if [ -n "${AUDIO_ALL_LOG:-}" ]; then if [ -n "${VERIFY_ALL_LOG:-}" ]; then
( cd "$repo" && $gate ) 2>&1 | tee -a "$AUDIO_ALL_LOG" ( cd "$repo" && $gate ) 2>&1 | tee -a "$VERIFY_ALL_LOG"
rc=${PIPESTATUS[0]} rc=${PIPESTATUS[0]}
else else
( cd "$repo" && $gate ) ( cd "$repo" && $gate )
@ -106,5 +121,5 @@ if [ -n "$skipped" ]; then
echo "$label: INCOMPLETE - skipped$skipped (build those ports, then re-run)" >&2 echo "$label: INCOMPLETE - skipped$skipped (build those ports, then re-run)" >&2
exit 3 exit 3
fi fi
echo "$label: PASS (all $(count "$passed") audio gates)" echo "$label: PASS (all $(count "$passed") $kind gates)"
exit 0 exit 0

67
scripts/verify-amiga-serial.sh Executable file
View file

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

View file

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

60
scripts/verify-dos-serial.sh Executable file
View file

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

View file

@ -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" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local launched = false
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function u16(off)
return mem:read_u8(0xE19DD0 + off) | (mem:read_u8(0xE19DD1 + off) << 8)
end
-- The stick spread bytes are biased by 128 (serloss.c MB_JOY_BIAS).
local function s8(off)
return mem:read_u8(0xE19DD0 + off) - 128
end
local function report()
io.write(string.format("VERIFY-IIGS-LOSS frame=%d sig=%02X%02X phase=%02X received=%d inSequence=%d breaks=%d presents=%d ticks=%d overruns=%d dropped=%d sum=%04X now=%d fillTicks=%d fillPresentTicks=%d joyPolls=%d joyX=%d..%d joyY=%d..%d sfxShots=%d\n",
frame, mem:read_u8(0xE19DD0), mem:read_u8(0xE19DD1), mem:read_u8(0xE19DD2),
u16(4), u16(6), u16(8), u16(10), u16(12), u16(14), u16(16), u16(18), u16(20), u16(22), u16(24),
u16(26), s8(28), s8(29), s8(30), s8(31), u16(32)))
io.flush()
end
-- With the paddles in play, serve VERTCNT/HORIZCNT (C02E/C02F) with the
-- real machine's 262-line mapping (VERTCNT*2 + HORIZCNT bit 7 = 250..511).
-- MAME's apple2gs get_vpos() wraps with -261 instead of -262, so its counter
-- repeats line 501 and never reads 250; the tight joystick read times its
-- passes with these counters, and a span across the repeat or the wrap is 65
-- ticks (~6 counts) off. The beam position comes from time_until_pos: MAME's
-- Lua has no screen vpos/hpos.
local vcTaps = {}
local function beamCountersFix()
if "$paddle" == "" then return end
local scr = manager.machine.screens[":screen"]
local function beam()
local e = scr.frame_period - scr:time_until_pos(0, 0)
if e >= 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" </dev/null 2>&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

View file

@ -41,13 +41,12 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
bin=$repo/build/x68000/bin/$PROG.X source "$repo/scripts/lib/x68kDisk.sh"
x68=$X68K_DIR bin=$(joeyExampleProgram "$prog" x68000) || exit 2
template=$x68/HUMAN302.XDF
source=$repo/src/x68000/audio.c source=$repo/src/x68000/audio.c
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; } [[ -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. # The noise table, straight out of the port so the two cannot drift apart.
table=$(python3 - "$source" <<'PY' table=$(python3 - "$source" <<'PY'
@ -63,25 +62,13 @@ PY
work=$(mktemp -d -t joey-x68audio.XXXXXX) work=$(mktemp -d -t joey-x68audio.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1
cp "$template" "$work/boot.xdf" x68kDataDisk "$work/data.xdf" || exit 1
cp "$template" "$work/data.xdf" x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null # The program's DATA tree sits next to it: its own folder's for Space Taxi,
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do # the shared bin/DATA for the rest.
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true if [[ -d $(dirname "$bin")/DATA ]]; then
done x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1
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)
fi fi
# The taps keep to arithmetic and file writes: a tap that reads memory # The taps keep to arithmetic and file writes: a tap that reads memory
@ -150,7 +137,7 @@ end)
LUA LUA
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 1800 "$MAME" x68000 \ ( 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 \ -flop1 boot.xdf -flop2 data.xdf \
-video none -sound none -nothrottle -skip_gameinfo \ -video none -sound none -nothrottle -skip_gameinfo \
-autoboot_script tap.lua </dev/null >mame.log 2>&1 ) || true -autoboot_script tap.lua </dev/null >mame.log 2>&1 ) || true

View file

@ -23,7 +23,7 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom 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}" GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
FRAMES="${X68K_GOLDEN_FRAMES:-60000}" FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
WALL="${X68K_GOLDEN_WALL:-7200}" 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 # 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 # 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 # launching so the log lands there too.
# disk also means a run can never corrupt the thing that boots it. x68kBootDisk "$work/gold.xdf" 'B:' 'B:\UBER.X' || exit 1
printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" x68kDataDisk "$work/data.xdf" || exit 1
cp "$TEMPLATE" "$work/gold.xdf" x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# 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
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is # Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM # jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
@ -165,7 +151,8 @@ emu.register_frame_done(function()
end) end)
LUA 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" \ -rompath "$X68K_DIR/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \
-autoboot_script "$work/gold.lua" </dev/null 2>&1 | grep '^GOLD') -autoboot_script "$work/gold.lua" </dev/null 2>&1 | grep '^GOLD')
@ -183,7 +170,7 @@ if ! echo "$runLog" | grep -q 'done-screen'; then
exit 1 exit 1
fi 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 echo "verify-x68000-golden: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
exit 1 exit 1
} }

View file

@ -1,15 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# verify-x68000-input.sh - Headless typed-character acceptance gate for # verify-x68000-input.sh - Headless typed-character acceptance gate for
# the X68000 port. Boots Human68k under the patched MAME with KEYS.X in # 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 # AUTOEXEC.BAT, waits for the example's A5A5 sentinel to appear on the
# GVRAM, types the RetroNet definition-of-done string on the emulated # screen, types the RetroNet definition-of-done string on the emulated
# keyboard, and reads the verification scanline back out of GVRAM. # keyboard, and reads the verification scanline back out of text VRAM.
# KEYS encodes every character it received from jlInputGetChar as nibble # KEYS encodes every character it received from jlInputGetChar as nibble
# pixel pairs, so the row must equal the typed ASCII exactly. # pixel pairs, so the row must equal the typed ASCII exactly.
# #
# GVRAM geometry (crtmod 13, 256-colour graphics plane): 16-bit word # Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128
# per pixel at $C00000, 512 words per row; the 320x200 stage is centred # bytes per row, leftmost pixel in bit 7; the 320x200 stage is centred at
# at origin (96, 156). Verification row = stage y 198 -> GVRAM y 354. # origin (96, 156). Verification row = stage y 198 -> screen y 354.
# #
# bash scripts/verify-x68000-input.sh # bash scripts/verify-x68000-input.sh
set -uo pipefail set -uo pipefail
@ -19,24 +19,19 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
TEMPLATE="$X68K_DIR/HUMAN302.XDF" source "$repo/scripts/lib/x68kDisk.sh"
WALL="${X68K_INPUT_WALL:-900}" WALL="${X68K_INPUT_WALL:-900}"
target="192.168.1.10:6510" 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; } [ -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) work=$(mktemp -d -t joey-x68input.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
printf 'A:\\KEYS.X\r\n\x1a' > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/boot.xdf" 'A:\KEYS.X' || exit 1
cp "$TEMPLATE" "$work/boot.xdf" x68kXdf add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1
done
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
cat > "$work/input.lua" <<LUA cat > "$work/input.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"] local cpu = manager.machine.devices[":maincpu"]

View file

@ -35,22 +35,17 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
TEMPLATE="$X68K_DIR/HUMAN302.XDF" source "$repo/scripts/lib/x68kDisk.sh"
WALL="${X68K_MOUSE_WALL:-900}" WALL="${X68K_MOUSE_WALL:-900}"
[ -f "$TEMPLATE" ] || { echo "verify-x68000-mouse: missing $TEMPLATE" >&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; } [ -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) work=$(mktemp -d -t joey-x68mouse.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
printf 'A:\\KEYS.X\r\n\x1a' > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/boot.xdf" 'A:\KEYS.X' || exit 1
cp "$TEMPLATE" "$work/boot.xdf" x68kXdf add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1
done
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
cat > "$work/mouse.lua" <<'LUA' cat > "$work/mouse.lua" <<'LUA'
local cpu = manager.machine.devices[":maincpu"] local cpu = manager.machine.devices[":maincpu"]

View file

@ -16,26 +16,39 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
TEMPLATE="$X68K_DIR/HUMAN302.XDF" source "$repo/scripts/lib/x68kDisk.sh"
ROMS="$X68K_DIR/roms" ROMS="$X68K_DIR/roms"
PORT="${X68K_SERIAL_PORT:-6800}" 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}" PROBE="${X68K_SERIAL_PROBE:-JOEYLIB-X68K}"
MAXWAIT="${X68K_SERIAL_TIMEOUT:-420}" 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; } [ -e "$f" ] || { echo "verify-x68000-serial: missing $f" >&2; exit 2; }
done done
work=$(mktemp -d -t joey-x68ser.XXXXXX) 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. # Autorun the program. No ECHO OFF so a load failure is visible in a screenshot.
printf 'A:\\SERIAL.X\r\n\x1a' > "$work/AUTOEXEC.BAT" x68kBootDisk "$work/serial.xdf" "A:\\$PROG.X" || exit 1
cp "$TEMPLATE" "$work/serial.xdf" x68kXdf add "$work/serial.xdf" "$repo/build/x68000/bin/$PROG.X" "$PROG.X" >/dev/null || exit 1
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
# Host end of the link. MAME's null_modem socket form CONNECTS, so we listen. # Host end of the link. MAME's null_modem socket form CONNECTS, so we listen.
python3 "$repo/scripts/x68kSerialPeer.py" \ python3 "$repo/scripts/x68kSerialPeer.py" \
@ -44,13 +57,45 @@ python3 "$repo/scripts/x68kSerialPeer.py" \
peer=$! peer=$!
sleep 2 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('<?xml version="1.0"?>\n<mameconfig version="10">\n <system name="x68000">\n <input>')
for name, default, value in ports:
print(f' <port tag="{tag}{name}" type="CONFIG" mask="255" defvalue="{default}" value="{value}" />')
print(' </input>\n </system>\n</mameconfig>')
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" \ -bios ipl10 -rompath "$ROMS" \
-flop1 "$work/serial.xdf" \ -flop1 "$work/serial.xdf" \
-rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \ -rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \
-video none -sound none -nothrottle </dev/null >/dev/null 2>&1 -video none -sound none -nothrottle </dev/null >/dev/null 2>&1 &
mame=$!
wait "$peer" wait "$peer"
rc=$? rc=$?
peer=""
joeyStop "$mame"
mame=""
cat "$work/peer.out" cat "$work/peer.out"
exit $rc exit $rc

View file

@ -19,6 +19,7 @@
#include <exec/io.h> #include <exec/io.h>
#include <exec/memory.h> #include <exec/memory.h>
#include <devices/serial.h> #include <devices/serial.h>
#include <hardware/cia.h>
#include <proto/exec.h> #include <proto/exec.h>
@ -29,6 +30,11 @@
// Largest single async transmit; a longer jlSerialWrite is split across frames. // Largest single async transmit; a longer jlSerialWrite is split across frames.
#define TX_STAGE_SIZE 1024 #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 ----- // ----- Module state -----
@ -38,16 +44,51 @@ static struct IOExtSer *gWriteReq = NULL;
static bool gOpen = false; static bool gOpen = false;
static bool gWritePending = false; static bool gWritePending = false;
static UBYTE gTxStage[TX_STAGE_SIZE]; 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 ----- // ----- Prototypes -----
static bool serialDtrIsOn(void);
static void serialDtrRestore(void);
static void serialDtrSet(bool on);
static void serialReapWrite(void); static void serialReapWrite(void);
static void serialTeardown(void); static void serialTeardown(bool keepDtr);
// ----- Helpers ----- // ----- 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. // Collect a completed async write so the request + staging buffer free up.
static void serialReapWrite(void) { static void serialReapWrite(void) {
if (gWritePending && CheckIO((struct IORequest *)gWriteReq) != NULL) { 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) { if (gWritePending) {
AbortIO((struct IORequest *)gWriteReq); AbortIO((struct IORequest *)gWriteReq);
WaitIO((struct IORequest *)gWriteReq); WaitIO((struct IORequest *)gWriteReq);
gWritePending = false; gWritePending = false;
} }
if (gReadReq != NULL && gReadReq->IOSer.io_Device != NULL) { if (gReadReq != NULL && gReadReq->IOSer.io_Device != NULL) {
Forbid();
dtrWasOn = serialDtrIsOn();
CloseDevice((struct IORequest *)gReadReq); // shared open; closes for both 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) { if (gWriteReq != NULL) {
DeleteIORequest((struct IORequest *)gWriteReq); DeleteIORequest((struct IORequest *)gWriteReq);
gWriteReq = NULL; gWriteReq = NULL;
@ -93,8 +163,9 @@ uint16_t jlpSerialAvailable(void) {
} }
// Leaves DTR as it was: see serialTeardown.
void jlpSerialClose(void) { void jlpSerialClose(void) {
serialTeardown(); serialTeardown(true);
gOpen = false; gOpen = false;
} }
@ -133,7 +204,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gReadReq == NULL) { if (gReadReq == NULL) {
gAmigaSerialFailStage = 2u; gAmigaSerialFailStage = 2u;
serialTeardown(); serialTeardown(false);
return 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. // 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; 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); err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul);
if (err != 0) { if (err != 0) {
// Exclusive access is refused if ANYONE holds the unit; fall back to a shared open, which is // 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. // verdict and io_Device is non-NULL if the device actually attached.
gAmigaSerialFailIoErr = gReadReq->IOSer.io_Error; gAmigaSerialFailIoErr = gReadReq->IOSer.io_Error;
gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u; gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u;
serialTeardown(); serialTeardown(false);
return false; return false;
} }
gReadReq->io_Baud = config->baud; gReadReq->io_Baud = config->baud;
@ -190,7 +266,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
if (DoIO((struct IORequest *)gReadReq) != 0) { if (DoIO((struct IORequest *)gReadReq) != 0) {
gAmigaSerialFailStage = 4u; gAmigaSerialFailStage = 4u;
gAmigaSerialFailErr = gReadReq->IOSer.io_Error; gAmigaSerialFailErr = gReadReq->IOSer.io_Error;
serialTeardown(); serialTeardown(false);
return false; return false;
} }
@ -198,7 +274,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gWriteReq == NULL) { if (gWriteReq == NULL) {
gAmigaSerialFailStage = 5u; gAmigaSerialFailStage = 5u;
serialTeardown(); serialTeardown(false);
return false; return false;
} }
*gWriteReq = *gReadReq; *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 jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n; uint16_t n;
uint16_t i; uint16_t i;

View file

@ -528,9 +528,9 @@ void jlpAudioCriticalExit(void) {
void jlpAudioNoise(uint8_t pitch, uint8_t atten) { void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
// Same Supexec-with-stashed-args shape as jlpAudioVoice; noise // Same Supexec-with-stashed-args shape as jlpAudioVoice. Noise plays
// inherits the MOD-collision caveat documented there (the PWM // through channel C, so it keeps playing while the PWM pump owns
// pump's installTimerA force-silences the whole PSG on start). // channel A (installTimerA takes channel A only).
gYmNoisePitch = pitch; gYmNoisePitch = pitch;
gYmNoiseAtten = atten; gYmNoiseAtten = atten;
Supexec(applyYmNoise); Supexec(applyYmNoise);

View file

@ -37,6 +37,12 @@
#define ST_RSCONF_INQUIRE ((int16_t)-2) #define ST_RSCONF_INQUIRE ((int16_t)-2)
#define ST_BAUD_CODE_MAX 15 #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 ----- // ----- Module state -----
@ -197,6 +203,10 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
baudCode = serialBaudCode(config->baud); baudCode = serialBaudCode(config->baud);
ucr = serialUcr(config); 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 // Snapshot the baud code BEFORE installing ours -- the inquire reports the
// rate currently in force. Range-gate the answer: a TOS that did not // 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 // 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 jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n; uint16_t n;

View file

@ -100,19 +100,17 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1];
#elif defined(JOEYLIB_PLATFORM_BLANK) #elif defined(JOEYLIB_PLATFORM_BLANK)
#define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops) #define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops)
#elif defined(JOEYLIB_PLATFORM_X68000) #elif defined(JOEYLIB_PLATFORM_X68000)
// Chunky 4bpp while the port is on the generic backend, so it shares DOS's // Plane-major like the Amiga (src/x68000/x68kTile.h reads jlTileT.pixels as
// target byte -- the payload is bytes, so the endian difference does not // 4 planes x 8 rows), baked by assetbake --target x68000.
// matter. This gets its own target the moment the storage model is decided #define EXPECTED_TILE_TARGET 5u
// (a text-plane port would bake Amiga-shaped tiles and want target 1).
#define EXPECTED_TILE_TARGET 3u
#else #else
#error "Unknown platform for asset loader" #error "Unknown platform for asset loader"
#endif #endif
// Per-platform target byte for .spc files (pre-compiled sprite code, so // 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 // it is per MACHINE CODE, not per pixel format: every port's banks carry
// chunky tile byte above but runs 68k planar sprite routines, and must // their own byte, so a bank baked for any other build is refused). Mirrors
// never accept an x86 bank). Mirrors SPC_TARGET in tools/spritebake. // SPC_TARGET in tools/spritebake.
#if defined(JOEYLIB_PLATFORM_AMIGA) #if defined(JOEYLIB_PLATFORM_AMIGA)
#define EXPECTED_SPC_TARGET 1u #define EXPECTED_SPC_TARGET 1u
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)

View file

@ -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 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 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 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 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 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); 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)) #define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n))
// The one Memory Manager owner ID every IIgs allocation uses (hal.c). // The one Memory Manager owner ID every IIgs allocation uses (hal.c).
uint16_t iigsMemUserId(void); 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 // SFX streams go to NTPstreamsound as unsigned bytes with 0x00 forbidden
// (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands // (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands
@ -1070,6 +1076,16 @@ void jlpGenericSerialFlush(void);
#define jlpSerialFlush() jlpGenericSerialFlush() #define jlpSerialFlush() jlpGenericSerialFlush()
#endif #endif
#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 // 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. // for a port whose RTS line is not controllable - a caller drives this unconditionally.
void jlpGenericSerialSetRts(bool on); void jlpGenericSerialSetRts(bool on);

View file

@ -7,8 +7,15 @@
// Single open port at a time (module-static gate), matching the add-on's // 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 // stated contract. Config is normalized + range-checked here so every per-port
// HAL can assume a sane jlSerialConfigT. // 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 <stddef.h> #include <stddef.h>
#include <stdlib.h>
#include "joey/serial.h" #include "joey/serial.h"
#include "port.h" #include "port.h"
@ -26,6 +33,12 @@
// ----- Module state ----- // ----- Module state -----
static bool gSerialOpen = false; static bool gSerialOpen = false;
static bool gSerialAtExitOn = false;
// ----- Prototypes -----
static void serialAtExit(void);
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
@ -79,6 +92,9 @@ bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
} }
gSerialOpen = jlpSerialOpen(device, &cfg); gSerialOpen = jlpSerialOpen(device, &cfg);
if (gSerialOpen && !gSerialAtExitOn) {
gSerialAtExitOn = atexit(serialAtExit) == 0;
}
return gSerialOpen; return gSerialOpen;
} }
@ -109,6 +125,14 @@ int16_t jlSerialReadByte(void) {
} }
void jlSerialSetDtr(bool on) {
if (!gSerialOpen) {
return;
}
jlpSerialSetDtr(on);
}
void jlSerialSetRts(bool on) { void jlSerialSetRts(bool on) {
if (!gSerialOpen) { if (!gSerialOpen) {
return; return;
@ -128,3 +152,11 @@ uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) {
bool jlSerialWriteByte(uint8_t b) { bool jlSerialWriteByte(uint8_t b) {
return jlSerialWrite(&b, 1u) == 1u; return jlSerialWrite(&b, 1u) == 1u;
} }
// ----- Internal -----
// The atexit handler (see the header comment): closes a port left open.
static void serialAtExit(void) {
jlSerialClose();
}

View file

@ -83,6 +83,7 @@ static _go32_dpmi_seginfo gNewIrqVec;
static void picMaskIrq(uint8_t irq); static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq); static void picUnmaskIrq(uint8_t irq);
static int picVectorForIrq(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 bool serialResolvePort(jlSerialDeviceE device, const jlSerialConfigT *cfg, uint16_t *outBase, uint8_t *outIrq);
static void serialRxIsr(void); 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 ----- // ----- Port resolution -----
// DEFAULT/MODEM/PRINTER -> COM1; SLOT -> COM<unit> (1..4). The canonical COM // DEFAULT/MODEM/PRINTER -> COM1; SLOT -> COM<unit> (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) { void jlpSerialClose(void) {
if (!gOpen) { if (!gOpen) {
return; return;
@ -188,7 +215,7 @@ void jlpSerialClose(void) {
picMaskIrq(gComIrq); picMaskIrq(gComIrq);
outportb((int)(gComBase + UART_IER), 0); // disable UART interrupts 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_set_protected_mode_interrupt_vector(vector, &gOldIrqVec);
_go32_dpmi_free_iret_wrapper(&gNewIrqVec); _go32_dpmi_free_iret_wrapper(&gNewIrqVec);
gIrqHooked = false; gIrqHooked = false;
@ -208,11 +235,14 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
uint8_t irq; uint8_t irq;
uint16_t divisor; uint16_t divisor;
uint8_t lcr; uint8_t lcr;
uint8_t mcrBefore;
int vector; int vector;
if (gOpen || !serialResolvePort(device, config, &base, &irq)) { if (gOpen || !serialResolvePort(device, config, &base, &irq)) {
return false; 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); divisor = (uint16_t)(UART_BAUD_BASE / config->baud);
if (divisor == 0u) { if (divisor == 0u) {
@ -251,7 +281,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
gNewIrqVec.pm_offset = (unsigned long)serialRxIsr; gNewIrqVec.pm_offset = (unsigned long)serialRxIsr;
gNewIrqVec.pm_selector = _go32_my_cs(); gNewIrqVec.pm_selector = _go32_my_cs();
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) { if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
outportb((int)(base + UART_MCR), 0); outportb((int)(base + UART_MCR), mcrBefore);
return false; return false;
} }
// Page-lock the ISR + every object it touches (fault at IRQ time hangs). // 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) { void jlpSerialPoll(void) {
// The RX IRQ fills the ring in the background; nothing to pump. // 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 jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n; uint16_t n;

View file

@ -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 // 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. // jlSerialSetRts unconditionally instead of guarding every call site with a platform test.
void jlpGenericSerialSetRts(bool on) { void jlpGenericSerialSetRts(bool on) {

View file

@ -49,6 +49,13 @@ typedef void *Pointer;
#define NTP_ENTRY_PLAY 3 #define NTP_ENTRY_PLAY 3
#define NTP_ENTRY_STOP 6 #define NTP_ENTRY_STOP 6
#define NTP_ENTRY_STREAMSOUND 24 #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 // The silent module handed to NTPprepare when no song is playing (see
// ntpArm). One track, one never-triggered streamed instrument, one // 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 // the machine down; guarding on gNTPPlaying instead would be safe but
// would silence every SFX in an application that plays no music, which // would silence every SFX in an application that plays no music, which
// is all of them today. // 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) { static void sfxTrigger(uint32_t structAddr) {
if (!gNTPArmed) { if (!gNTPArmed) {
return; return;
} }
*(volatile uint16_t *)(gNTPBase + NTP_IRQ_WINDOW) = gIigsIrqTight;
ntpCall(gNTPBase + NTP_ENTRY_STREAMSOUND, ntpCall(gNTPBase + NTP_ENTRY_STREAMSOUND,
(uint16_t)(structAddr & 0xFFFFu), (uint16_t)(structAddr & 0xFFFFu),
(uint16_t)((structAddr >> 16) & 0xFFu), (uint16_t)((structAddr >> 16) & 0xFFu),

View file

@ -134,8 +134,6 @@ extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y,
// bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index // bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index
// table (PERF-AUDIT #3). // table (PERF-AUDIT #3).
extern void iigsInitRowLut(void); 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 // Filled circle, scanline-style. Takes the raw fill nibble; the asm
// derives the doubled fill byte and RMW nibble bytes (PERF-AUDIT #41). // 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); 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. // in jlpInit so the visible bezel matches the cleared SHR background.
#define BORDER_COLOR_MASK 0xF0 #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 ----- // ----- Module state -----
static uint8_t gPreviousNewVideo = 0; static uint8_t gPreviousNewVideo = 0;
@ -190,6 +203,25 @@ static uint8_t gPreviousBorder = 0;
static uint8_t gPreviousShadow = 0; static uint8_t gPreviousShadow = 0;
static bool gModeSet = false; 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 / // SCB / palette upload skipping is now driven by gStageScbDirty /
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp- // gStagePaletteDirty (core/surface.c). The old per-frame memcmp-
// against-cached-copy approach was costing ~7 ms / frame. // against-cached-copy approach was costing ~7 ms / frame.
@ -315,6 +347,7 @@ void jlpBigFree(void *p) {
static void claimStagePixels(void); static void claimStagePixels(void);
static void irqLatencyApply(bool tight);
bool jlpInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
uint16_t tick; uint16_t tick;
@ -353,7 +386,9 @@ bool jlpInit(const jlConfigT *config) {
// bank $E1 is unreliable from jlpInit's calling context, so we // bank $E1 is unreliable from jlpInit's calling context, so we
// don't try it here. // don't try it here.
iigsInitRowLut(); iigsInitRowLut();
irqLatencyApply(gIrqTightHolds != 0u);
gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u;
gIigsBeamFrameTicks = (uint16_t)(((gFrameHz == 50u) ? BEAM_LINES_PAL : BEAM_LINES_NTSC) * BEAM_TICKS_PER_LINE);
gModeSet = true; gModeSet = true;
return true; return true;
} }
@ -396,25 +431,28 @@ void jlpPresent(const jlSurfaceT *src) {
// rows, which seed all 16 palettes, present, then do pixel-only presents // rows, which seed all 16 palettes, present, then do pixel-only presents
// over rows 190-199 (where a descending-push overrun would land first) // over rows 190-199 (where a descending-push overrun would land first)
// and re-compare $E1 against the stage. // and re-compare $E1 against the stage.
iigsSerialPump();
uploadScbAndPaletteIfNeeded(src); uploadScbAndPaletteIfNeeded(src);
iigsBlitStageToShr(); iigsBlitStageToShr();
iigsSerialPump();
} }
// GS/OS QuitGS, issued from the pure-constant asm routine in // Release one hold on the tight interrupt-latency bound; the last one out
// joeyDraw.s (iigsQuitGS). The first cut built the call stub in a C // puts the loose bound back.
// BSS array and hit the cross-bank DBR trap on the 10-segment STAXI void iigsIrqLatencyRelax(void) {
// link: the byte stores went DB-relative into bank $08 while the if (gIrqTightHolds == 0u) {
// `jsl gQuitStub` resolved to the array's true bank $10 -- the JSL return;
// executed zeros and BRK'd. The asm routine has no relocations; its }
// parameter block is the fixed scratch word at $E1:9DCC (inside the gIrqTightHolds--;
// survives-present $E1:9DC8-$9DFD window), written here before the irqLatencyApply(gIrqTightHolds != 0u);
// 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) // 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 // 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; *IIGS_SHADOW_REG = gPreviousShadow;
gModeSet = false; gModeSet = false;
} }
// Quit to GS/OS HERE instead of returning: the llvm-mos runtime's // Returns like every port's: main's return then goes through the
// post-main exit path is broken (a php/rtl thunk at segment offset // runtime's exit(), which runs the atexit handlers (an open serial
// $0072 mis-balances the stack and the machine COPs to the monitor // port's close among them) before GS/OS QUIT. This used to QuitGS
// at $00:0002 -- reproduced identically for KEYS and STAXI, see // itself because that runtime exit path crashed into the monitor
// claudeDocs/LLVM816-ASKS.md #6). jlShutdown is the last thing every example // (LLVM816-ASKS #6, fixed 2026-09-28).
// 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;
} }
@ -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) { uint8_t *jlpStageAllocPixels(void) {
return IIGS_STAGE_PIXELS; return IIGS_STAGE_PIXELS;
} }
@ -642,10 +679,7 @@ void jlpWaitVBL(void) {
tick = iigsGetTickWord(); tick = iigsGetTickWord();
while (iigsGetTickWord() == tick) { while (iigsGetTickWord() == tick) {
// The VBL spin is the client loop's longest deaf window (~16ms); drain the 3-byte SCC FIFO // spin until the heartbeat advances
// into the serial soft ring while waiting or a 9600-baud downlink loses mid-frame bytes
// (the X68000 lesson, same Z8530).
iigsSerialPump();
} }
} }

View file

@ -55,15 +55,10 @@
#define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L) #define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L)
#define IIGS_KMSTATUS ((volatile uint8_t *)0x00C027L) #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_BTN0 ((volatile uint8_t *)0x00C061L)
#define IIGS_BTN1 ((volatile uint8_t *)0x00C062L) #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_BUTTON_BIT 0x80
#define IIGS_PADDLE_BUSY 0x80
#define PADDLE_TIMEOUT 256
#define PADDLE_LO_THRESHOLD 64 #define PADDLE_LO_THRESHOLD 64
#define PADDLE_HI_THRESHOLD 192 #define PADDLE_HI_THRESHOLD 192
@ -115,6 +110,7 @@
static void buildAsciiTable(void); static void buildAsciiTable(void);
static void pollJoystick(void); static void pollJoystick(void);
static uint32_t pollJoystickTight(void);
static void pollMouse(void); static void pollMouse(void);
static void readModifierKeys(void); static void readModifierKeys(void);
static int8_t signExtend7(uint8_t raw); static int8_t signExtend7(uint8_t raw);
@ -235,8 +231,8 @@ static int8_t thresholdPaddle(uint8_t v) {
// approximates the paddle's 0..255 position (the Apple firmware // 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 // 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 // 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 // stays set forever and the asm's settle wait times out every frame --
// every frame -- ~3 ms wasted per frame on a stick that isn't there. // ~1 ms wasted per frame on a stick that isn't there.
// //
// After JOY_DISCONNECT_THRESHOLD consecutive timeouts we latch the // After JOY_DISCONNECT_THRESHOLD consecutive timeouts we latch the
// stick as absent and stop polling entirely. The app calls // 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) // py = (ret >> 16) & 0xFF (JoyY 0..255)
extern uint32_t iigsPollJoystickInner(void); 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) { static void pollJoystick(void) {
uint32_t result; uint32_t result;
uint8_t px; uint8_t px;
@ -310,7 +353,13 @@ static void pollJoystick(void) {
} }
// Asm read at 1 MHz -- result returned packed in registers (A:X). // Asm read at 1 MHz -- result returned packed in registers (A:X).
// 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(); result = iigsPollJoystickInner();
}
resolvedFlags = (uint8_t)(result & 0xFFu); resolvedFlags = (uint8_t)(result & 0xFFu);
px = (uint8_t)((result >> 8) & 0xFFu); px = (uint8_t)((result >> 8) & 0xFFu);
py = (uint8_t)((result >> 16) & 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 // 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 // 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 // rather than assuming an order, so we stay in sync even if a stray
@ -433,12 +560,6 @@ void jlpInputInit(void) {
void jlpInputPoll(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 kbd;
uint8_t ascii; uint8_t ascii;
uint8_t key; uint8_t key;
@ -457,14 +578,6 @@ void jlpInputPoll(void) {
// ever fails to clear. // ever fails to clear.
strobeObserved = false; strobeObserved = false;
for (drainGuard = 0; drainGuard < KBD_DRAIN_GUARD; drainGuard++) { 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; kbd = *IIGS_KBD;
if ((kbd & KBD_STROBE_BIT) == 0) { if ((kbd & KBD_STROBE_BIT) == 0) {
break; break;

View file

@ -175,6 +175,12 @@ clrExit:
; inhibited by jlpInit. ~1.5x faster. ; inhibited by jlpInit. ~1.5x faster.
; ;
; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI. ; 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 ; 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 ; ($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 ; `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 tcs ; SP = top of $01 stage region
lda sclrFillCap ; A = fillWord = the pushed value lda sclrFillCap ; A = fillWord = the pushed value
ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes
ldy gIigsClearChunkBlocks ; blocks until the next interrupt window
clrFastBlock: clrFastBlock:
pha pha
pha pha
@ -284,7 +291,34 @@ clrFastBlock:
pha pha
pha pha
dex dex
beq clrFastDone
dey
bne clrFastBlock 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 lda sclrSavedSp
tcs ; restore original SP tcs ; restore original SP
sep #0x20 sep #0x20
@ -314,6 +348,11 @@ sclrFillCap:
sclrSavedSp: sclrSavedSp:
.zero 2 .zero 2
.section .bss.sclrSlamSp,"aw"
.globl sclrSlamSp
sclrSlamSp:
.zero 2
; Flood-fill seed/match scratch (asm-only data; no C definition exists). ; Flood-fill seed/match scratch (asm-only data; no C definition exists).
; iigsFloodWalkAndScansInner writes the seed match result and the left/ ; iigsFloodWalkAndScansInner writes the seed match result and the left/
; right span extents here for the C wrapper to read back. ; 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 ; PLAN.md W2). The old rule both UNDER-promoted left-anchored
; mid-width bands and OVER-promoted right-anchored ones. ; mid-width bands and OVER-promoted right-anchored ones.
PEI_RUN_MIN = 8 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 php
phb phb
@ -4706,7 +4752,7 @@ PEI_RUN_MIN = 8
ldx #0 ; X = absolute row counter 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 ; 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 ; 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 ; clipped at the chunk budget, so no SEI hold got longer -- every row
@ -4805,7 +4851,7 @@ peiRowSkipGroup:
peiRowBounds: peiRowBounds:
cpx #200 cpx #200
bcs peiChunkDone bcs peiChunkDone
cpy #40 cpy gIigsPresentChunkRows
bcc peiRowLoop bcc peiRowLoop
peiChunkDone: peiChunkDone:
; Per-chunk teardown is now JUST the CLI: runs restore SP and switches ; Per-chunk teardown is now JUST the CLI: runs restore SP and switches
@ -4841,7 +4887,7 @@ peiRunGather:
iny iny
cpx #200 cpx #200
bcs peiRunGathered bcs peiRunGathered
cpy #40 cpy gIigsPresentChunkRows
bcs peiRunGathered ; chunk budget clips the run bcs peiRunGathered ; chunk budget clips the run
lda gStageMinWord,x lda gStageMinWord,x
inc a inc a
@ -4875,7 +4921,7 @@ peiRunGathered: ; X/Y = scan resume values
txa txa
sec sec
sbc gPeiRunFirst 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 ; STAGE B: computed entry. spOffset = 2*runMax + 1, parked
; at $E1:9DFE (the ONLY datum the switches-on loop loads); ; at $E1:9DFE (the ONLY datum the switches-on loop loads);
; the burst enters the PEI table at word runMax, pushing ; the burst enters the PEI table at word runMax, pushing
@ -5048,7 +5094,7 @@ peiRunDone:
lda gPeiOrigSp lda gPeiOrigSp
tcs ; caller stack back tcs ; caller stack back
; telemetry (W2 Stage 0): gPresentCopiedBytes += ; 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. ; ~13 cyc per slammed row -- accepted for verifiability.
; DBR is the caller's globals bank (slam never touches ; DBR is the caller's globals bank (slam never touches
; DBR), so the absolute counter access is safe. ; DBR), so the absolute counter access is safe.
@ -5112,7 +5158,7 @@ peiMvnGather:
iny iny
cpx #200 cpx #200
bcs peiMvnRunGo bcs peiMvnRunGo
cpy #40 cpy gIigsPresentChunkRows
bcs peiMvnRunGo bcs peiMvnRunGo
lda gStageMinWord,x lda gStageMinWord,x
cmp gPeiBandMin cmp gPeiBandMin
@ -5791,6 +5837,407 @@ gjNoJoy:
rtl ; uint32_t in A:X 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) ; iigsInputSnapshot(void)
; ;
@ -6912,23 +7359,3 @@ wsDoubledByte:
.globl wsHasTrail .globl wsHasTrail
wsHasTrail: wsHasTrail:
.zero 2 .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

View file

@ -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: // Three endpoints behind one API, selected at open:
// JL_SERIAL_MODEM -- modem port = Zilog 8530 SCC channel B // JL_SERIAL_MODEM -- modem port = Zilog 8530 SCC channel B
@ -14,22 +14,46 @@
// table. // table.
// //
// Direct register pokes through 24-bit long pointers to $00C0xx, exactly like // 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 // input.c / the video + DOC pokes -- no GS/OS driver and no ROM serial
// latency, and it does not fight the GS/OS heartbeat (JoeyLib's stated policy // firmware. RECEIVE IS INTERRUPT-DRIVEN (serialIrq.s): the open port's
// is to avoid ISRs that do). RX is drained on jlSerialPoll() into a bank-0 // receive interrupt reaches a handler through the ROM interrupt manager --
// ring; TX is a bounded poll on the transmit-empty status bit. The app owns // the SCC through the Misc Tool Set's serial vector, a Super Serial Card
// the port exclusively between jlSerialOpen and jlSerialClose. // 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 // 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 // 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 // (status) and WR0 need no pointer step. The pointer is ONE register for the
// (the pointer is not re-entrant), which they do here -- no serial ISR runs. // 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, // 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 // the classic Apple Super Serial Card decode) and the slot's Control-Panel
// "Your Card" setting should be confirmed against the target hardware/emulator // "Your Card" setting should be confirmed against the target hardware/emulator
// before relying on the SSC path. // before relying on the SSC path.
#include <iigs/toolbox.h>
#include "joey/serial.h" #include "joey/serial.h"
#include "port.h" #include "port.h"
@ -38,8 +62,7 @@
#define SCC_B_CTRL ((volatile uint8_t *)0x00C038L) // modem port command/control (ch B) #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_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_DATA_OFFSET 2 // a channel's data register sits 2 above its command register
#define SCC_A_DATA ((volatile uint8_t *)0x00C03BL) // printer port data (ch A)
// WR9 bits 7-6 are the reset command: 00 none, 01 channel B, 10 channel A, // 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 // 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_A 0x80u
#define SCC_WR9_RESET_B 0x40u #define SCC_WR9_RESET_B 0x40u
// RR0 (status) bits, read directly from the command register. // The rest of WR9, written along with the reset command: master interrupt
#define SCC_RR0_RX_AVAIL 0x01 // enable + no vector. WR9 is chip-wide and write-only, so this rewrites the
#define SCC_RR0_TX_EMPTY 0x04 // 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 // WR1 receive-interrupt mode: interrupt on every character (and on a special
// is GONE. Latched until an Error Reset (WR0 command 0x30), so it survives to the next pump pass. // receive condition). 0 = this channel never interrupts.
#define SCC_RR1_RX_OVRN 0x20 #define SCC_WR1_RX_INT_ALL 0x10u
#define SCC_WR0_ERR_RESET 0x30 #define SCC_WR1_NO_INTS 0x00u
// 6551 status bits. // RR3 interrupt-pending bits (ext/status, Tx, Rx) of each channel. The ROM's
#define ACIA_ST_RX_FULL 0x08 // interrupt entry only calls the serial vector for bits that are also in
#define ACIA_ST_TX_EMPTY 0x10 // SERFLAG, and dismisses any others untouched -- so a channel's bits must be
#define ACIA_ST_RX_OVRN 0x04 // 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 // 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. // /16 clock mode, so baud = 115200 / (TC+2) -> TC = 115200/baud - 2.
#define SCC_TC_NUM 115200L #define SCC_TC_NUM 115200L
// SSC 6551 lives at the high half of slot s's I/O window: $C080 + s*16 + 8. // ----- 6551 (Super Serial Card) -----
#define SSC_BASE(_slot) ((volatile uint8_t *)(0x00C088L + (long)(_slot) * 16L))
// 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_SIZE 512u
#define RX_RING_MASK (RX_RING_SIZE - 1u) #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. // ring->caller since open. Cheap enough to keep unconditionally.
uint16_t gIigsRxPumped = 0u; uint16_t gIigsRxPumped = 0u;
uint16_t gIigsRxRead = 0u; uint16_t gIigsRxRead = 0u;
// Receive-loss counters, same read-via-extern contract. Overruns = loss EVENTS the hardware latched // 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, // (SCC RR1 overrun, checked after every byte / 6551 status bit 2) - one latch observation may stand for
// so this is a loss indicator, not a byte count. Before it existed a pump-discipline gap was invisible // SEVERAL destroyed bytes, so this is a loss indicator, not a byte count; MAME's z80scc never surfaces
// except as a frame CRC drop and a retransmit stall. Dropped = bytes the SOFT ring refused because it // the SCC flag, so there it stays 0. Dropped = bytes the SOFT ring refused because it was full (read
// was full (read from the chip, then discarded); saturating, like the overrun count. // from the chip, then discarded). Both saturate.
uint16_t gIigsRxOverruns = 0u; uint16_t gIigsRxOverruns = 0u;
uint16_t gIigsRxDropped = 0u; uint16_t gIigsRxDropped = 0u;
static uint16_t gRxHead;
static uint16_t gRxTail;
static bool gOpen; // ----- Module state -----
static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA
static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg static volatile uint8_t *gReg; // SCC command register / 6551 command register
static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so jlpSerialSetRts 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 // can flip ONE bit without guessing the rest back: WR5 is
// write-only on this chip, and rebuilding it from scratch would // write-only on this chip, and rebuilding it from scratch would
// silently drop the data-bit and Tx-enable settings // 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 ----- // ----- Prototypes -----
extern uint8_t jlpIoRead8(const volatile uint8_t *p); extern uint16_t iigsIrqOff(void);
static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg); extern void iigsIrqRestore(uint16_t p);
static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_t reset, const jlSerialConfigT *cfg); extern uint32_t iigsSerialIsrAddress(uint16_t kind);
static bool serialTxReady(void); extern uint16_t iigsSerialService(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); 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);
// ----- Guaranteed 8-bit I/O read ----- static void serialIrqInstall(uint16_t kind, volatile uint8_t *port, uint8_t rr3Bits, uint16_t vector);
// static void serialIrqRemove(void);
// llvm816 widens a volatile uint8_t LOAD through a variable long pointer to a 16-bit `lda [dp],y` static void serialOpenScc(volatile uint8_t *ctrl, uint8_t reset, uint8_t rr3Bits, const jlSerialConfigT *cfg);
// (m=0), so every such register read also performs a phantom bus read of address+1 - and the SCC's static void serialSetLine(uint8_t sccBit, uint8_t aciaMask, uint8_t aciaOn, bool on);
// 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"
);
// ----- SCC helpers ----- // ----- 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) { static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val) {
uint16_t p;
p = iigsIrqOff();
if (reg != 0u) { if (reg != 0u) {
*ctrl = reg; *ctrl = reg;
} }
*ctrl = val; *ctrl = val;
iigsIrqRestore(p);
} }
// Point at RRn, then read it. Same adjacency rule as sccWrite: nothing may touch the SCC between the // Program a channel and start it, with its interrupts still off.
// pointer write and the read (safe here - WR1=0 means this channel never interrupts, and the HAL is static void serialInitScc(volatile uint8_t *ctrl, uint8_t reset, const jlSerialConfigT *cfg) {
// 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) {
uint8_t wr4; uint8_t wr4;
uint8_t wr3; uint8_t wr3;
uint8_t wr5; uint8_t wr5;
int32_t tc; int32_t tc;
(void)data;
// WR4: /16 clock, stop bits, parity. // WR4: /16 clock, stop bits, parity.
wr4 = 0x40u; // /16 clock rate wr4 = 0x40u; // /16 clock rate
wr4 |= (cfg->stopBits == 2u) ? 0x0Cu : 0x04u; // 2 or 1 stop bit 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 // 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). // 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. // This used to be WR9 = 0xC0, a force-hardware-reset that took the OTHER channel down with it.
// // The same write sets the chip-wide interrupt control (SCC_WR9_MIE_NV); a channel reset leaves
// Not fully solved, and it cannot be from here: WR9 is a single chip-wide register AND is // this channel's WR1 interrupt enables clear, and it stays quiet until serialOpenScc has the
// write-only, so the reset command unavoidably rewrites the chip's interrupt-control bits // handler in place.
// (MIE/VIS/NV) with our zeroes, and there is no way to read the previous value back to preserve sccWrite(ctrl, 9u, (uint8_t)(reset | SCC_WR9_MIE_NV));
// them. A driver on the other channel that takes interrupts -- AppleTalk -- is therefore still sccWrite(ctrl, 1u, SCC_WR1_NO_INTS); // WR1 is per-channel, WR9 is not
// disturbed, just no longer wiped. Sharing the SCC properly would mean going through GS/OS's
// serial drivers, which this HAL deliberately bypasses (see the file header).
sccWrite(ctrl, 9u, reset);
sccWrite(ctrl, 1u, 0x00u); // no interrupts from THIS channel -- WR1 is per-channel, WR9 is not
sccWrite(ctrl, 10u, 0x00u); // NRZ, no loop/mark-idle: a CHANNEL reset preserves the sccWrite(ctrl, 10u, 0x00u); // NRZ, no loop/mark-idle: a CHANNEL reset preserves the
// encoding bits the old chip-wide reset cleared, so a // encoding bits the old chip-wide reset cleared, so a
// channel left in FM0 by LocalTalk would stay there. // channel left in FM0 by LocalTalk would stay there.
sccWrite(ctrl, 4u, wr4); sccWrite(ctrl, 4u, wr4);
sccWrite(ctrl, 3u, wr3); // Rx bits, not yet enabled 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, 11u, 0x50u); // Rx & Tx clock = baud rate generator
sccWrite(ctrl, 12u, (uint8_t)(tc & 0xFFu)); // time constant low sccWrite(ctrl, 12u, (uint8_t)(tc & 0xFFu)); // time constant low
sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK
sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable 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 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 ----- // ----- 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) { static void serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) {
uint8_t control; uint8_t control;
uint8_t command;
uint8_t baudCode; uint8_t baudCode;
// 6551 internal baud rate codes (bits 3-0 of the control register). // 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; control |= 0x80u;
} }
// Command: DTR on (bit0) | rx IRQ disabled (bit1, polled) | RTS low, TX IRQ // Command: DTR on, receiver interrupt ENABLED, RTS asserted with the transmit interrupt off, parity.
// off (bits 3-2 = 10) | parity. gAciaCmd = (uint8_t)(ACIA_CMD_DTR | ACIA_CMD_RTS_ON);
command = 0x0Bu;
if (cfg->parity == JL_SERIAL_PARITY_ODD) { 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) { } 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[ACIA_STATUS] = 0x00u; // write to status register = programmed reset (DTR off: no interrupts)
base[3] = control; // control register base[ACIA_CONTROL] = control;
base[2] = command; // command register
} }
// ----- Status polling ----- // ----- Interrupt install / remove -----
static bool serialTxReady(void) { // Point `vector` at the handler for `kind` and publish the port to it. For the SCC, rr3Bits go into
if (gIsScc) { // SERFLAG so the ROM routes the channel's interrupts to the vector at all. Masked throughout: the
return (jlpIoRead8(gStatus) & SCC_RR0_TX_EMPTY) != 0u; // 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) {
return (jlpIoRead8(gStatus) & ACIA_ST_TX_EMPTY) != 0u; 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) ----- // ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) { 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) { void jlpSerialClose(void) {
gOpen = false; uint16_t p;
// Leave the chip configured; a polled port needs no teardown, and the app
// owns it until re-opened. 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) { void jlpSerialFlush(void) {
gRxHead = gRxTail; gIigsRxHead = gIigsRxTail;
} }
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
if (gOpen) { volatile uint8_t *base;
return false;
} gIigsRxHead = 0u;
gRxHead = 0u; gIigsRxTail = 0u;
gRxTail = 0u;
switch (device) { switch (device) {
case JL_SERIAL_PRINTER: case JL_SERIAL_PRINTER:
gIsScc = true; serialOpenScc(SCC_A_CTRL, SCC_WR9_RESET_A, SCC_RR3_CHAN_A, config);
gStatus = SCC_A_CTRL;
gData = SCC_A_DATA;
serialInitScc(SCC_A_CTRL, SCC_A_DATA, SCC_WR9_RESET_A, config);
break; break;
case JL_SERIAL_SLOT: { case JL_SERIAL_SLOT:
volatile uint8_t *base;
if (config->unit < 1u || config->unit > 7u) { if (config->unit < 1u || config->unit > 7u) {
return false; return false;
} }
base = SSC_BASE(config->unit); base = SSC_BASE(config->unit);
gIsScc = false; gReg = &base[ACIA_COMMAND];
gStatus = &base[1]; gData = base;
gData = &base[0]; gTxReadyBit = ACIA_ST_TX_EMPTY;
serialInitAcia(base, config); serialInitAcia(base, config);
serialIrqInstall(IIGS_SER_ACIA, base, 0u, IIGS_VECTOR_OTHER);
*gReg = gAciaCmd; // handler in: start the receiver and its interrupt
break; break;
}
case JL_SERIAL_MODEM: case JL_SERIAL_MODEM:
case JL_SERIAL_DEFAULT: case JL_SERIAL_DEFAULT:
default: default:
gIsScc = true; serialOpenScc(SCC_B_CTRL, SCC_WR9_RESET_B, SCC_RR3_CHAN_B, config);
gStatus = SCC_B_CTRL;
gData = SCC_B_DATA;
serialInitScc(SCC_B_CTRL, SCC_B_DATA, SCC_WR9_RESET_B, config);
break; break;
} }
gOpen = true;
return true; return true;
} }
// Drain the SCC hardware FIFO into the soft ring from ANY wait/blit/input site (the X68000 lesson: // The interrupt keeps the ring filled; this only catches what it could not -- a Super Serial Card
// a polled port with a tiny hardware FIFO loses mid-frame bytes during every deaf window - the Z8530 // whose interrupt switch is off.
// 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
}
void jlpSerialPoll(void) { void jlpSerialPoll(void) {
uint16_t tail; (void)iigsSerialService();
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
} }
uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
uint16_t n; uint16_t n;
uint16_t head; uint16_t head;
uint16_t tail;
head = gRxHead; head = gIigsRxHead;
for (n = 0u; n < max && head != gRxTail; n++) { tail = gIigsRxTail; // one snapshot: the handler may append while we copy
buf[n] = gRxRing[head]; for (n = 0u; n < max && head != tail; n++) {
buf[n] = gIigsRxRing[head];
head = (uint16_t)((head + 1u) & RX_RING_MASK); head = (uint16_t)((head + 1u) & RX_RING_MASK);
} }
gRxHead = head; gIigsRxHead = head;
gIigsRxRead = (uint16_t)(gIigsRxRead + n); gIigsRxRead = (uint16_t)(gIigsRxRead + n);
return 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 jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n; uint16_t n;
for (n = 0u; n < len; n++) { for (n = 0u; n < len; n++) {
uint16_t guard = 0u; uint16_t guard = 0u;
while (!serialTxReady()) { // The status read goes through iigsSerialService, which also drains the receiver: a 6551
// Full-duplex: drain the RX FIFO while waiting for TX-ready. Each TX byte at 9600 is a // status read clears its interrupt request, so whoever reads it must take the byte.
// ~1ms wait and the Z8530 RX FIFO is 3 bytes - an unpumped uplink mid-downlink-burst while ((iigsSerialService() & gTxReadyBit) == 0u) {
// measurably ate 4-byte clumps out of arriving frames.
jlpSerialPoll();
guard++; guard++;
if (guard == 0u) { // wrapped 65536 spins -- link stalled if (guard == 0u) { // wrapped 65536 spins -- link stalled
return n; return n;
@ -490,6 +524,5 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
} }
*gData = buf[n]; *gData = buf[n];
} }
jlpSerialPoll();
return n; return n;
} }

364
src/iigs/serialIrq.s Normal file
View file

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

View file

@ -197,11 +197,6 @@ static void spreadSetPalBase(uint8_t palBase);
static void uploadGfxPalette(const jlSurfaceT *src); static void uploadGfxPalette(const jlSurfaceT *src);
#endif #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 vdispHandler(void);
static void vdispPoll(void); static void vdispPoll(void);
@ -735,7 +730,6 @@ void jlpPresent(const jlSurfaceT *src) {
uint16_t plane; uint16_t plane;
uint16_t firstByte; uint16_t firstByte;
uint16_t rowBytes; uint16_t rowBytes;
uint16_t blitRows = 0;
// Four rows of dirty band read as ONE long. A clean row's minWord is the // 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 // 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so
@ -768,7 +762,6 @@ void jlpPresent(const jlSurfaceT *src) {
applyScbBands(); applyScbBands();
gStageScbDirty = false; gStageScbDirty = false;
} }
x68kSerialPump();
for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) { for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) {
if (minGroups[g] == 0xFFFFFFFFul) { if (minGroups[g] == 0xFFFFFFFFul) {
continue; continue;
@ -778,12 +771,6 @@ void jlpPresent(const jlSurfaceT *src) {
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
continue; 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. // Bands are 16-bit stage words = 4 pixels = 2 bytes of a plane.
firstByte = (uint16_t)(gStageMinWord[y] >> 1); firstByte = (uint16_t)(gStageMinWord[y] >> 1);
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
@ -816,7 +803,6 @@ void jlpPresent(const jlSurfaceT *src) {
const uint8_t *sp; const uint8_t *sp;
uint16_t b; uint16_t b;
uint8_t palBase; uint8_t palBase;
uint16_t blitRows = 0;
if (src == NULL) { if (src == NULL) {
return; return;
@ -837,18 +823,10 @@ void jlpPresent(const jlSurfaceT *src) {
return; return;
} }
sp = (const uint8_t *)&gSpreadPair[0][0][0]; 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++) { for (y = 0; y < SURFACE_HEIGHT; y++) {
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
continue; 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. // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane.
firstByte = (uint16_t)(gStageMinWord[y] >> 1); firstByte = (uint16_t)(gStageMinWord[y] >> 1);
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
@ -1015,9 +993,6 @@ void jlpInputPoll(void) {
uint32_t mouseButtons; uint32_t mouseButtons;
for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { 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); groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF);
} }
// Write both states each poll: the bitmap is live key-DOWN data, // Write both states each poll: the bitmap is live key-DOWN data,
@ -1148,11 +1123,6 @@ void jlpWaitVBL(void) {
guard = 0ul; guard = 0ul;
while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) { while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) {
vdispPoll(); 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++; guard++;
} }
} }

View file

@ -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 // Transmit goes through IOCS, whose calls map one-for-one onto the ST HAL's
// interface, hijack no vector, buffer nothing ourselves. The IOCS calls map // XBIOS ones (src/atarist/serial.c) --
// one-for-one onto the ST's XBIOS ones --
// _iocs_set232c <- Rsconf (baud + framing, one packed word) // _iocs_set232c <- Rsconf (baud + framing, one packed word)
// _iocs_isns232c <- Bconstat (input status) // _iocs_isns232c <- Bconstat (input status)
// _iocs_inp232c <- Bconin (read one byte, BLOCKS if empty) // _iocs_inp232c <- Bconin (read one byte, BLOCKS if empty)
// _iocs_osns232c <- Bcostat (transmitter ready) // _iocs_osns232c <- Bcostat (transmitter ready)
// _iocs_out232c <- Bconout (write one byte) // _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 // 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 // is not reachable here, so jlSerialDeviceE is accepted and ignored exactly as
// on the ST. // on the ST.
#include <x68k/dos.h>
#include <x68k/iocs.h> #include <x68k/iocs.h>
#include "port.h" #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 // _iocs_set232c's mode word, from the IOCS call reference (reproduced on Data
// confirmed. What IS confirmed, from XEiJ's RS232CTerminal.java (which reads // Crystal's X68k/IOCS page):
// the live mode word out of the RSDRV.SYS work area): // bits 15-14 stop bits 1 = 1, 2 = 1.5, 0 or 3 = 2
// bit 7 RTS/CTS flow control // bits 13-12 parity 1 = odd, 3 = even, 0 or 2 = none
// bit 9 XON/XOFF flow control // bits 11-10 data bits 0 = 5 or fewer, 1 = 6, 2 = 7, 3 = 8
// and its baud-rate table is // bit 9 XON/XOFF
// 0=75 1=150 2=300 3=600 4=1200 5=2400 6=4800 7=9600 8=19200 9=31250 // bit 8 SI/SO (does not work in ROM IOCS; never set)
// 10=38400 11=50000 12=57600 13=76800 14=115200 15=230400 // bit 7 RTS/CTS (RSDRV.SYS only, below)
// i.e. the rate is a FOUR-bit field, so it cannot sit at bits 0-2 with parity // bits 6-0 rate index 0..7 = 75 150 300 600 1200 2400 4800 9600
// at bit 3. The positions of the data/stop/parity fields remain unknown. // It returns the mode it replaced, which jlpSerialClose puts back.
// //
// Writing a guessed mode word is worse than writing none: a wrong framing // Two implementations answer it, and they differ above 9600 (both read from
// setting still transmits, it just corrupts every byte, which is exactly the // their code). The ROM's (IPL-ROM 1.0) takes indexes 0..7 and turns anything
// failure this port hit first time out (every received byte came back as 0x00 // else -- bit 7 included -- into 1200. Human68k's RS-232C driver RSDRV.SYS
// or 0x80 -- only the MSB surviving). // (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
// So jlpSerialOpen inherits whatever RSDRV.SYS configured at boot. The Human68k // constant 2, really 39062 bps, 1.7% fast) -- and reads bit 7 as RTS/CTS,
// "RS-232C DRIVER for X68000 version 2.02" banner appears during startup and // which overrides XON/XOFF: its _OSNS232C reports "not ready" while CTS is
// leaves the port at its default, which pairs with MAME's null_modem default // off, and its receive side drops RTS as its buffer fills. So the rates above
// of 9600 8N1. Honour jlSerialConfigT properly once the layout is confirmed // 9600 and RTS/CTS are used only when RSDRV is detected (x68kRsdrvPresent);
// against hardware documentation. // 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 // 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. // write instead of hanging the frame. Same guard the ST HAL uses.
@ -48,24 +91,18 @@
// ----- Soft receive ring ----------------------------------------------------- // ----- Soft receive ring -----------------------------------------------------
// //
// RSDRV.SYS's own receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a // The IOCS receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a 203-byte
// 203-byte frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the // frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the signature
// signature of a 64-entry ring filling while the client was busy and discarding until drained. Every // of a 64-entry ring filling while the client was busy and discarding until drained. So this port
// other JoeyLib target rides a big interrupt-fed ring (DOS hardware IRQ ring, ST XBIOS IOREC, and the // takes the receive interrupt over (below) and fills a soft ring of its own, the way DOS rides a
// 8-bit clients' IRQ rings), so only this port could lose mid-frame bytes whenever the caller went // hardware IRQ ring and the ST TOS's IOREC: the program can go deaf for a whole present without
// deaf for longer than ~66 ms at 9600 baud -- and jlpPresent's full-stage blit, jlpWaitVBL's V-DISP // losing a byte. Only when the takeover cannot be installed does x68kSerialPump drain the IOCS
// spin, and jlpInputPoll's IOCS trap storm all can. The fix: drain the IOCS ring into this soft ring // ring instead, from the add-on's own entry points -- then the app must call jlSerialPoll often.
// 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.
// //
// SIZED FOR THE FASTEST LINE, not just the 9600 default: this port inherits RSDRV.SYS's boot rate // 4 KB holds over four seconds at 9600 (jlpSerialOpen's ceiling, see the mode-word note above),
// (the mode word is deliberately not written), and RSDRV's table reaches 230400, where the 64-byte // longer than the worst deaf window (a >100 ms all-dirty present), so even an unpaced burst -- the
// IOCS ring covers only ~2.8 ms -- so every pump site keeps its interval near 1 ms (present: every // server's flowWait timeout degrade streams a whole screen without acks -- fits while the loop is
// 2 blitted rows; input poll: every bitsns group; VBL wait: every 64 spins). 4 KB here holds ~178 ms // busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes.
// 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.
#define X68K_RX_RING_CAP 4096u #define X68K_RX_RING_CAP 4096u
// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer // 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 x68kSccRxIsr(void);
extern void x68kSccSpIsr(void); extern void x68kSccSpIsr(void);
static uint16_t x68kIrqMask(void);
static void x68kIrqRestore(uint16_t sr);
static void sccIsrInstall(void); static void sccIsrInstall(void);
static void sccIsrUninstall(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; static bool gSccIsrInstalled = false;
// Has the app opened the port at all? Without this the polled pump traps into // 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 -- // 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. // about fifty _ISNS232C traps per frame for a game that never touched serial.
static bool gSerialOpened = false; 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 uint8_t gSccRxVec = 0u;
static uint32_t gSccOldRx = 0ul; static uint32_t gSccOldRx = 0ul;
static uint32_t gSccOldSp = 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) { static void sccIsrInstall(void) {
uint16_t sr; uint16_t sr;
uint8_t base; 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; return;
} }
// RR2 read is a two-access pointer sequence on the shared SCC, so no interrupt (the channel B // 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(); sr = x68kIrqMask();
*X68K_SCC_A_CTRL = 2u; *X68K_SCC_A_CTRL = 2u;
base = *X68K_SCC_A_CTRL; base = *X68K_SCC_A_CTRL;
@ -151,24 +191,108 @@ static void sccIsrInstall(void) {
static void sccIsrUninstall(void) { static void sccIsrUninstall(void) {
uint16_t sr; uint16_t sr;
int32_t ssp;
if (!gSccIsrInstalled) { if (!gSccIsrInstalled) {
return; 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(); sr = x68kIrqMask();
*X68K_VECTOR(gSccRxVec) = gSccOldRx; *X68K_VECTOR(gSccRxVec) = gSccOldRx;
*X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp; *X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp;
x68kIrqRestore(sr); x68kIrqRestore(sr);
x68kSuperLeave(ssp);
gSccIsrInstalled = false; gSccIsrInstalled = false;
} }
// Drain everything the IOCS ring holds into the soft ring. Called from the HAL's own long-running // Installing and removing the RX ISR touches the vector table, the SCC and
// paths (present / VBL wait / input poll) and from every serial entry point. With the SCC ISR // the SR, all supervisor-only. Inside a JoeyLib app jlInit has already
// installed this is a cheap no-op guard (RSDRV's ring never fills); without it, it is the polled // switched to supervisor, but the add-on must not depend on that: SERTEST
// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead // opens the port without jlInit. _dos_super(0) switches and returns the old
// of RSDRV's ~64 bytes. // stack pointer, or a negative value when the CPU already was supervisor;
void x68kSerialPump(void) { // 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) { if (gSccIsrInstalled || !gSerialOpened) {
return; 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) ----- // ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) { 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) { 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 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; gSerialOpened = false;
} }
@ -207,16 +386,15 @@ void jlpSerialFlush(void) {
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
uint16_t mode;
(void)device; // one RS-232C line; channel B is the mouse. (void)device; // one RS-232C line; channel B is the mouse.
if (config == NULL) { if (config == NULL) {
return false; return false;
} }
// Line config is accepted but NOT applied -- see the mode-word note above. The mode = x68kSerialMode(config, x68kRsdrvPresent());
// port keeps whatever RSDRV.SYS set at boot. (An RTS/CTS enable via an gPrevMode = _iocs_set232c((int)mode);
// _iocs_set232c(-1) read-modify-write was tried and REVERTED: the -1-as-query gX68kWr5 = x68kWr5ForMode(mode); // _SET232C just wrote this, DTR on
// idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the
// RSDRV work area instead -- so the write could not be trusted.)
(void)config;
gSerialOpened = true; gSerialOpened = true;
sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong
return true; 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 // 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. // 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 // 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; uint16_t sr;
int ready; 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++) { for (n = 0u; n < len; n++) {
guard = 0ul; guard = 0ul;
for (;;) { for (;;) {
if (gSccIsrInstalled) {
sr = x68kIrqMask(); sr = x68kIrqMask();
ready = _iocs_osns232c(); ready = _iocs_osns232c();
x68kIrqRestore(sr); x68kIrqRestore(sr);
} else {
ready = _iocs_osns232c();
}
if (ready != 0) { if (ready != 0) {
break; break;
} }
@ -285,9 +485,13 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
return n; return n;
} }
} }
if (gSccIsrInstalled) {
sr = x68kIrqMask(); sr = x68kIrqMask();
_iocs_out232c((int)buf[n]); _iocs_out232c((int)buf[n]);
x68kIrqRestore(sr); x68kIrqRestore(sr);
} else {
_iocs_out232c((int)buf[n]);
}
} }
return n; return n;
} }

View file

@ -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 // 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 // is 8 rows x 4 planes x 1 byte on both machines and the .tbk payload is
// byte-compatible; only the type names differ. // byte-compatible (assetbake --target x68000 writes the Amiga layout under its
// Amiga tile ops as always-inline functions (NATIVE-PERF Phase 2 // own target byte); only the type names differ.
// R4a). Bodies moved verbatim from src/amiga/hal.c so the public // Tile ops as always-inline functions, so the public wrappers in
// wrappers in src/core/tile.c inline them, deleting the second full // src/core/tile.c inline them, deleting the second full cdecl call layer
// cdecl call layer (~130-190 cycles/op on the 68000). The jlpTile* // (~130-190 cycles/op on the 68000). The jlpTile* dispatch names are
// dispatch names are macro-aliased to these in port.h, which also // macro-aliased to these in port.h, which also #includes this header at its
// #includes this header at its tail -- AFTER the jlpGenericTile* // tail -- AFTER the jlpGenericTile* prototypes the portData == NULL fallbacks
// prototypes the portData == NULL fallbacks call. Do not include this // call. Do not include this header directly; it comes with port.h.
// header directly; it comes with port.h on the Amiga.
#ifndef JOEY_X68K_TILE_H #ifndef JOEY_X68K_TILE_H
#define JOEY_X68K_TILE_H #define JOEY_X68K_TILE_H

View file

@ -10,6 +10,7 @@
// final tally; exits nonzero if any check failed, so it doubles as a CI gate. // final tally; exits nonzero if any check failed, so it doubles as a CI gate.
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "joey/serial.h" #include "joey/serial.h"
@ -27,6 +28,10 @@ static uint16_t gFifoTail;
static bool gMockOpen; static bool gMockOpen;
static jlSerialConfigT gMockLastCfg; // config the core handed the HAL static jlSerialConfigT gMockLastCfg; // config the core handed the HAL
static bool gMockOpenResult = true; // test can force open failure 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_*) ----- // ----- 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 jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n; 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) { static jlSerialConfigT makeConfig(uint32_t baud, uint8_t dataBits, uint8_t stopBits) {
jlSerialConfigT cfg; jlSerialConfigT cfg;
@ -122,6 +152,7 @@ int main(void) {
int16_t b; int16_t b;
printf("serial-host: core gate/normalize/loopback\n"); printf("serial-host: core gate/normalize/loopback\n");
atexit(checkClosedAtExit);
// 1. Every call is inert before a successful open. // 1. Every call is inert before a successful open.
check("closed: available == 0", jlSerialAvailable() == 0u); 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: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u);
check("closed: readByte == -1", jlSerialReadByte() == -1); check("closed: readByte == -1", jlSerialReadByte() == -1);
check("closed: writeByte == false", jlSerialWriteByte('x') == false); 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 // 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). // 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. // 3. Zero fields normalize to library defaults before reaching the HAL.
cfg = makeConfig(0u, 0u, 0u); cfg = makeConfig(0u, 0u, 0u);
check("open: zero-field config accepted", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == true); 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: baud -> 9600", gMockLastCfg.baud == 9600u);
check("normalize: dataBits -> 8", gMockLastCfg.dataBits == 8u); check("normalize: dataBits -> 8", gMockLastCfg.dataBits == 8u);
check("normalize: stopBits -> 1", gMockLastCfg.stopBits == 1u); check("normalize: stopBits -> 1", gMockLastCfg.stopBits == 1u);
@ -172,8 +216,14 @@ int main(void) {
jlSerialFlush(); jlSerialFlush();
check("flush: available == 0", jlSerialAvailable() == 0u); 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(); 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); check("closed again: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u);
cfg = makeConfig(2400u, 8u, 1u); cfg = makeConfig(2400u, 8u, 1u);
check("reopen after close", jlSerialOpen(JL_SERIAL_PRINTER, &cfg) == true); 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); check("open: closed after HAL failure", jlSerialWriteByte('x') == false);
gMockOpenResult = true; 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); printf("serial-host: %d passed, %d failed\n", gPass, gFail);
return (gFail == 0) ? 0 : 1; return (gFail == 0) ? 0 : 1;
} }

View file

@ -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_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_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_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 CADIUS='/home/scott/claude/joeylib/toolchains/opt/cadius/bin/cadius'
export DOS_CWSDPMI='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin' export DOS_CWSDPMI='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin'
export DOS_CWSDSTUB='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin/CWSDSTUB.EXE' export DOS_CWSDSTUB='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin/CWSDSTUB.EXE'

138
tools/adfTool.py Executable file
View file

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

View file

@ -6,8 +6,8 @@
# Each target's runtime can use the baked bytes directly: # Each target's runtime can use the baked bytes directly:
# * Tile banks (.tbk) are per-target planar (or chunky for DOS/IIgs). # * Tile banks (.tbk) are per-target planar (or chunky for DOS/IIgs).
# The Phase 11 walker reads sprite tile data as chunky 4bpp, but # The Phase 11 walker reads sprite tile data as chunky 4bpp, but
# tile-paste is platform-private -- Amiga is plane-major, ST is # tile-paste is platform-private -- Amiga and X68000 are plane-major,
# row-major-with-planes-per-row, DOS / IIgs are chunky 4bpp. # ST is row-major-with-planes-per-row, DOS / IIgs are chunky 4bpp.
# * Sprite cels (.spr) are cross-target chunky 4bpp. The sprite walker # * Sprite cels (.spr) are cross-target chunky 4bpp. The sprite walker
# reads chunky and does c2p inline at draw time, so the same blob # reads chunky and does c2p inline at draw time, so the same blob
# works on every platform. # works on every platform.
@ -27,7 +27,8 @@
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# offset bytes field # offset bytes field
# 0 4 magic "JTB1" # 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) # 5 1 hasPalette (0 or 1)
# 6 2 tileCount (LE16) # 6 2 tileCount (LE16)
# 8 4 reserved (zero) # 8 4 reserved (zero)
@ -38,6 +39,8 @@
# [r0_p0 r0_p1 r0_p2 r0_p3][r1_p0..] # [r0_p0 r0_p1 r0_p2 r0_p3][r1_p0..]
# dos : chunky 4bpp [r0_b0..3][r1_b0..3]... # dos : chunky 4bpp [r0_b0..3][r1_b0..3]...
# iigs : chunky 4bpp (same as dos) # 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 # .spr format
@ -70,12 +73,14 @@ TARGET_AMIGA = 1
TARGET_ATARIST = 2 TARGET_ATARIST = 2
TARGET_DOS = 3 TARGET_DOS = 3
TARGET_IIGS = 4 TARGET_IIGS = 4
TARGET_X68000 = 5
TARGET_NAMES = { TARGET_NAMES = {
"amiga": TARGET_AMIGA, "amiga": TARGET_AMIGA,
"atarist": TARGET_ATARIST, "atarist": TARGET_ATARIST,
"dos": TARGET_DOS, "dos": TARGET_DOS,
"iigs": TARGET_IIGS, "iigs": TARGET_IIGS,
"x68000": TARGET_X68000,
} }
TILE_PX = 8 # 8x8 tile TILE_PX = 8 # 8x8 tile
@ -134,7 +139,7 @@ def encodeTileChunky(grid, ox, oy):
def encodeTilePlaneMajor(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. # Plane 0 bytes 0..7, plane 1 bytes 8..15, etc.
out = bytearray() out = bytearray()
for k in range(4): for k in range(4):
@ -167,6 +172,7 @@ TILE_ENCODERS = {
TARGET_ATARIST: encodeTileRowPlanesPerRow, TARGET_ATARIST: encodeTileRowPlanesPerRow,
TARGET_DOS: encodeTileChunky, TARGET_DOS: encodeTileChunky,
TARGET_IIGS: encodeTileChunky, TARGET_IIGS: encodeTileChunky,
TARGET_X68000: encodeTilePlaneMajor,
} }