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