Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL.

This commit is contained in:
Scott Duensing 2026-09-28 15:27:29 -05:00
parent 07f18d9ef5
commit 8b9590ddb2
116 changed files with 5632 additions and 4003 deletions

7
.gitignore vendored
View file

@ -11,8 +11,11 @@ build/
*.gs *.gs
*.bin *.bin
# Toolchain installations (everything fetched/built by install.sh) # Everything ./install.sh downloads, builds or generates. Nothing in it is
toolchains/* # tracked, so the folder can be deleted at any time and re-created.
toolchains/
# A project's private-bundle URL (installer/bundle.conf) is the owner's
# choice to commit or not; it is not ignored.
# Disk images # Disk images
*.adf *.adf

View file

@ -22,7 +22,7 @@ JoeyLib. License compatibility for these matters most.
- **License:** MIT - **License:** MIT
- **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Linked - **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Linked
into AUDIO binaries on the chunky-display platforms. into AUDIO binaries on the chunky-display platforms.
- **Vendored at:** `toolchains/audio/libxmp-lite/` - **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
### NinjaTrackerPlus (NTP) ### NinjaTrackerPlus (NTP)
@ -33,10 +33,9 @@ JoeyLib. License compatibility for these matters most.
by Jesse Blue (NinjaForce). Treated here as free for use; consult by Jesse Blue (NinjaForce). Treated here as free for use; consult
upstream before commercial redistribution. upstream before commercial redistribution.
- **Role:** Apple IIgs Ensoniq DOC `.NTP` module replayer + SFX - **Role:** Apple IIgs Ensoniq DOC `.NTP` module replayer + SFX
streamer. Merlin32-assembled and **linked** into the AUDIO binary as streamer. Merlin32-assembled into `ntpplayer`, which ships on the disk
bytes (see `make/iigs.mk` `$(NTP_HEADER)` rule). next to each IIgs program and is loaded at run time.
- **Vendored at:** `toolchains/iigs/ntp/ninjatrackerplus.s` (source); - **Installed at:** `toolchains/opt/ntp/` (source; installer component `ntp`).
build product baked into `examples/audio` AUDIO binary.
### PTPlayer ### PTPlayer
@ -46,7 +45,7 @@ JoeyLib. License compatibility for these matters most.
- **License:** Public Domain - **License:** Public Domain
- **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the - **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the
Paula audio HAL. Paula audio HAL.
- **Vendored at:** `toolchains/amiga/ptplayer/` - **Installed at:** `toolchains/opt/ptplayer/` (installer component `ptplayer`)
--- ---
@ -64,10 +63,10 @@ produced game binaries.
LLVM license). LLVM license).
- **Role:** clang C compiler targeting the 65816, plus llvm-mc for - **Role:** clang C compiler targeting the 65816, plus llvm-mc for
assembling hand-written `.s` files and link816 for producing OMF assembling hand-written `.s` files and link816 for producing OMF
S16 executables. Driven by `toolchains/iigs/clang-build.sh`; the S16 executables. Driven by `make/support/iigs/clang-build.sh`; the
toolchain root is located via the `LLVM816_ROOT` environment toolchain root is located via the `LLVM816_ROOT` environment
variable. variable.
- **Vendored at:** `toolchains/iigs/llvm-mos/` - **Installed at:** `toolchains/opt/llvm816/` (installer component `llvm816`)
### Merlin32 ### Merlin32
@ -75,12 +74,12 @@ produced game binaries.
- **URL:** <https://brutaldeluxe.fr/products/crossdevtools/merlin/> - **URL:** <https://brutaldeluxe.fr/products/crossdevtools/merlin/>
- **License:** Freely distributed by Brutal Deluxe Software - **License:** Freely distributed by Brutal Deluxe Software
(Olivier Zardini, Antoine Vignau). Source published with no (Olivier Zardini, Antoine Vignau). Source published with no
formal open-source license header — Brutal Deluxe's "open source formal open-source license header -- Brutal Deluxe's "open source
philosophy" but copyright is retained. Treat as "free to use, philosophy" but copyright is retained. Treat as "free to use,
redistribute as part of a build chain; ask before forking redistribute as part of a build chain; ask before forking
commercially." commercially."
- **Role:** 65816 cross-assembler. Builds NinjaTrackerPlus. - **Role:** 65816 cross-assembler. Builds NinjaTrackerPlus.
- **Vendored at:** `toolchains/iigs/merlin32/` - **Installed at:** `toolchains/opt/merlin32/` (installer component `merlin32`)
### Bebbo m68k-amigaos GCC toolchain ### Bebbo m68k-amigaos GCC toolchain
@ -92,13 +91,13 @@ produced game binaries.
domain, ira freeware, etc.). domain, ira freeware, etc.).
- **Role:** AmigaOS HUNK-format gcc + binutils + libnix used to - **Role:** AmigaOS HUNK-format gcc + binutils + libnix used to
compile the Amiga port and link against PTPlayer. compile the Amiga port and link against PTPlayer.
- **Vendored at:** `toolchains/amiga/gcc/` - **Installed at:** `toolchains/opt/amigaGcc/` (installer component `amigaGcc`)
### vasm ### vasm
- **Used on:** Amiga (PTPlayer only) - **Used on:** Amiga (PTPlayer only)
- **URL:** <http://sun.hasenbraten.de/vasm/> - **URL:** <http://sun.hasenbraten.de/vasm/>
- **License:** vasm custom license — © Volker Barthelmann; free for - **License:** vasm custom license -- (c) Volker Barthelmann; free for
non-commercial use, plus an explicit **commercial-use exception non-commercial use, plus an explicit **commercial-use exception
for M68k + AmigaOS targets** (see vasm distribution README). Other for M68k + AmigaOS targets** (see vasm distribution README). Other
commercial use requires written permission. JoeyLib's only commercial use requires written permission. JoeyLib's only
@ -107,7 +106,8 @@ produced game binaries.
- **Role:** Assemble third-party `ptplayer.asm` (Frank Wille's - **Role:** Assemble third-party `ptplayer.asm` (Frank Wille's
Devpac/vasm-syntax source). Our own `.s` files are GAS syntax and Devpac/vasm-syntax source). Our own `.s` files are GAS syntax and
go through `m68k-amigaos-as` (binutils, GPL) instead. go through `m68k-amigaos-as` (binutils, GPL) instead.
- **Vendored at:** `toolchains/amiga/vasm/` - **Installed at:** built with the Amiga GCC toolchain (AmigaPorts `vasm`
sub-project), `toolchains/opt/amigaGcc/bin/vasmm68k_mot`
### m68k-atari-mint GCC toolchain (Thorsten Otto builds) ### m68k-atari-mint GCC toolchain (Thorsten Otto builds)
@ -118,7 +118,7 @@ produced game binaries.
and permissive (per-file headers). and permissive (per-file headers).
- **Role:** GEMDOS-native gcc + binutils + mintlib used to compile - **Role:** GEMDOS-native gcc + binutils + mintlib used to compile
the Atari ST port. the Atari ST port.
- **Vendored at:** `toolchains/atarist/gcc/` - **Installed at:** `toolchains/opt/mintGcc/` (installer component `mintGcc`)
### DJGPP (build-djgpp packaging) ### DJGPP (build-djgpp packaging)
@ -127,23 +127,23 @@ produced game binaries.
(DJGPP upstream: <http://www.delorie.com/djgpp/>) (DJGPP upstream: <http://www.delorie.com/djgpp/>)
- **License:** Build script GNU GPL v3; bundled gcc/binutils GNU - **License:** Build script GNU GPL v3; bundled gcc/binutils GNU
GPL v2; DJGPP runtime libraries (libc) are mostly public-domain or GPL v2; DJGPP runtime libraries (libc) are mostly public-domain or
permissive — see DJGPP `readme.1st`. permissive -- see DJGPP `readme.1st`.
- **Role:** 32-bit protected-mode gcc cross-compiler producing - **Role:** 32-bit protected-mode gcc cross-compiler producing
COFF binaries that run on 386+ DOS via DPMI. COFF binaries that run on 386+ DOS via DPMI.
- **Vendored at:** `toolchains/dos/djgpp/` - **Installed at:** `toolchains/opt/djgpp/` (installer component `djgpp`)
### CWSDPMI ### CWSDPMI
- **Used on:** DOS - **Used on:** DOS
- **URL:** <https://sandmann.dotster.com/cwsdpmi/> - **URL:** <https://sandmann.dotster.com/cwsdpmi/>
- **License:** Custom redistribution terms (Charles W. Sandmann, - **License:** Custom redistribution terms (Charles W. Sandmann,
© 1995–2010). Binary may be redistributed without source provided (c) 1995-2010). Binary may be redistributed without source provided
the executables are unmodified and end users are notified of the executables are unmodified and end users are notified of
source availability — full terms in `cwsdpmi.doc`. Source is source availability -- full terms in `cwsdpmi.doc`. Source is
GPL-licensed if distributed. GPL-licensed if distributed.
- **Role:** Stubbed-into-binary DPMI server for DJGPP-compiled DOS - **Role:** Stubbed-into-binary DPMI server for DJGPP-compiled DOS
executables (via `embed-dpmi.sh`). executables (via `embed-dpmi.sh`).
- **Vendored at:** `toolchains/dos/cwsdpmi/` - **Installed at:** `toolchains/opt/cwsdpmi/` (installer component `cwsdpmi`)
### NASM ### NASM
@ -152,93 +152,95 @@ produced game binaries.
- **License:** 2-clause BSD - **License:** 2-clause BSD
- **Role:** Assembler for x86 inline-asm support files in the DOS - **Role:** Assembler for x86 inline-asm support files in the DOS
port. port.
- **Vendored at:** `toolchains/dos/nasm/` - **Installed from:** the Linux distribution's `nasm` package
--- ---
## Test-time emulators (not shipped, not linked) ## Test-time emulators (not shipped, not linked)
Used by `scripts/run-*.sh` to run the produced binaries on the Used by `joey-run` and the test gates to run the produced binaries on the
developer's machine. Not part of any redistributed artifact. developer's machine. Not part of any redistributed artifact. Installed by
`./install.sh install emulators`.
### GSplus (KEGS fork)
- **Used on:** Apple IIgs
- **URL:** <https://github.com/digarok/gsplus>
- **License:** GNU GPL v3
- **Vendored at:** `toolchains/emulators/gsplus/`
### MAME ### MAME
- **Used on:** Apple IIgs (apple2gs driver, optional) - **Used on:** Apple IIgs (apple2gs driver) and Sharp X68000 (x68k driver)
- **URL:** <https://www.mamedev.org/> - **URL:** <https://www.mamedev.org/>
- **License:** GNU GPL v2+ (GPL-2.0-or-later) plus a 3-clause BSD - **License:** GNU GPL v2+ (GPL-2.0-or-later) plus a 3-clause BSD
alternative for some components. alternative for some components.
- **Installed at:** `toolchains/opt/mame/`, built from the 0.264 release with
the patches in `installer/patches/mame/` (installer component `mame`).
### Hatari ### Hatari
- **Used on:** Atari ST - **Used on:** Atari ST
- **URL:** <https://www.hatari-emu.org/> - **URL:** <https://www.hatari-emu.org/>
- **License:** GNU GPL v2 - **License:** GNU GPL v2
- **Installed at:** the distribution package on Debian and Ubuntu; on Fedora,
`toolchains/opt/hatari/`, built from the 2.4.1 source (installer component
`hatari`)
### DOSBox ### DOSBox Staging
- **Used on:** DOS - **Used on:** DOS
- **URL:** <https://www.dosbox.com/> - **URL:** <https://www.dosbox-staging.org/>
- **License:** GNU GPL v2 - **License:** GNU GPL v2+
- **Installed at:** `toolchains/opt/dosbox/` (the project's Linux release;
installer component `dosbox`)
### FS-UAE ### FS-UAE
- **Used on:** Amiga - **Used on:** Amiga
- **URL:** <https://fs-uae.net/> - **URL:** <https://fs-uae.net/>
- **License:** GNU GPL v2 (UAE codebase) - **License:** GNU GPL v2 (UAE codebase)
- **Installed at:** the distribution package on Debian and Ubuntu; on Fedora,
`toolchains/opt/fsuae/`, built from the 3.1.66 release (installer component
`fsuae`)
### EmuTOS ### EmuTOS
- **Used on:** Atari ST (TOS replacement for Hatari) - **Used on:** Atari ST (TOS replacement for Hatari)
- **URL:** <https://emutos.sourceforge.io/> - **URL:** <https://emutos.sourceforge.io/>
- **License:** GNU GPL v2 - **License:** GNU GPL v2
- **Vendored at:** `toolchains/emulators/support/emutos-512k.img` - **Installed at:** `toolchains/firmware/atarist/emutos-512k.img` (installer
component `emutos`)
--- ---
## System ROMs and disk images (used for testing only) ## System ROMs and disk images (used for testing only)
These are **not** part of JoeyLib distribution and **not** These are **not** part of JoeyLib distribution. `./install.sh`
redistributed. They are downloaded to the developer's local cache by places them in `toolchains/firmware/` for emulator-side testing only; see
`toolchains/install.sh` for emulator-side testing only. License terms the source of each before shipping anything that depends on them.
prohibit redistribution; please consult the source if you intend to
ship anything that depends on them.
### Apple IIgs ROM 03 ### Downloaded by the installer
- **Used on:** Apple IIgs (GSplus, MAME) - **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, and `apple2gsr1`,
- **URL:** Various preservation archives, e.g. ROM 01): Apple Inc. proprietary firmware, from the archive.org MAME 0.240
<https://mirrors.apple2.org.za/ftp.apple.asimov.net/emulators/rom_images/> split set. Component `iigsFirmware`.
- **License:** Apple Inc. proprietary firmware. Provided here for - **Apple IIgs System 6.0.4** (`System.Disk`): Apple Inc. proprietary OS, from
developer testing only; **do not redistribute** as part of any the Apple II archive (<https://mirrors.apple2.org.za/>). Component
derivative project. `iigsFirmware`.
- **CFFA2 card firmware** (`cffa20eec02.bin`): freely redistributed by
dreher.net (<https://dreher.net/>). Component `iigsFirmware`.
- **X68000 IPL-ROM and Human68k 3.02 system disk**: from Sharp's own free
release (2000), published by the X68000 LIBRARY
(<http://retropc.net/x68000/software/sharp/>). Component `x68kFirmware`.
### Apple IIgs System 6.0.4 ### Never downloaded (supplied by the user or the project's own bundle)
- **Used on:** Apple IIgs (GSplus boot disk) Installed with `./install.sh import PATH`, or from the bundle a
- **URL:** <https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/gs/os/gsos/> project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in
- **License:** Apple Inc. proprietary OS. Same caveat as the ROM. `installer/files.sha1`.
### Apple IIgs apple2gs MAME ROM set - **Amiga Kickstart 3.1 ROM**: Cloanto / Amiga Corp. proprietary, sold today
(Amiga Forever). Required by the Amiga test gates; `joey-run` falls back to
- **Used on:** Apple IIgs (MAME testing path) FS-UAE's AROS without it. Component `kickstart`.
- **URL:** Same preservation archives as above. - **X68000 CG (font) ROM**: not part of Sharp's free release. MAME's x68000
- **License:** Apple Inc. proprietary firmware. Required for MAME's driver needs it. Component `x68kCgrom`.
apple2gs driver but not redistributed. - **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original
game's data, from which the example's levels are extracted. Optional.
### Amiga Kickstart ROM (optional) Component `staxiData`.
- **Used on:** Amiga (FS-UAE, optional — falls back to AROS)
- **URL:** Cloanto / Amiga Forever (commercial), or original media.
- **License:** Cloanto / Amiga Corp. proprietary. JoeyLib does **not**
bundle Kickstart; users supply their own ROM at
`toolchains/emulators/support/kickstart.rom` if they have one.
--- ---
@ -249,7 +251,7 @@ When adding a new third-party dependency:
1. Append a row in the appropriate section above. 1. Append a row in the appropriate section above.
2. Record platform(s), upstream URL, license name, and a one-line 2. Record platform(s), upstream URL, license name, and a one-line
role description. role description.
3. If the dependency is vendored, list the path under 3. List where the installer puts it (`toolchains/opt/<component>/` or
`toolchains/`. `toolchains/firmware/`).
4. If license terms restrict redistribution, call that out 4. If license terms restrict redistribution, call that out
explicitly (as we do for Apple ROMs). explicitly (as we do for Apple ROMs).

View file

@ -1,27 +1,30 @@
# JoeyLib top-level Makefile. # JoeyLib top-level Makefile.
# #
# Usage: # Usage:
# source toolchains/env.sh (first, every shell session) # make builds every target whose toolchain is installed
# make builds every target whose toolchain is present
# make iigs builds Apple IIgs only # make iigs builds Apple IIgs only
# make amiga builds Commodore Amiga only # make amiga builds Commodore Amiga only
# make atarist builds Atari ST only # make atarist builds Atari ST only
# make dos builds MS-DOS only # make dos builds MS-DOS only
# make x68000 builds Sharp X68000 only
# make docs renders the manual to build/docs (HTML + PDF) # make docs renders the manual to build/docs (HTML + PDF)
# make verify-audio runs every port's audio gate (needs emulators) # make verify-audio runs every port's audio gate (needs emulators)
# make clean removes all build outputs # make clean removes all build outputs
# #
# The build references cross tools exclusively from toolchains/, set up # Cross tools come from ./install.sh, which records their paths in
# by sourcing toolchains/env.sh. Do not invoke make without sourcing # toolchains/config.mk; every makefile reads that file itself, so no
# env.sh first or builds will fail with missing tool errors. # environment setup is needed first.
REPO_DIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) REPO_DIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST))))
# Detect which toolchains are actually available. -include $(REPO_DIR)/toolchains/config.mk
HAVE_IIGS := $(shell [ -x "$(IIGS_CC)" ] && echo 1)
HAVE_AMIGA := $(shell [ -x "$(AMIGA_CC)" ] && echo 1) # A port is available when its real compiler binary is installed.
HAVE_ATARIST := $(shell [ -x "$(ST_CC)" ] && echo 1) HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),1)
HAVE_DOS := $(shell [ -x "$(DOS_CC)" ] && echo 1) HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1)
HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1)
HAVE_DOS := $(if $(wildcard $(DOS_CC)),1)
HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1)
ALL_TARGETS := ALL_TARGETS :=
ifeq ($(HAVE_IIGS),1) ifeq ($(HAVE_IIGS),1)
@ -36,12 +39,15 @@ endif
ifeq ($(HAVE_DOS),1) ifeq ($(HAVE_DOS),1)
ALL_TARGETS += dos ALL_TARGETS += dos
endif endif
ifeq ($(HAVE_X68000),1)
ALL_TARGETS += x68000
endif
.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos 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 docs
all: $(ALL_TARGETS) tools all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),) ifeq ($(ALL_TARGETS),)
@echo "No toolchains detected. Run ./toolchains/install.sh and source toolchains/env.sh." @echo "No toolchains detected. Run: ./install.sh install toolchains"
@exit 1 @exit 1
endif endif
@ -66,6 +72,9 @@ atarist:
dos: dos:
@$(MAKE) -f $(REPO_DIR)/make/dos.mk @$(MAKE) -f $(REPO_DIR)/make/dos.mk
x68000:
@$(MAKE) -f $(REPO_DIR)/make/x68000.mk
# Compile + link-check the new-port template (src/blank/blank.c) with the host # Compile + link-check the new-port template (src/blank/blank.c) with the host
# cc. No toolchain needed; keeps the template from bit-rotting as the lib grows. # cc. No toolchain needed; keeps the template from bit-rotting as the lib grows.
blank-check: blank-check:
@ -94,11 +103,12 @@ status:
@echo " amiga: $(if $(HAVE_AMIGA),yes,NO)" @echo " amiga: $(if $(HAVE_AMIGA),yes,NO)"
@echo " atarist: $(if $(HAVE_ATARIST),yes,NO)" @echo " atarist: $(if $(HAVE_ATARIST),yes,NO)"
@echo " dos: $(if $(HAVE_DOS),yes,NO)" @echo " dos: $(if $(HAVE_DOS),yes,NO)"
@echo " x68000: $(if $(HAVE_X68000),yes,NO)"
help: help:
@echo "Targets: iigs amiga atarist dos all clean status help" @echo "Targets: iigs amiga atarist dos x68000 all clean status help"
@echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template)" @echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)"
@echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)" @echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)"
@echo " docs (manual to build/docs; FORMAT=html for HTML only)" @echo " docs (manual to build/docs; FORMAT=html for HTML only)"
@echo "Source toolchains/env.sh first (blank-check needs no toolchain)." @echo "Install toolchains first: ./install.sh install toolchains"

View file

@ -17,42 +17,68 @@ See `claudeDocs/DESIGN.md` for the full 1.0 design.
## Quick start ## Quick start
JoeyLib builds on Linux x86-64, and on Windows inside WSL2. One script
installs every cross toolchain, pinned and checksum-verified, into
`toolchains/`; outside that directory it only installs ordinary packages
from your distribution (apt or dnf), and only when asked.
``` ```
git clone <repo> joeylib git clone https://forge.duensing.digital/AI_Slop/joeylib2.git joeylib
cd joeylib cd joeylib
./toolchains/install.sh ./install.sh deps toolchains --install # distribution packages (sudo)
# follow any non-free-tool placement instructions the script prints ./install.sh install toolchains # every cross toolchain
source toolchains/env.sh
make make
``` ```
This builds `libjoey.a` for every target whose toolchain is installed, The first `install` takes about an hour, almost all of it compiling the IIgs
plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`, and Amiga compilers. `./install.sh install toolchain:dos` (or any
other port) installs just one. `./install.sh list` shows every
component and whether it is current, and `./install.sh doctor`
smoke-tests what is installed. The manual's Getting Started chapter
(`docs/GettingStarted.adoc`) has the full installer reference.
No environment setup is needed: `make` reads the tool paths the installer
records in `toolchains/config.mk`. For running a cross compiler by
hand, `source toolchains/env.sh` sets the same variables.
`make` builds `libjoey.a` for every port whose toolchain is installed, plus
the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
`sertest`, `save`, `joy`, `sprite`, `audio`, `uber`, `adventure`, `sertest`, `save`, `joy`, `sprite`, `audio`, `uber`, `adventure`,
`adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception: `adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception:
`make iigs` compile-checks the library and runs the clang smoke test, and `make iigs` builds the library, and `make iigs-disk` builds its examples and
`make -f make/iigs.mk iigs-examples` builds its example binaries. The packages them onto a disk image.
Sharp X68000 is not covered at all -- see below.
Space Taxi's levels come from the original Commodore 64 game, which is not in
the repository. Without `stuff/spacetaxi/raw.bin` the build compiles the
STAXI program, skips only its data, and prints a note saying so.
## Running programs
`joey-run` starts a built program in the right emulator; the installer sets up
the emulators and their firmware:
```
./install.sh install emulators # or emulator:dos, emulator:iigs, ...
./joey-run build/dos/bin/DRAW.EXE
./joey-run --port amiga draw
./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
covers `joey-run`'s options and the private-file bundle.
## Building for a single target ## Building for a single target
``` ```
source toolchains/env.sh
make iigs make iigs
make amiga make amiga
make atarist make atarist
make dos make dos
``` make x68000
The Sharp X68000 has no top-level target: `toolchains/install.sh` does not
fetch elf2x68k and the root `Makefile` has no `x68000` rule. Stage the
elf2x68k tarball under `toolchains/x68000/m68k-xelf` and drive the fragment
directly -- it resolves its own compiler relative to the repo, so this one
does not need `env.sh`:
```
make -f make/x68000.mk # libjoey.a plus SERIAL.X, UBER.X, AUDIO.X
``` ```
Every fragment also answers to a bare `clean`, and every fragment that builds Every fragment also answers to a bare `clean`, and every fragment that builds
@ -79,8 +105,8 @@ make -f make/x68000.mk x68000-verify-golden # ~70 min under the patched MAME
make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min
``` ```
Both X68000 gates want `X68K_SCRATCH` pointing at a work dir containing Both X68000 gates run under the patched MAME and the X68000 firmware that
`x68mame/`, and a MAME built with the patches in `patches/`. `./install.sh install emulator:x68000` sets up.
## Repository layout ## Repository layout
@ -104,11 +130,14 @@ tools/songbake/ chip-tracker text notation -> JYM1 stream
tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer
tools/diff-uber-hashes UBER golden-hash comparator tools/diff-uber-hashes UBER golden-hash comparator
examples/ example programs examples/ example programs
scripts/ run / bench / verification scripts joey-run run a built program in its emulator
patches/ MAME patches the X68000 gates need scripts/ bench / verification scripts (scripts/lib: shared helpers)
tests/goldens/uber/ frozen UBER conformance hashes tests/goldens/uber/ frozen UBER conformance hashes
toolchains/ self-contained cross-build tools install.sh installs toolchains, emulators and firmware
make/ per-target Makefile fragments installer/ the installer's component recipes, patches, lists
toolchains/ everything install.sh downloads and builds (not in
git; delete it any time and re-run install.sh)
make/ per-target Makefile fragments (make/support: build helpers)
build/<plat>/ per-target build outputs build/<plat>/ per-target build outputs
``` ```

View file

@ -133,8 +133,10 @@
|=== |===
|Symptom |Likely cause |Symptom |Likely cause
|Build produces stale or missing binaries, `Error 127 (ignored)` |`make` stops with "toolchain:PORT is not installed"
|`toolchains/env.sh` was not sourced in this shell. |That port's cross toolchain is missing or incomplete. Run
`./install.sh install toolchain:PORT`; `./install.sh doctor`
smoke-tests what is installed.
|Sprites draw but the game crawls |Sprites draw but the game crawls
|The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check |The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check

View file

@ -212,8 +212,10 @@ The output extension selects the conversion:
Amiga replayer, PTPlayer, has no native song-end signal, so without this Amiga replayer, PTPlayer, has no native song-end signal, so without this
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore
`E8FF`, so an `.amod` is harmless if it reaches the wrong port. `E8FF`, so an `.amod` is harmless if it reaches the wrong port.
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the staged * *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
converter under `toolchains/iigs/ntp/`. NinjaTrackerPlus converter the installer's `ntp` component provides (part of
`toolchain:iigs`; it needs the PHP command-line interpreter, which the
installer's host packages include).
Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and
the sample data has to fit in memory alongside your game. the sample data has to fit in memory alongside your game.

View file

@ -112,28 +112,28 @@ can hide hundreds of kilobytes in subdirectories that a directory listing does
not show; rebuild from empty rather than deleting files if space seems to not show; rebuild from empty rather than deleting files if space seems to
vanish. vanish.
*Amiga.* The runner writes an `s/startup-sequence` that invokes your binary. *Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary.
=== Verifying It Actually Runs === Verifying It Actually Runs
Build success is weak evidence on retro targets. Run it: Build success is weak evidence on retro targets. Run it with `joey-run` (see
<<Running What You Built>>), on every machine:
[source,console] [source,console]
---- ----
$ scripts/run-iigs.sh MYGAME $ ./joey-run --port iigs mygame
$ scripts/run-amiga.sh MyGame $ ./joey-run --port amiga mygame
$ scripts/run-atarist.sh MYGAME $ ./joey-run --port atarist mygame
$ scripts/run-dos.sh MYGAME $ ./joey-run --port dos mygame
$ scripts/run-x68000.sh MYGAME $ ./joey-run --port x68000 mygame
---- ----
There are also headless runners that capture a screenshot and the program's log Without a window, which is what you want while iterating, run it headless and
without opening a window, which is what you want when iterating: look at the snapshot; the program's log lands next to it as usual:
[source,console] [source,console]
---- ----
$ scripts/run-atarist-headless.sh MYGAME 90 $ ./joey-run --headless --seconds 90 --snapshot mygame.png --port atarist mygame
$ scripts/run-amiga-headless.sh MyGame 90
---- ----
=== The Gates === The Gates

View file

@ -2,29 +2,47 @@
=== Setting Up the Toolchains === Setting Up the Toolchains
JoeyLib cross-compiles for five machines, and every cross toolchain lives under JoeyLib cross-compiles for five machines. It builds on Linux x86-64, and on
`toolchains/` in the repository. They are fetched and built once: Windows inside WSL2. One script installs every cross toolchain, pinned to a
known-good version and checked against a published checksum, into
`toolchains/` in the repository. The only things it installs anywhere else
are ordinary packages from your distribution (apt on Debian and Ubuntu, dnf
on Fedora), and only when you ask it to.
Two commands set up a new machine:
[source,console] [source,console]
---- ----
$ ./toolchains/install.sh $ ./install.sh deps toolchains --install
$ ./install.sh install toolchains
---- ----
Then, *in every shell session before you build anything*: The first installs the distribution packages the toolchains need; it runs
`sudo` once. The second downloads or builds every cross toolchain. The first
run takes about an hour, almost all of it compiling the Apple IIgs and Amiga
compilers. To start working sooner, install only the ports you need:
[source,console] [source,console]
---- ----
$ source toolchains/env.sh $ ./install.sh install toolchain:dos toolchain:atarist
---- ----
[WARNING] Running `install` again is cheap. Components that are already current are
==== skipped, and a component is rebuilt only when its pinned version changes.
Forgetting `source toolchains/env.sh` is the single most common build problem.
The makefiles reference the cross compilers only through the variables that No environment setup is needed afterwards. `make` reads the tool paths the
script sets, so without it a build can appear to succeed while producing stale installer records in `toolchains/config.mk`, and the scripts in
or missing binaries, and errors are reported as `Error 127 (ignored)`. If a `scripts/` load the same settings themselves. To use a cross compiler by hand
build behaves strangely, source the script and try again. in a shell, `source toolchains/env.sh` sets the same variables.
====
If a port's toolchain is missing, its build stops at once and says what to
install:
[source,console]
----
$ make amiga
make/amiga.mk:5: *** toolchain:amiga is not installed; run: ./install.sh install toolchain:amiga. Stop.
----
Check what the build can see: Check what the build can see:
@ -36,27 +54,143 @@ Toolchain availability:
amiga: yes amiga: yes
atarist: yes atarist: yes
dos: yes dos: yes
x68000: yes
---- ----
==== Installer reference
`./install.sh [options] COMMAND [TARGETS...]`, run from the top of the
repository.
[cols="1,3"]
|===
| Command | What it does
| `list`
| Every component, the groups it belongs to, and whether it is current,
outdated or missing.
| `install TARGETS`
| Installs or updates the targets and everything they depend on, and
smoke-tests each one it installs. A failure does not stop the rest; the
failures are listed at the end.
| `deps TARGETS`
| Lists the distribution packages the targets need and which are missing.
With `--install`, installs the missing ones.
| `doctor`
| Checks the host, then smoke-tests every installed component (each cross
compiler compiles a file, and every prebuilt program must find its shared
libraries, for example). It reports anything out of date, any component
whose files have gone, and any part of a port you installed only partly,
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.
| `bundle FILE`
| Packs those files, as present on this machine, into `FILE` (a `.tar.gz`)
for a project to host.
|===
A target is a component name from `list`, or a group:
[cols="1,3"]
|===
| Target | Means
| `toolchain:PORT`
| Everything needed to build for one port: `iigs`, `amiga`, `atarist`, `dos`
or `x68000`.
| `emulator:PORT`
| Everything needed to run that port's programs: the emulator and its
firmware.
| `PORT`
| Both of the above.
| `toolchains`
| Every port's toolchain.
| `emulators`
| Every port's emulator.
| `all`
| Everything except optional components (such as `staxiData`, Space Taxi's
level source), which are installed by name.
|===
[cols="1,3"]
|===
| Option | Effect
| `-y`, `--yes`
| Install missing distribution packages without stopping to ask.
| `--install`
| With `deps`: install the missing packages.
| `--force`
| Reinstall components even when they are current.
| `--jobs N`
| Parallel build jobs. The default is every core.
| `--keep-build`
| Keep the scratch build trees (normally deleted after a successful install).
| `--llvm816-dir PATH`
| Use an existing checkout of llvm816, the IIgs compiler, instead of cloning
it. For working on the compiler and JoeyLib together. The setting is
remembered; `--llvm816-dir clone` goes back to a managed clone.
|===
Everything the installer creates stays inside `toolchains/`:
[cols="1,3"]
|===
| Path | Contents
| `toolchains/opt/`
| The installed components, one directory each.
| `toolchains/firmware/`
| ROM and system-disk images for the emulators.
| `toolchains/cache/`
| Downloads, kept and re-verified by checksum on reuse, and scratch build
trees.
| `toolchains/state/`
| Which version of each component is installed, and settings such as
`--llvm816-dir`.
| `toolchains/config.mk`, `toolchains/env.sh`
| The tool paths, for `make` and for shells.
|===
Nothing in `toolchains/` is part of the repository: delete the whole folder
to return the machine to a clean state, and `./install.sh` re-creates it. The
installer itself lives in `install.sh` and `installer/` (its component
recipes, patches and package lists), and the build helpers the makefiles call
live in `make/support/`.
=== Building === Building
[source,console] [source,console]
---- ----
$ make # every target whose toolchain is present $ make # every target whose toolchain is installed
$ make iigs # Apple IIgs only $ make iigs # Apple IIgs only
$ make amiga # Commodore Amiga only $ make amiga # Commodore Amiga only
$ make atarist # Atari ST only $ make atarist # Atari ST only
$ make dos # MS-DOS only $ make dos # MS-DOS only
$ make x68000 # Sharp X68000 only
$ make clean # remove all build outputs $ make clean # remove all build outputs
---- ----
The X68000 port has its own makefile:
[source,console]
----
$ make -f make/x68000.mk
----
Built programs land under `build/<platform>/bin/`. The IIgs additionally Built programs land under `build/<platform>/bin/`. The IIgs additionally
produces a bootable disk image: produces a bootable disk image:
@ -65,6 +199,15 @@ produces a bootable disk image:
$ make iigs-disk # build/iigs/bin/joey.2mg $ make iigs-disk # build/iigs/bin/joey.2mg
---- ----
Space Taxi's levels come from the original Commodore 64 game, which is not
part of the repository. Without `stuff/spacetaxi/raw.bin` the build still
compiles the STAXI program, skips only its data files, and says so:
[source,console]
----
note: Space Taxi data skipped: stuff/spacetaxi/raw.bin is not present
----
[TIP] [TIP]
==== ====
Never judge a retro build by its exit status alone. Check that the file you Never judge a retro build by its exit status alone. Check that the file you
@ -165,19 +308,107 @@ back buffer called the *stage*; nothing reaches the display until you present.
=== Running What You Built === Running What You Built
Helper scripts boot each target in an emulator: `joey-run`, at the top of the repository, starts a built program in the right
emulator. The emulators, and the firmware they boot, come from the installer:
[source,console] [source,console]
---- ----
$ scripts/run-iigs.sh DRAW $ ./install.sh install emulators # every machine
$ scripts/run-amiga.sh Draw $ ./install.sh install emulator:dos # or just one
$ scripts/run-atarist.sh DRAW
$ scripts/run-dos.sh DRAW
$ scripts/run-x68000.sh DRAW
---- ----
Note the capitalisation differences: the IIgs, ST and DOS use uppercase names MAME (for the IIgs and X68000) is always built from source, because the test
(ProDOS, GEMDOS and DOS 8.3 conventions), while the Amiga keeps mixed case. gates need JoeyLib's patches to it. DOSBox Staging is upstream's own Linux
release. Hatari and FS-UAE are distribution packages on Debian and Ubuntu;
Fedora does not ship the versions the gates expect, so there the installer
builds both from source.
Point `joey-run` at a program, or name an example together with `--port`:
[source,console]
----
$ ./joey-run build/dos/bin/DRAW.EXE
$ ./joey-run build/amiga/bin/Draw
$ ./joey-run --port atarist draw
$ ./joey-run build/iigs/bin/STAXI.2mg
----
It recognises the machine from the file itself. Interactive runs use the
program's own folder or disk image, so saves and logs persist from one run to
the next. Nothing is written to your home directory.
[cols="1,3"]
|===
| Option | Effect
| `--port PORT`
| The machine, when naming an example rather than a file.
| `--data DIR`
| Copy `DIR`'s files into the program's `DATA` folder for this run, for
example an AGI game's files.
| `--headless`
| No window and no sound, for scripts and continuous integration. Stops after
60 seconds unless `--seconds` says otherwise.
| `--seconds N`
| Stop after `N` seconds.
| `--snapshot FILE`
| Save a PNG of the screen when the run stops.
| `--machine rom01`
| Apple IIgs: the ROM 01 machine instead of ROM 03.
| `--ram SIZE`
| Apple IIgs: MAME's memory size. The default is `4M`; AGI wants `8M`.
| `--debug`
| Apple IIgs and X68000: start MAME's debugger.
| `-- ARGS`
| Pass `ARGS` to the program. DOS only: the other machines' autostart cannot
pass arguments.
|===
A headless run with a snapshot is the quickest way to check a change:
[source,console]
----
$ ./joey-run --headless --seconds 30 --snapshot draw.png --port dos draw
----
==== The Apple IIgs
The IIgs runs programs from disk images. `make -f make/iigs.mk iigs-game-disk
GAME=DRAW` builds a bootable one, `build/iigs/bin/DRAW.2mg`, which is also how
a game ships. Given a data disk instead, such as `joey.2mg`, `joey-run` boots
GS/OS beside it and you open the program in the Finder. Given a bare program,
`build/iigs/bin/DRAW`, it builds both disks itself, which needs the IIgs
toolchain.
==== Firmware that is not downloaded
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,
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:
[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.
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
a machine that has them, and setting `JOEY_BUNDLE_URL` to where it is hosted,
in `installer/bundle.conf`, makes every `install` fetch it automatically.
=== The Example Programs === The Example Programs

View file

@ -25,7 +25,7 @@ Same loop every time, so it stays cheap to iterate:
`stLevel.c`, `spacetaxi.c`). `stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi` 6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
first, then once stable also amiga / atarist / iigs. first, then once stable also amiga / atarist / iigs.
7. **User test** -- run `scripts/run-dos.sh staxi`, report 7. **User test** -- run `./joey-run --port dos staxi`, report
behavior. Do not start the next phase before this. behavior. Do not start the next phase before this.
## Phase 0 -- foundation (DONE) ## Phase 0 -- foundation (DONE)

View file

@ -82,9 +82,9 @@ plus the inline scripts run during verification sessions.
## TRACE INFRASTRUCTURE ## TRACE INFRASTRUCTURE
- `stuff/spacetaxi/trace.py`: instrumented wrapper around `cpu6502.Cpu6502`. `Tracer.from_dump('raw.bin').call(addr)` runs a subroutine and snapshots memory + JSR chain. - `stuff/spacetaxi/trace.py`: instrumented wrapper around `cpu6502.Cpu6502`. `Tracer.from_dump('raw.bin').call(addr)` runs a subroutine and snapshots memory + JSR chain.
- `stuff/spacetaxi/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths). - `examples/spacetaxi/assets/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths).
- `stuff/spacetaxi/dumpScenes.py` + `dumpAllLevels.py`: pre-existing scene-decompress drivers. - `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/dumpAllLevels.py`: pre-existing scene-decompress drivers.
- `stuff/spacetaxi/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator. - `examples/spacetaxi/assets/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator.
## METHODOLOGY (for future verification sessions) ## METHODOLOGY (for future verification sessions)

View file

@ -0,0 +1,560 @@
#!/usr/bin/env python3
# Minimal 6502 CPU emulator -- just enough to run the Space Taxi
# level decompressor at $6283 against a captured 64KB RAM dump.
#
# Scope: implements the opcodes used by $6283 / $9656 / $964A / $96A3
# and their helpers. Not a general-purpose 6502 emulator; it has no
# decimal mode, no BRK handling, no NMI/IRQ -- the routine runs with
# SEI in effect and never returns control.
#
# Flags tracked: N Z C. V (overflow) is computed for ADC/SBC since
# those instructions set it, even if no branch tests it. D (decimal)
# is ignored.
class Cpu6502:
def __init__(self, ram):
# ram is a bytearray of 65536 bytes. Caller pre-loaded the dump.
self.ram = ram
self.a = 0
self.x = 0
self.y = 0
self.sp = 0xFF
self.pc = 0
self.n = self.z = self.c = self.v = False
# Stop tracking when RTS unwinds to this depth (set by run()).
self._depth_floor = 0
# ---- memory ----
def read8(self, addr):
return self.ram[addr & 0xFFFF]
def write8(self, addr, val):
self.ram[addr & 0xFFFF] = val & 0xFF
def read16(self, addr):
return self.ram[addr & 0xFFFF] | (self.ram[(addr + 1) & 0xFFFF] << 8)
# ---- stack ----
def push8(self, val):
self.ram[0x100 + self.sp] = val & 0xFF
self.sp = (self.sp - 1) & 0xFF
def pop8(self):
self.sp = (self.sp + 1) & 0xFF
return self.ram[0x100 + self.sp]
def push16(self, val):
self.push8((val >> 8) & 0xFF)
self.push8(val & 0xFF)
def pop16(self):
lo = self.pop8()
hi = self.pop8()
return (hi << 8) | lo
# ---- flag helpers ----
def set_nz(self, val):
val &= 0xFF
self.z = (val == 0)
self.n = (val & 0x80) != 0
return val
def pack_p(self):
# B and U are usually set when pushed by PHP/BRK.
p = 0x20
if self.n: p |= 0x80
if self.v: p |= 0x40
if self.z: p |= 0x02
if self.c: p |= 0x01
return p | 0x10 # B = 1 when pushed by PHP
def unpack_p(self, p):
self.n = (p & 0x80) != 0
self.v = (p & 0x40) != 0
self.z = (p & 0x02) != 0
self.c = (p & 0x01) != 0
# ---- addressing helpers (return effective address) ----
def addr_imm(self):
a = self.pc; self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_zp(self):
a = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_zpx(self):
a = (self.read8(self.pc) + self.x) & 0xFF; self.pc = (self.pc + 1) & 0xFFFF; return a
def addr_abs(self):
a = self.read16(self.pc); self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_absx(self):
a = (self.read16(self.pc) + self.x) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_absy(self):
a = (self.read16(self.pc) + self.y) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a
def addr_indy(self):
zp = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF
base = self.ram[zp] | (self.ram[(zp + 1) & 0xFF] << 8)
return (base + self.y) & 0xFFFF
# ---- branch helper ----
def branch(self, cond):
off = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF
if off >= 0x80: off -= 0x100
if cond:
self.pc = (self.pc + off) & 0xFFFF
# ---- step ----
def step(self):
op = self.read8(self.pc)
self.pc = (self.pc + 1) & 0xFFFF
f = _DISPATCH.get(op)
if f is None:
raise NotImplementedError(f"opcode ${op:02X} at PC ${(self.pc-1)&0xFFFF:04X}")
f(self)
def call(self, target, max_steps=10_000_000):
"""Call subroutine at `target` as if via JSR. Returns when the
matching RTS pops the stack back below the original SP. Hard
cap on steps to avoid runaway."""
# Push a sentinel return address. When RTS pops to this, stop.
# Use $FFFF as sentinel: subsequent step would attempt to fetch
# from $FFFF; we detect via depth floor instead.
original_sp = self.sp
self.push16(0xFFFE) # RTS will pop this, then step will land at $FFFF
self.pc = target & 0xFFFF
for _ in range(max_steps):
if self.pc == 0xFFFF:
return
self.step()
raise RuntimeError(f"6502 call exceeded {max_steps} steps; PC=${self.pc:04X}")
# ---- opcode implementations ----
# Each takes the cpu and reads remaining operand bytes via cpu.addr_*.
def _NOP(cpu): pass
def _SEI(cpu): pass # we don't model interrupts
def _CLD(cpu): pass # no decimal mode
def _SEC(cpu): cpu.c = True
def _CLC(cpu): cpu.c = False
def _PHP(cpu): cpu.push8(cpu.pack_p())
def _PLP(cpu): cpu.unpack_p(cpu.pop8())
def _PHA(cpu): cpu.push8(cpu.a)
def _PLA(cpu): cpu.a = cpu.set_nz(cpu.pop8())
def _TXA(cpu): cpu.a = cpu.set_nz(cpu.x)
def _TAX(cpu): cpu.x = cpu.set_nz(cpu.a)
def _TYA(cpu): cpu.a = cpu.set_nz(cpu.y)
def _TAY(cpu): cpu.y = cpu.set_nz(cpu.a)
def _INX(cpu): cpu.x = cpu.set_nz((cpu.x + 1) & 0xFF)
def _INY(cpu): cpu.y = cpu.set_nz((cpu.y + 1) & 0xFF)
def _DEX(cpu): cpu.x = cpu.set_nz((cpu.x - 1) & 0xFF)
def _DEY(cpu): cpu.y = cpu.set_nz((cpu.y - 1) & 0xFF)
def _LDA_imm(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDA_zp(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _LDA_zpx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zpx()))
def _LDA_abs(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDA_absx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absx()))
def _LDA_absy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absy()))
def _LDA_indy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_indy()))
def _LDX_imm(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDX_abs(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDX_absy(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_absy()))
def _LDX_zp(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _LDY_imm(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_imm()))
def _LDY_abs(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_abs()))
def _LDY_absx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_absx()))
def _LDY_zp(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zp()))
def _STA_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.a)
def _STA_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.a)
def _STA_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.a)
def _STA_absx(cpu): cpu.write8(cpu.addr_absx(), cpu.a)
def _STA_absy(cpu): cpu.write8(cpu.addr_absy(), cpu.a)
def _STA_indy(cpu): cpu.write8(cpu.addr_indy(), cpu.a)
def _STX_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.x)
def _STX_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.x)
def _STY_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.y)
def _STY_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.y)
def _AND_imm(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_imm()))
def _AND_zp(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_zp()))
def _AND_abs(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_abs()))
def _ORA_imm(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_imm()))
def _ORA_zp(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zp()))
def _EOR_imm(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_imm()))
def _EOR_abs(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_abs()))
def _adc(cpu, m):
r = cpu.a + m + (1 if cpu.c else 0)
cpu.c = r > 0xFF
cpu.v = ((cpu.a ^ r) & (m ^ r) & 0x80) != 0
cpu.a = cpu.set_nz(r & 0xFF)
def _ADC_imm(cpu): _adc(cpu, cpu.read8(cpu.addr_imm()))
def _ADC_zp(cpu): _adc(cpu, cpu.read8(cpu.addr_zp()))
def _ADC_abs(cpu): _adc(cpu, cpu.read8(cpu.addr_abs()))
def _ADC_absy(cpu): _adc(cpu, cpu.read8(cpu.addr_absy()))
def _sbc(cpu, m):
_adc(cpu, m ^ 0xFF)
def _SBC_imm(cpu): _sbc(cpu, cpu.read8(cpu.addr_imm()))
def _SBC_zp(cpu): _sbc(cpu, cpu.read8(cpu.addr_zp()))
def _SBC_abs(cpu): _sbc(cpu, cpu.read8(cpu.addr_abs()))
def _SBC_absx(cpu): _sbc(cpu, cpu.read8(cpu.addr_absx()))
def _SBC_absy(cpu): _sbc(cpu, cpu.read8(cpu.addr_absy()))
def _AND_absx(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absx()))
def _AND_absy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absy()))
def _AND_indy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_indy()))
def _ORA_absx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_absx()))
def _ORA_abs(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_abs()))
def _ORA_zpx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zpx()))
def _ORA_indy(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_indy()))
def _EOR_zp(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zp()))
def _EOR_zpx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zpx()))
def _EOR_absx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absx()))
def _EOR_absy(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absy()))
def _ADC_zpx(cpu): _adc(cpu, cpu.read8(cpu.addr_zpx()))
def _ADC_absx(cpu): _adc(cpu, cpu.read8(cpu.addr_absx()))
def _CMP_absx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absx()))
def _CMP_absy(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absy()))
def _CMP_zpx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zpx()))
def _JMP_ind(cpu):
addr = cpu.addr_abs()
cpu.pc = cpu.read8(addr) | (cpu.read8((addr & 0xFF00) | ((addr + 1) & 0xFF)) << 8)
def _BIT_zp(cpu):
v = cpu.read8(cpu.addr_zp())
cpu.z = (cpu.a & v) == 0
cpu.n = (v & 0x80) != 0
cpu.v = (v & 0x40) != 0
def _BIT_abs(cpu):
v = cpu.read8(cpu.addr_abs())
cpu.z = (cpu.a & v) == 0
cpu.n = (v & 0x80) != 0
cpu.v = (v & 0x40) != 0
def _ASL_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _ASL_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _LSR_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_abs(cpu):
addr = cpu.addr_abs()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _STA_zpx(cpu_): cpu_.write8(cpu_.addr_zpx(), cpu_.a)
def _TSX(cpu): cpu.x = cpu.set_nz(cpu.sp)
def _TXS(cpu): cpu.sp = cpu.x
def _CLI(cpu): pass
def _CLV(cpu): cpu.v = False
def _SED(cpu): pass # decimal mode ignored
def _STX_zpy(cpu):
a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF
cpu.write8(a, cpu.x)
def _STY_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.y)
def _LDX_zpy(cpu):
a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF
cpu.x = cpu.set_nz(cpu.read8(a))
def _LDY_zpx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zpx()))
def _cmp_with(cpu, lhs, m):
r = (lhs - m) & 0x1FF
cpu.c = lhs >= m
cpu.set_nz(r & 0xFF)
def _CMP_imm(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_imm()))
def _CMP_zp(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zp()))
def _CMP_abs(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_abs()))
def _CPX_imm(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_imm()))
def _CPX_abs(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_abs()))
def _CPY_imm(cpu): _cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_imm()))
def _ASL_a(cpu):
cpu.c = (cpu.a & 0x80) != 0
cpu.a = cpu.set_nz((cpu.a << 1) & 0xFF)
def _LSR_a(cpu):
cpu.c = (cpu.a & 0x01) != 0
cpu.a = cpu.set_nz(cpu.a >> 1)
def _LSR_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_a(cpu):
new_c = (cpu.a & 0x80) != 0
cpu.a = cpu.set_nz(((cpu.a << 1) | (1 if cpu.c else 0)) & 0xFF)
cpu.c = new_c
def _ROL_zp(cpu):
addr = cpu.addr_zp()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_a(cpu):
new_c = (cpu.a & 0x01) != 0
cpu.a = cpu.set_nz(((cpu.a >> 1) | (0x80 if cpu.c else 0)) & 0xFF)
cpu.c = new_c
def _INC_abs(cpu):
addr = cpu.addr_abs()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_absx(cpu):
addr = cpu.addr_absx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_zp(cpu):
addr = cpu.addr_zp()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _INC_zpx(cpu):
addr = cpu.addr_zpx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF))
def _DEC_abs(cpu):
addr = cpu.addr_abs()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_absx(cpu):
addr = cpu.addr_absx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_zp(cpu):
addr = cpu.addr_zp()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _DEC_zpx(cpu):
addr = cpu.addr_zpx()
cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF))
def _ASL_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _ASL_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
cpu.c = (v & 0x80) != 0
cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF))
def _LSR_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _LSR_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
cpu.c = (v & 0x01) != 0
cpu.write8(addr, cpu.set_nz(v >> 1))
def _ROL_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROL_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
new_c = (v & 0x80) != 0
v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_zpx(cpu):
addr = cpu.addr_zpx()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _ROR_absx(cpu):
addr = cpu.addr_absx()
v = cpu.read8(addr)
new_c = (v & 0x01) != 0
v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF
cpu.write8(addr, cpu.set_nz(v))
cpu.c = new_c
def _LDA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.read8(addr))
def _STA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.write8(addr, cpu.a)
def _AND_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a & cpu.read8(addr))
def _ORA_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a | cpu.read8(addr))
def _EOR_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(addr))
def _ADC_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_adc(cpu, cpu.read8(addr))
def _SBC_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_sbc(cpu, cpu.read8(addr))
def _CMP_indx(cpu):
zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF
addr_ptr = (zp + cpu.x) & 0xFF
addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8)
_cmp_with(cpu, cpu.a, cpu.read8(addr))
def _CPX_zp(cpu):
_cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_zp()))
def _CPY_zp(cpu):
_cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_zp()))
def _BPL(cpu): cpu.branch(not cpu.n)
def _BMI(cpu): cpu.branch(cpu.n)
def _BNE(cpu): cpu.branch(not cpu.z)
def _BEQ(cpu): cpu.branch(cpu.z)
def _BCC(cpu): cpu.branch(not cpu.c)
def _BCS(cpu): cpu.branch(cpu.c)
def _BVC(cpu): cpu.branch(not cpu.v)
def _BVS(cpu): cpu.branch(cpu.v)
def _JMP_abs(cpu): cpu.pc = cpu.addr_abs()
def _JSR(cpu):
target = cpu.addr_abs()
cpu.push16((cpu.pc - 1) & 0xFFFF)
cpu.pc = target
def _RTS(cpu):
cpu.pc = (cpu.pop16() + 1) & 0xFFFF
# Build dispatch table
_DISPATCH = {
0x00: _NOP, # treat BRK as NOP (we never expect it)
0x18: _CLC, 0x38: _SEC, 0x58: _NOP, 0x78: _SEI, 0xD8: _CLD,
0x08: _PHP, 0x28: _PLP, 0x48: _PHA, 0x68: _PLA,
0x8A: _TXA, 0xAA: _TAX, 0x98: _TYA, 0xA8: _TAY,
0xE8: _INX, 0xC8: _INY, 0xCA: _DEX, 0x88: _DEY,
0xA9: _LDA_imm, 0xA5: _LDA_zp, 0xB5: _LDA_zpx, 0xAD: _LDA_abs,
0xBD: _LDA_absx, 0xB9: _LDA_absy, 0xB1: _LDA_indy,
0xA2: _LDX_imm, 0xAE: _LDX_abs, 0xBE: _LDX_absy, 0xA6: _LDX_zp,
0xA0: _LDY_imm, 0xAC: _LDY_abs, 0xBC: _LDY_absx, 0xA4: _LDY_zp,
0x85: _STA_zp, 0x95: _STA_zpx, 0x8D: _STA_abs,
0x9D: _STA_absx, 0x99: _STA_absy, 0x91: _STA_indy,
0x86: _STX_zp, 0x8E: _STX_abs,
0x84: _STY_zp, 0x8C: _STY_abs,
0x29: _AND_imm, 0x25: _AND_zp, 0x2D: _AND_abs,
0x3D: _AND_absx, 0x39: _AND_absy, 0x31: _AND_indy,
0x09: _ORA_imm, 0x05: _ORA_zp, 0x0D: _ORA_abs, 0x15: _ORA_zpx,
0x1D: _ORA_absx, 0x11: _ORA_indy,
0x49: _EOR_imm, 0x4D: _EOR_abs, 0x45: _EOR_zp, 0x55: _EOR_zpx,
0x5D: _EOR_absx, 0x59: _EOR_absy,
0x69: _ADC_imm, 0x65: _ADC_zp, 0x6D: _ADC_abs, 0x79: _ADC_absy,
0x75: _ADC_zpx, 0x7D: _ADC_absx,
0xE9: _SBC_imm, 0xE5: _SBC_zp, 0xED: _SBC_abs, 0xFD: _SBC_absx, 0xF9: _SBC_absy,
0xC9: _CMP_imm, 0xC5: _CMP_zp, 0xCD: _CMP_abs, 0xD5: _CMP_zpx,
0xDD: _CMP_absx, 0xD9: _CMP_absy,
0xE0: _CPX_imm, 0xEC: _CPX_abs,
0xC0: _CPY_imm,
0x24: _BIT_zp, 0x2C: _BIT_abs,
0x0A: _ASL_a, 0x06: _ASL_zp, 0x0E: _ASL_abs,
0x4A: _LSR_a, 0x46: _LSR_zp, 0x4E: _LSR_abs,
0x2A: _ROL_a, 0x26: _ROL_zp, 0x2E: _ROL_abs,
0x6A: _ROR_a, 0x66: _ROR_zp, 0x6E: _ROR_abs,
0xEE: _INC_abs, 0xFE: _INC_absx, 0xE6: _INC_zp, 0xF6: _INC_zpx,
0xCE: _DEC_abs, 0xDE: _DEC_absx, 0xC6: _DEC_zp, 0xD6: _DEC_zpx,
0x16: _ASL_zpx, 0x1E: _ASL_absx,
0x56: _LSR_zpx, 0x5E: _LSR_absx,
0x36: _ROL_zpx, 0x3E: _ROL_absx,
0x76: _ROR_zpx, 0x7E: _ROR_absx,
0xA1: _LDA_indx, 0x81: _STA_indx, 0x21: _AND_indx,
0x01: _ORA_indx, 0x41: _EOR_indx,
0x61: _ADC_indx, 0xE1: _SBC_indx, 0xC1: _CMP_indx,
0xE4: _CPX_zp, 0xC4: _CPY_zp,
0x6C: _JMP_ind,
0x9A: _TXS, 0xBA: _TSX,
0x58: _CLI, 0xB8: _CLV, 0xF8: _SED,
0x96: _STX_zpy, 0x94: _STY_zpx,
0xB6: _LDX_zpy, 0xB4: _LDY_zpx,
0x10: _BPL, 0x30: _BMI, 0xD0: _BNE, 0xF0: _BEQ,
0x90: _BCC, 0xB0: _BCS, 0x50: _BVC, 0x70: _BVS,
0x4C: _JMP_abs, 0x20: _JSR, 0x60: _RTS,
0xEA: _NOP,
}

View file

@ -8,7 +8,7 @@ no good either -- it captures the banked-IN view, so $A000-$BFFF comes
back as BASIC ROM and 13 of the 15 utterances are missing. Every back as BASIC ROM and 13 of the 15 utterances are missing. Every
mem0000*.bin in this tree has that problem. mem0000*.bin in this tree has that problem.
The way out needs no emulator install: stuff/spacetaxi/cpu6502.py is a The way out needs no emulator install: cpu6502.py (next to this script) is a
plain 6502 core over a flat 64K bytearray with no banking and no I/O, plain 6502 core over a flat 64K bytearray with no banking and no I/O,
which is all a depacker needs. Load the .prg, run from the BASIC SYS which is all a depacker needs. Load the .prg, run from the BASIC SYS
entry, and the depacked image IS the RAM -- $A000-$BFFF included. entry, and the depacked image IS the RAM -- $A000-$BFFF included.
@ -41,11 +41,8 @@ def main():
prgPath = sys.argv[1] if len(sys.argv) > 1 else os.path.join(STUFF, "extract", "SPACE TAXI+.prg") prgPath = sys.argv[1] if len(sys.argv) > 1 else os.path.join(STUFF, "extract", "SPACE TAXI+.prg")
outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin") outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin")
sys.path.insert(0, STUFF) sys.path.insert(0, HERE)
try: from cpu6502 import Cpu6502
from cpu6502 import Cpu6502
except ImportError:
sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF)
prg = open(prgPath, "rb").read() prg = open(prgPath, "rb").read()
load = prg[0] | (prg[1] << 8) load = prg[0] | (prg[1] << 8)

View file

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# Drive the captured Space Taxi RAM through the 6502 emulator to
# decompress every scene the game knows how to load.
#
# Approach:
# 1. Load a VICE memory dump as flat 64KB RAM. The dump captured at
# runtime contains the decompressor at $9656, the scene pointer
# table at $9600, and (we hope) the compressed data the pointers
# reach into.
# 2. Sanity check: run scene ID 1 (the captured live level) through
# $6283 and compare the resulting screen buffer at $7530-$7929
# with the live screen RAM at $0400-$07E7 already in the dump.
# If they match, the emulator + dump together can reproduce any
# scene. If not, we know something's missing (likely hidden RAM
# under ROMs that VICE didn't capture).
# 3. If sanity passes: iterate scene IDs 0..23 (lettered levels)
# plus known special IDs (title=$24, level-complete=$19) and
# dump each scene's screen + color buffer.
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
def load_dump(path):
with open(path, 'rb') as f:
raw = f.read()
if len(raw) == 0x10000:
return bytearray(raw)
if len(raw) == 0x10002:
# 2-byte LE load-addr header
return bytearray(raw[2:])
raise SystemExit(f'{path}: unexpected dump size {len(raw)}')
# Memory map landmarks (from MECHANICS.md):
ADDR_DECOMP_ENTRY = 0x6283 # bank-switch wrapper around $9656
SCREEN_BUF_BASE = 0x7530 # decompressed screen RAM target
SCREEN_BUF_SIZE = 0x400 # 4 pages = 1024 bytes
COLOR_BUF_BASE = 0x7918 # decompressed color RAM target
COLOR_BUF_SIZE = 0x400
def run_scene(ram, scene_id):
"""Run $6283(X=scene_id) on a fresh copy of `ram`. Returns a tuple
(screen_buf, color_buf, final_ram) so the caller can compare or
examine state."""
work = bytearray(ram) # copy so callers can keep the original
cpu = Cpu6502(work)
cpu.a = scene_id
cpu.x = scene_id
cpu.y = 0
cpu.call(ADDR_DECOMP_ENTRY)
screen = bytes(work[SCREEN_BUF_BASE:SCREEN_BUF_BASE + SCREEN_BUF_SIZE])
color = bytes(work[COLOR_BUF_BASE:COLOR_BUF_BASE + COLOR_BUF_SIZE])
return screen, color, work
def compare_screen(decomp_screen, live_screen, label):
"""Compare 1024-byte decompressed screen vs live screen RAM.
Only the first 1000 bytes are visible (40x25). Bytes 1000..1023
are unused tail / overlap with color RAM, so we ignore them."""
a = decomp_screen[:1000]
b = live_screen[:1000]
if a == b:
print(f' {label}: MATCH')
return True
# Show how many cells differ and a sample
diffs = [(i, a[i], b[i]) for i in range(1000) if a[i] != b[i]]
print(f' {label}: {len(diffs)} / 1000 cells differ')
for i, av, bv in diffs[:6]:
row, col = divmod(i, 40)
print(f' cell ({col:2d},{row:2d}): decomp=${av:02X} live=${bv:02X}')
return False
def main():
if len(sys.argv) < 2:
print('usage: dumpScenes.py <dump-path> [scene-id]', file=sys.stderr)
sys.exit(2)
ram = load_dump(sys.argv[1])
# Figure out what the captured state is, then pick the scene to
# test against. $7215 = current level number (0..23 = A..X),
# but the title screen / specials don't update it -- the
# most-recent JSR $6283 used scene $24 (title) or $19 (other
# special). Default to the level-number scene, but allow override.
state_level = ram[0x7215]
if len(sys.argv) >= 3:
scene = int(sys.argv[2], 0)
else:
# Heuristic: if state sentinel $07A6 says we're on the title,
# the live screen matches scene 36 ($24). Otherwise we're
# mid-gameplay and the live screen matches scene = level.
# Title state in this game: top-left tile is the inner frame
# corner $5D / $5C. For now just try scene 36 then scene N.
scene = 36
print(f'=== Sanity test: scene {scene} (${scene:02X}) vs live screen RAM ===')
print(f' captured state: $7215={state_level} $07A6=${ram[0x07A6]:02X}')
try:
scr, col, _ = run_scene(ram, scene)
except Exception as e:
print(f' emulator raised: {type(e).__name__}: {e}')
return
live_scr = bytes(ram[0x0400:0x0400 + 1024])
live_col = bytes(b & 0x0F for b in ram[0xD800:0xD800 + 1024])
compare_screen(scr, live_scr, 'screen')
masked_col = bytes(b & 0x0F for b in col)
compare_screen(masked_col, live_col, 'color ')
if __name__ == '__main__':
main()

View file

@ -82,7 +82,7 @@ without changes.
3. Author each level layout in a text editor or grid tool, save as a 3. Author each level layout in a text editor or grid tool, save as a
.dat per this spec. The helper `examples/spacetaxi/mkstlevel` .dat per this spec. The helper `examples/spacetaxi/mkstlevel`
converts from a human-readable text grid to .dat. The 24 canonical converts from a human-readable text grid to .dat. The 24 canonical
C64 levels are emitted by `stuff/spacetaxi/romToLevel.py` directly C64 levels are emitted by `examples/spacetaxi/assets/romToLevel.py` directly
from a raw VICE dump of the original .prg. from a raw VICE dump of the original .prg.
## Naming ## Naming

View file

@ -0,0 +1,205 @@
#!/usr/bin/env python3
# Convert ROM-extracted Space Taxi scenes directly into the runtime
# STL1 binary format (see examples/spacetaxi/stLevel.c). Uses the
# 6502 emulator + raw RAM dump to decompress each scene, then writes
# screen + color RAM bytes straight into the .dat layout the JoeyLib
# port loads at runtime.
#
# Replaces the .txt -> mkstlevel pipeline for canonical levels. The C
# mkstlevel tool stays in tree for hand-authored .txt levels.
#
# Usage:
# romToLevel.py <raw-dump> <out-dir> # all 24 levels
# romToLevel.py <raw-dump> <out-dir> <scene_id> # one specific
#
# Writes:
# <out-dir>/level01.dat ... level24.dat (scene 0 = "A" = level01)
# <out-dir>/tbank0.png (charset extracted from $2800)
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
from dumpScenes import load_dump, run_scene
# STL2 binary layout (see examples/spacetaxi/stLevel.c):
# "STL2"
# nameLen + name
# tileBank, music, bgColor, borderColor, taxiSpawnX, taxiSpawnY
# xAccel, yAccel, xGrav (int8), yGrav (int8)
# bgColor1, bgColor2, bgColor3, spriteMc0, spriteMc1, sprite0Color, sprite1Color
# padCount + padCount * (letter, tileX, tileY, tileW)
# fareCount + fareCount * (spawnPad, destPad)
# tilemap[40 * 25]
# colormap[40 * 25]
ST_TILEMAP_W = 40
ST_PLAYFIELD_ROWS = 25
ST_TILE_PX = 8
# VIC-II sprite-register origins: sprite-X $18 (24) is display column 0,
# sprite-Y $32 (50) is the first visible raster line. All in-game pad/taxi
# positions are stored in this sprite-register space, so tile conversion
# subtracts these origins. PAD_SURFACE_ORIGIN folds in the ~14px landing-
# gear offset (50-14=36) so a pad's row byte maps to its contact-surface
# tile row rather than the sprite-top row.
VIC_X_ORIGIN = 24
VIC_Y_ORIGIN = 50
PAD_SURFACE_ORIGIN = 36
LEVEL_LETTERS = [chr(ord('A') + i) for i in range(24)]
def write_level(path, level_index, after, screen, colormap, sprite_blocks):
"""Write an STL4 level .dat straight from post-decompression RAM.
Everything stays in the C64's own units so the runtime can mirror the
original routines byte for byte:
name[22] $7D78-$7D8D level name (screen codes, ASCII-compatible)
header[9] $7D00-$7D08 VIC colours: border, bg, bg1-3, mc0, mc1,
sprite-0, sprite-1
padCount $7D5A pads in the table
specialPad $7D09 fare pads (== padCount, or padCount-1 when
the last pad is the fuel station)
fuelRate $7D60 ticks of flight per half fuel cell
spawn[5] $7D5B-$7D5F X frac, X lo, X hi, Y frac, Y hi (sprite coords)
templates[8] $7D8F-$7D96 Y accel, X accel, Y gravity, X gravity
(16-bit little-endian each)
pads[10][8] $7D0A-$7D59 raw slots: x1 hi/lo, x2 hi/lo, row,
passenger X frac, passenger X col, unused
tilemap[1000] screen RAM (all 25 rows, the HUD rows included)
colormap[1000] colour RAM low nibbles
levelIndex 0..23 = A..X, 24 = title
spriteCount N, then N x (ptr, 63 bytes): the per-level sprite
bitmaps the level's extra scene loads at $2000+
hookData[616] $7D98-$7FFF: the level's hook code and tables
"""
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
assert len(screen) == cells, f'screen is {len(screen)}, need {cells}'
assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}'
with open(path, 'wb') as fp:
fp.write(b'STL4')
fp.write(bytes(after[0x7D78:0x7D78 + 22]))
fp.write(bytes(after[0x7D00:0x7D09]))
fp.write(bytes([after[0x7D5A], after[0x7D09], after[0x7D60]]))
fp.write(bytes(after[0x7D5B:0x7D60]))
fp.write(bytes(after[0x7D8F:0x7D97]))
pad_count = min(after[0x7D5A], 10)
for pad_idx in range(10):
if pad_idx < pad_count:
off = 0x7D0A + pad_idx * 8
fp.write(bytes(after[off:off + 8]))
else:
fp.write(bytes(8))
fp.write(bytes(screen))
fp.write(bytes(colormap))
fp.write(bytes([level_index]))
fp.write(bytes([len(sprite_blocks)]))
for ptr, data in sprite_blocks:
fp.write(bytes([ptr]))
fp.write(bytes(data))
fp.write(bytes(after[0x7D98:0x8000]))
# Levels whose data blob flags an extra scene ($7D97 != 0) load a second
# scene from this table ($6F55, indexed by level) right after the level
# itself. Those extra scenes only write sprite bitmaps into $2000-$27FF
# (block pointers $80-$9F) for the level's hook code to animate.
_EXTRA_SCENE = [0, 0, 0, 0, 0, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0, 0x1F, 0, 0, 0,
0x20, 0x21, 0, 0, 0, 0x22, 0, 0, 0, 0x23]
def extract_one_level(ram, scene_id, out_dir):
"""Decompress `scene_id` and write out_dir/level<NN>.dat (NN = scene+1)."""
letter = LEVEL_LETTERS[scene_id]
screen_buf, color_buf, after = run_scene(ram, scene_id)
sprite_blocks = []
if after[0x7D97] != 0 and _EXTRA_SCENE[scene_id] != 0:
# Start the extra scene from a zeroed sprite window so the blocks
# it writes are exactly the ones the level ships with.
work = bytearray(after)
for i in range(0x2000, 0x2800):
work[i] = 0
_s, _c, after2 = run_scene(bytes(work), _EXTRA_SCENE[scene_id])
for ptr in range(0x80, 0xA0):
block = bytes(after2[ptr * 64:ptr * 64 + 63])
if any(block):
sprite_blocks.append((ptr, block))
# The level data itself (pads, hooks) is untouched by the extra
# scene; keep reading it from the level's own image.
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
level_num = scene_id + 1
out_path = os.path.join(out_dir, f'level{level_num:02d}.dat')
write_level(out_path, scene_id, after, screen, colormap, sprite_blocks)
return out_path, after[0x7D5A], len(sprite_blocks)
def extract_screen_y(ram, out_dir):
"""Screen Y, the 25th screen ("MYSTERY SCREEN !!!"), is scene 24.
$5010 dispatches on the screens-completed counter: at 24 it jumps to
$5167, which loads #$59 ('Y') as the pending screen; the shift only
ends when the counter reaches 25. So a full 24-hour shift plays A..X
and then Y -- the 25 screens the intro advertises.
"""
screen_buf, color_buf, after = run_scene(ram, 24)
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
out_path = os.path.join(out_dir, 'level25.dat')
write_level(out_path, 24, after, screen, colormap, [])
return out_path
def extract_title(ram, out_dir):
"""The title screen is scene 25 (what $477A decompresses).
Pads, spawn and templates are dummies."""
screen_buf, color_buf, after = run_scene(ram, 25)
cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS
screen = list(screen_buf[:cells])
colormap = [(b & 0x0F) for b in color_buf[:cells]]
out_path = os.path.join(out_dir, 'title.dat')
# The title scene writes no level name, so $7D78 still holds whatever
# the previously decompressed scene left there (PUZZLER, level H).
# The title never draws the name; ship it blank rather than stale.
work = bytearray(after)
work[0x7D78:0x7D78 + 22] = b' ' * 22
write_level(out_path, 25, work, screen, colormap, [])
return out_path
def main():
if len(sys.argv) < 3:
print('usage: romToLevel.py <raw-dump> <out-dir> [scene_id]', file=sys.stderr)
sys.exit(2)
ram = load_dump(sys.argv[1])
out_dir = sys.argv[2]
os.makedirs(out_dir, exist_ok=True)
if len(sys.argv) >= 4:
scene_ids = [int(sys.argv[3], 0)]
else:
# Levels A..X = scenes 0..23. The title comes from screen RAM
# at $0400 (the dump was taken on the title screen), not from
# a decompressed scene.
scene_ids = list(range(24))
for sid in scene_ids:
path, pad_count, nspr = extract_one_level(ram, sid, out_dir)
size = os.path.getsize(path)
print(f' scene {sid:2d} (level {LEVEL_LETTERS[sid]}): pads={pad_count} sprites={nspr} size={size} -> {path}')
# Screen Y and the title are not part of the A..X sweep.
if len(sys.argv) < 4:
y_path = extract_screen_y(ram, out_dir)
print(f' scene 24 (screen Y) : -> {y_path} ({os.path.getsize(y_path)} bytes)')
title_path = extract_title(ram, out_dir)
print(f' title (screen RAM $0400) : -> {title_path} ({os.path.getsize(title_path)} bytes)')
if __name__ == '__main__':
main()

View file

@ -13,8 +13,8 @@ extracted the speech:
A song is over when all three voice stream pointers' high bytes are zero A song is over when all three voice stream pointers' high bytes are zero
(that is exactly what the game's play-and-wait loop at $44D5 spins on). (that is exactly what the game's play-and-wait loop at $44D5 spins on).
""" """
import json, sys import json, os, sys
sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi") sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502 from cpu6502 import Cpu6502
RAM = bytearray(open(sys.argv[1], "rb").read()[:65536]) RAM = bytearray(open(sys.argv[1], "rb").read()[:65536])

View file

@ -1,7 +1,7 @@
// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared // Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared
// by the game and the host-side simulation harness. // by the game and the host-side simulation harness.
// //
// Layout (all bytes, written by stuff/spacetaxi/romToLevel.py): // Layout (all bytes, written by examples/spacetaxi/assets/romToLevel.py):
// "STL4" // "STL4"
// name[22] $7D78 level name in screen codes // name[22] $7D78 level name in screen codes
// header[9] $7D00 VIC colour block // header[9] $7D00 VIC colour block

447
install.sh Executable file
View file

@ -0,0 +1,447 @@
#!/usr/bin/env bash
#
# JoeyLib installer: everything needed to build (and, with the emulator
# components, run and test) JoeyLib programs, pinned and checksummed.
# Host packages come from the Linux distribution (apt or dnf); everything
# else is downloaded or built under toolchains/ and nowhere else, so that
# folder can be deleted at any time and re-created by running this again.
#
# Usage:
# ./install.sh [options] <command> [targets...]
#
# Commands:
# list every component, its groups and whether it is current
# install TARGETS... install (or update) components and groups
# deps TARGETS... show the host packages TARGETS need (--install: install them)
# doctor check the host and smoke-test every installed component
# import PATH... take private files (Kickstart, ...) from files, folders
# or .zip archives, matched by checksum
# (installer/files.sha1)
# bundle FILE.tar.gz pack the private files present here, for the owner
# to host (JOEY_BUNDLE_URL in installer/bundle.conf)
# help this text
#
# Targets are component names (see `list`) or groups:
# toolchain:PORT emulator:PORT PORT toolchains emulators all
# where PORT is iigs, amiga, atarist, dos or x68000. `all` leaves out
# optional components (such as staxiData); name those explicitly.
#
# Options:
# -y, --yes install missing host packages without asking
# --install with `deps`: install the missing host packages
# --force reinstall even when a component is current
# --jobs N parallel build jobs (default: every core)
# --keep-build keep the scratch build trees in cache/build
# --llvm816-dir PATH use an existing llvm816 checkout for the IIgs
# compiler (remembered; "--llvm816-dir clone" undoes it)
#
# Examples:
# ./install.sh install toolchains every cross compiler
# ./install.sh install emulator:dos what a tester needs for DOS
# ./install.sh install amiga Amiga compiler + emulator
# ./install.sh deps all --install just the host packages
#
# make reads toolchains/config.mk by itself. For interactive shells,
# `source toolchains/env.sh` sets the same variables.
set -euo pipefail
# shellcheck source=installer/lib/common.sh
source "$(dirname "${BASH_SOURCE[0]}")/installer/lib/common.sh"
# shellcheck source=installer/lib/distro.sh
source "$JOEY_INSTALLER/lib/distro.sh"
# shellcheck source=installer/lib/config.sh
source "$JOEY_INSTALLER/lib/config.sh"
# shellcheck source=installer/lib/bundle.sh
source "$JOEY_INSTALLER/lib/bundle.sh"
JOEY_PORTS="iigs amiga atarist dos x68000"
depsInstall=0
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
}
# groupMembers GROUP -- print the components in GROUP.
groupMembers() {
local group="$1"
local id
local groups
for id in $(componentIds); do
groups=" $(compQuery "$id" compGroups) "
case "$group" in
all)
if [[ "$groups" != *" optional "* ]]; then
printf '%s\n' "$id"
fi
;;
toolchains)
if [[ "$groups" == *" toolchain:"* ]]; then
printf '%s\n' "$id"
fi
;;
emulators)
if [[ "$groups" == *" emulator:"* ]]; then
printf '%s\n' "$id"
fi
;;
iigs|amiga|atarist|dos|x68000)
if [[ "$groups" == *" toolchain:$group "* || "$groups" == *" emulator:$group "* ]]; then
printf '%s\n' "$id"
fi
;;
*)
if [[ "$groups" == *" $group "* ]]; then
printf '%s\n' "$id"
fi
;;
esac
done
}
# resolveTargets TARGET... -- print component ids, each after the components
# it requires.
resolveTargets() {
local target
local id
local members
declare -A seen=()
local ordered=()
addWithRequires() {
local req
[ -z "${seen[$1]:-}" ] || return 0
seen[$1]=1
for req in $(compQuery "$1" compRequires); do
addWithRequires "$req"
done
ordered+=("$1")
}
[ $# -gt 0 ] || die "no targets given (try: toolchains, emulators, all, or a port name)"
for target in "$@"; do
if [ -f "$JOEY_COMPONENTS/$target.sh" ]; then
addWithRequires "$target"
continue
fi
members="$(groupMembers "$target")"
[ -n "$members" ] || die "unknown target: $target (see: ./install.sh list)"
for id in $members; do
addWithRequires "$id"
done
done
printf '%s\n' "${ordered[@]}"
}
# hostPackagesFor ID... -- the distribution packages the components need.
hostPackagesFor() {
local id
local logical=()
for id in "$@"; do
# shellcheck disable=SC2207
logical+=($(compQuery "$id" compHostDeps))
done
[ ${#logical[@]} -gt 0 ] || return 0
distroPackageNames $(printf '%s\n' "${logical[@]}" | sort -u) | sort -u
}
cmdDeps() {
local ids
local pkgs
local missing
# Command substitution (not < <(...)) so a failure -- an unknown target,
# an unsupported distribution -- stops the script.
ids="$(resolveTargets "$@")"
pkgs="$(hostPackagesFor $ids)"
missing="$(distroMissing $pkgs)"
info "host packages for $*: $(printf '%s ' $pkgs)"
if [ -z "$missing" ]; then
ok "all present"
return 0
fi
warn "missing: $(printf '%s ' $missing)"
if [ "$depsInstall" = "1" ]; then
distroInstall $missing
else
info "install them with: ./install.sh deps $* --install"
return 1
fi
}
cmdInstall() {
local ids
local pkgs
local missing
local id
local installed=0
local idsText
idsText="$(resolveTargets "$@")"
mapfile -t ids <<< "$idsText"
pkgs="$(hostPackagesFor "${ids[@]}")"
missing="$(distroMissing $pkgs)"
if [ -n "$missing" ]; then
if [ "$JOEY_ASSUME_YES" = "1" ]; then
distroInstall $missing
else
die "missing host packages: $(printf '%s ' $missing)
Install them with: ./install.sh deps $* --install
or re-run with --yes to install them as part of this command."
fi
fi
# A failed component does not stop the others (a tester without the
# private bundle still gets every other emulator); anything that
# requires a failed component is skipped. All of it is reported at the
# end, and the exit status is non-zero.
local failed=()
local req
local blocked
for id in "${ids[@]}"; do
blocked=""
for req in $(compQuery "$id" compRequires); do
if [[ " ${failed[*]:-} " == *" $req "* ]]; then
blocked="$req"
fi
done
if [ -n "$blocked" ]; then
warn "$id skipped: it needs $blocked, which failed"
failed+=("$id")
continue
fi
if [ "$JOEY_FORCE" != "1" ] && compIsCurrent "$id"; then
ok "$id is current"
continue
fi
header "$id: $(compQuery "$id" compDescription)"
stampClear "$id"
if ! compRun "$id" compInstall; then
warn "$id: install failed"
failed+=("$id")
continue
fi
if ! compRun "$id" compInstalled; then
warn "$id: install finished but the result is missing"
failed+=("$id")
continue
fi
if ! compRun "$id" compVerify >/dev/null; then
warn "$id: installed, but its smoke test failed"
failed+=("$id")
continue
fi
stampWrite "$id" "$(compQuery "$id" compPin)"
ok "$id installed"
installed=$((installed + 1))
done
writeConfig
if [ ${#failed[@]} -gt 0 ]; then
die "$(( ${#ids[@]} - ${#failed[@]} )) of ${#ids[@]} components current; FAILED: ${failed[*]}"
fi
ok "${#ids[@]} components current ($installed installed now)"
info "make picks the tools up by itself; for a shell: source toolchains/env.sh"
}
cmdImport() {
local installedText
local id
[ $# -gt 0 ] || die "import needs at least one file, folder or .zip archive"
installedText="$(importPaths "$@")"
if [ -z "$installedText" ]; then
warn "nothing matched: none of those files is in installer/files.sha1"
return 1
fi
printf '%s\n' "$installedText" | sort -u | while read -r dest; do
ok "imported $dest"
done
# Mark the private components whose file is now in place as installed.
for id in $(componentIds); do
if [ -n "$(compQuery "$id" compPrivateFile)" ] && compRun "$id" compInstalled; then
stampWrite "$id" "$(compQuery "$id" compPin)"
fi
done
writeConfig
}
cmdList() {
local id
local status
local stamp
printf '%-13s %-9s %-46s %s\n' "COMPONENT" "STATUS" "GROUPS" "DESCRIPTION"
for id in $(componentIds); do
stamp="$(stampRead "$id")"
if [ -z "$stamp" ] || ! compRun "$id" compInstalled >/dev/null 2>&1; then
status="missing"
elif [ "$stamp" = "$(compQuery "$id" compPin)" ]; then
status="current"
else
status="outdated"
fi
printf '%-13s %-9s %-46s %s\n' "$id" "$status" "$(compQuery "$id" compGroups)" "$(compQuery "$id" compDescription)"
done
printf '\ngroups: toolchains emulators all %s, and toolchain:PORT / emulator:PORT\n' "$JOEY_PORTS"
}
# portGroups ID -- ID's toolchain:PORT and emulator:PORT groups.
portGroups() {
local group
for group in $(compQuery "$1" compGroups); do
case "$group" in
toolchain:*|emulator:*) printf '%s\n' "$group" ;;
esac
done
}
# wantedGroups -- the port groups the user chose: those holding an installed
# component that belongs to that group alone. Shared members (headless,
# libxmp, mame, cadius) do not count, or installing one port would make
# every port that shares them look half-installed.
wantedGroups() {
local id
local groups
for id in $(componentIds); do
[ -n "$(stampRead "$id")" ] || continue
groups="$(portGroups "$id")"
[ -n "$groups" ] && [ "$groups" = "${groups%%$'\n'*}" ] && printf '%s\n' "$groups"
done
return 0
}
# firstGroupIn ID " GROUP... " -- print ID's first port group in the list.
firstGroupIn() {
local group
for group in $(portGroups "$1"); do
if [[ "$2" == *" $group "* ]]; then
printf '%s\n' "$group"
return 0
fi
done
}
cmdDoctor() {
local id
local failures=0
local family
local wanted
local group
header "host"
family="$(distroFamily)"
if [ "$family" = "unknown" ]; then
warn "unrecognised distribution: host packages must be installed by hand"
failures=$((failures + 1))
else
ok "package manager: $family"
fi
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
warn "bash ${BASH_VERSION} is older than 4"
failures=$((failures + 1))
fi
header "components"
wanted=" $(printf '%s ' $(wantedGroups))"
for id in $(componentIds); do
if [ -z "$(stampRead "$id")" ]; then
group="$(firstGroupIn "$id" "$wanted")"
if [ -n "$group" ]; then
warn "$id: not installed, but the rest of $group is (run: ./install.sh install $id)"
failures=$((failures + 1))
fi
elif ! compRun "$id" compInstalled >/dev/null 2>&1; then
if [ -n "$(compQuery "$id" compPrivateFile)" ]; then
warn "$id: $(compQuery "$id" compPrivateFile) is gone; supply it with ./install.sh import PATH (or JOEY_BUNDLE_URL)"
else
warn "$id: its files are gone (run: ./install.sh install $id)"
fi
failures=$((failures + 1))
elif ! compIsCurrent "$id"; then
warn "$id: out of date, its pin changed (run: ./install.sh install $id)"
failures=$((failures + 1))
elif compRun "$id" compVerify >/dev/null; then
ok "$id"
else
warn "$id: smoke test failed"
failures=$((failures + 1))
fi
done
writeConfig
if [ "$failures" -gt 0 ]; then
die "$failures problem(s) found"
fi
ok "everything installed checks out"
}
# ---- argument parsing -----------------------------------------------------
command=""
targets=()
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes)
JOEY_ASSUME_YES=1
;;
--install)
depsInstall=1
;;
--force)
JOEY_FORCE=1
;;
--keep-build)
JOEY_KEEP_BUILD=1
;;
--jobs)
[ $# -ge 2 ] || die "--jobs needs a number"
JOEY_JOBS="$2"
shift
;;
--llvm816-dir)
[ $# -ge 2 ] || die "--llvm816-dir needs a path (or 'clone')"
mkdir -p "$JOEY_STATE"
if [ "$2" = "clone" ]; then
rm -f "$JOEY_STATE/llvm816Dir"
else
[ -f "$2/setup.sh" ] || die "$2 does not look like an llvm816 checkout (no setup.sh)"
(cd "$2" && pwd) > "$JOEY_STATE/llvm816Dir"
fi
shift
;;
-h|--help|help)
usage
exit 0
;;
-*)
die "unknown option: $1 (see: ./install.sh help)"
;;
*)
if [ -z "$command" ]; then
command="$1"
else
targets+=("$1")
fi
;;
esac
shift
done
case "$command" in
install) cmdInstall "${targets[@]}" ;;
deps) cmdDeps "${targets[@]}" ;;
import) cmdImport "${targets[@]}" ;;
bundle) [ ${#targets[@]} -eq 1 ] || die "bundle needs one output file name"; bundleCreate "${targets[0]}" ;;
list) cmdList ;;
doctor) cmdDoctor ;;
"") usage; exit 2 ;;
*) die "unknown command: $command (see: ./install.sh help)" ;;
esac

View file

@ -0,0 +1,96 @@
# Amiga C toolchain: Bebbo's m68k-amigaos-gcc 6.5 (AmigaPorts), with
# binutils, libnix, clib2, the NDK 3.2 headers and vasm. Built from source
# (~15-40 minutes).
#
# The build clones its sub-projects by BRANCH name, which would make the
# result drift; every sub-project is pre-cloned here at a pinned commit so
# the Makefile finds it in place and skips its own clone. It also wget's
# the NDK and ixemul SDK on every build; those are checked afterwards.
compDescription="Amiga compiler: m68k-amigaos-gcc 6.5 + NDK 3.2 + vasm (source build)"
compGroups="toolchain:amiga"
compRequires="host"
compHostDeps="build-tools git which bison flex m4 texinfo autoconf automake gettext rsync wget lhasa gmp-dev mpfr-dev mpc-dev ncurses-dev readline-dev"
compUrl="https://github.com/AmigaPorts/m68k-amigaos-gcc.git"
compCommit="7d3e35f58e61bdf325d84b912b139f973d9b7452"
compPin="$compCommit"
# name commit (the URL comes from the checkout's default-repos file)
subProjects=(
"amiga-netinclude 7d155ca3a3090afe3466f819dd178f2af6f5c176"
"aros-stuff 86711c70569ffc6b70ee1fb99895866c1d9f30e4"
"binutils 45a7a53c6d42a38e544ea30116e03eb6b2b37f8b"
"clib2 694064c4000b335247164773dd2d231e809a8e8e"
"fd2pragma e98f0901340b8989308ca31e53c02a6e61f8339e"
"fd2sfd 403714de37f1b9851475ab455964af355bcd6523"
"gcc 87dab3c2f2524682287f5e841d75caa60ba504c2"
"ira bb7c11384a32c409341fdb0d16d715b7959a0290"
"libdebug 42d9d3a7a0d26fa4679384ffc78cf79fbce394d8"
"libnix e1ba6cfae3d39c560f975bc2e44d9e8e99b42868"
"newlib-cygwin 4c8cbd88df31cd06c89b258585ea5cd86cc9dc45"
"sfdc 0e9fa28272aab30f72f34b2d71c178d6ca88d9dd"
"vasm 291f6f08226c8843711e1319864526e6cd57ce06"
)
# file sha256 -- what the build is expected to have downloaded
buildDownloads=(
"NDK3.2.lha 96cabd4ad683dced632e147bf86dee0f50dcb1254386216c25c362916a6409bb"
"ixemul-sdk.lha 00b4054283b99d22f6b353194fa8c75917ec0cc08a3a64395a891da446db07fc"
)
compInstall() {
local src
local entry
local name
local sha
local url
local dir
src="$(freshBuildDir "$compId")"
gitFetchCommit "$compUrl" "$compCommit" "$src"
for entry in "${subProjects[@]}"; do
read -r name sha <<< "$entry"
url="$(awk -v n="$name" '$1 == n { print $2 }' "$src/default-repos")"
[ -n "$url" ] || die "amiga-gcc: no URL for sub-project $name in default-repos"
gitFetchCommit "$url" "$sha" "$src/projects/$name"
done
rm -rf "$compDir"
make -C "$src" all PREFIX="$compDir" -j"$JOEY_JOBS"
# Anything cloned by the build itself is unpinned; say so.
for dir in "$src"/projects/*/; do
name="$(basename "$dir")"
[ -d "$dir/.git" ] || continue
if [[ " ${subProjects[*]} " != *" $name "* ]]; then
warn "amiga-gcc: the build cloned unpinned sub-project '$name'; pin it in $compId.sh"
fi
done
for entry in "${buildDownloads[@]}"; do
read -r name sha <<< "$entry"
[ "$(sha256Of "$src/download/$name")" = "$sha" ] ||
die "amiga-gcc: $name downloaded by the build does not match its pin; upstream changed it"
done
dropBuildDir "$compId"
}
compInstalled() {
[ -x "$compDir/bin/m68k-amigaos-gcc" ] && [ -x "$compDir/bin/vasmm68k_mot" ] && [ -d "$compDir/m68k-amigaos/sys-include" ]
}
compVerify() {
verifyCompiles "$compDir/bin/m68k-amigaos-gcc" -m68000 -O2
}
compEnv() {
envVar AMIGA_PREFIX "$compDir"
envVar AMIGA_CC "$compDir/bin/m68k-amigaos-gcc"
envVar AMIGA_AR "$compDir/bin/m68k-amigaos-ar"
envVar AMIGA_LD "$compDir/bin/m68k-amigaos-ld"
# vasm assembles only ptplayer.asm (Devpac syntax); JoeyLib's own .s
# files are GAS syntax and go through the gcc driver.
envVar AMIGA_AS "$compDir/bin/vasmm68k_mot"
envVar AMIGA_NDK "$compDir/m68k-amigaos/sys-include"
envPath "$compDir/bin"
}

View file

@ -0,0 +1,44 @@
# cadius: ProDOS disk-image tool used by every IIgs disk script. The
# scripts parse its CATALOG output, so it stays pinned to the mach-kernel
# fork commit they were written against (v1.4.6).
compDescription="IIgs disk images: cadius 1.4.6 (ProDOS image tool)"
compGroups="toolchain:iigs emulator:iigs"
compRequires="host"
compHostDeps="build-tools git"
compUrl="https://github.com/mach-kernel/cadius.git"
compCommit="75c4aa684f6aa0de0c2879b729be465196a348ba"
# Built with _GNU_SOURCE: its strptime() call has no prototype otherwise,
# which gcc 14 (Fedora 40) rejects. The flag is in the pin so existing
# builds redo it.
cflags="-D_GNU_SOURCE"
compPin="$compCommit $cflags"
compInstall() {
local src
src="$(freshBuildDir "$compId")"
gitFetchCommit "$compUrl" "$compCommit" "$src"
# Through the environment: its Makefile appends its own flags to it.
CFLAGS="$cflags" make -C "$src" -j"$JOEY_JOBS"
install -D -m 755 "$src/bin/release/cadius" "$compDir/bin/cadius"
dropBuildDir "$compId"
}
compInstalled() {
[ -x "$compDir/bin/cadius" ]
}
compVerify() {
local banner
# Run with no arguments it prints its banner and exits non-zero.
banner="$("$compDir/bin/cadius" 2>&1 || true)"
[[ "$banner" == *"v 1.4.6"* ]]
}
compEnv() {
envVar CADIUS "$compDir/bin/cadius"
}

View file

@ -0,0 +1,36 @@
# CWSDPMI r7: the DPMI host embedded into every DOS executable
# (make/support/dos/embed-dpmi.sh prepends CWSDSTUB.EXE), so programs run
# without a separate CWSDPMI.EXE.
compDescription="DOS DPMI host: CWSDPMI r7 (embedded into each .EXE)"
compGroups="toolchain:dos"
compRequires="host"
compHostDeps="unzip"
compUrl="https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip"
compSha256="deacda0488e1cdd7c4a9f32fab45662b34c0ed6b2d7d4d13bc07041b62004a8c"
compPin="$compSha256"
compInstall() {
local zip
zip="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
# The zip holds bin/ and src/ trees; bin/ is what the build uses.
extractArchive "$zip" "$compDir"
}
compInstalled() {
[ -f "$compDir/bin/CWSDSTUB.EXE" ] && [ -f "$compDir/bin/CWSDPMI.EXE" ]
}
compVerify() {
[ "$(head -c 2 "$compDir/bin/CWSDSTUB.EXE")" = "MZ" ]
}
compEnv() {
envVar DOS_CWSDPMI "$compDir/bin"
envVar DOS_CWSDSTUB "$compDir/bin/CWSDSTUB.EXE"
}

View file

@ -0,0 +1,44 @@
# DOS C toolchain: DJGPP with gcc 12.2.0, prebuilt for Linux x86-64 by
# andrewwutw/build-djgpp (release v3.4).
compDescription="DOS compiler: DJGPP gcc 12.2.0 (build-djgpp v3.4)"
compGroups="toolchain:dos"
compRequires="host"
compHostDeps="bzip2 libfl"
compUrl="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-linux64-gcc1220.tar.bz2"
compSha256="8464f17017d6ab1b2bb2df4ed82357b5bf692e6e2b7fee37e315638f3d505f00"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -x "$compDir/bin/i586-pc-msdosdjgpp-gcc" ] && [ -x "$compDir/i586-pc-msdosdjgpp/bin/exe2coff" ]
}
compVerify() {
verifyLinked "$compDir/bin" "$compDir/i586-pc-msdosdjgpp/bin" "$compDir"/libexec/gcc/*/*
verifyCompiles "$compDir/bin/i586-pc-msdosdjgpp-gcc" -march=i386 -O2
}
compEnv() {
envVar DOS_PREFIX "$compDir"
envVar DOS_CC "$compDir/bin/i586-pc-msdosdjgpp-gcc"
envVar DOS_AR "$compDir/bin/i586-pc-msdosdjgpp-ar"
envVar DOS_LD "$compDir/bin/i586-pc-msdosdjgpp-ld"
# The DOS-target utilities live in the target sysroot's bin, not the
# host bin that holds the cross gcc.
envVar DOS_STUBEDIT "$compDir/i586-pc-msdosdjgpp/bin/stubedit"
envVar DOS_STUBIFY "$compDir/i586-pc-msdosdjgpp/bin/stubify"
envVar DOS_EXE2COFF "$compDir/i586-pc-msdosdjgpp/bin/exe2coff"
envVar DOS_EMBED_DPMI "$JOEY_BUILD_SUPPORT/dos/embed-dpmi.sh"
envPath "$compDir/bin"
}

View file

@ -0,0 +1,41 @@
# DOSBox Staging 0.83.0: the DOS emulator, as the project's own prebuilt
# Linux release (bundled libraries), because Debian/Ubuntu only package the
# old 0.74 and the gates are written for Staging.
compDescription="emulator: DOSBox Staging 0.83.0 (MS-DOS; upstream Linux release)"
compGroups="emulator:dos"
compRequires="host"
compHostDeps="xz"
compUrl="https://github.com/dosbox-staging/dosbox-staging/releases/download/v0.83.0/dosbox-staging-linux-x86_64-v0.83.0.tar.xz"
compSha256="d3a94f7f1c3e68a47ec88d61145506c7904452adb0c9c5928cb8cfe2331d6c5c"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -x "$compDir/dosbox" ]
}
compVerify() {
local xdg
local version
verifyLinked "$compDir"
# Isolated: even --version creates ~/.config/dosbox otherwise.
xdg="$(mktemp -d)"
version="$(XDG_CONFIG_HOME="$xdg" "$compDir/dosbox" --noprimaryconf --nolocalconf --version 2>&1 || true)"
rm -rf "$xdg"
[[ "$version" == *"version 0.83.0"* ]]
}
compEnv() {
envVar DOSBOX "$compDir/dosbox"
}

View file

@ -0,0 +1,36 @@
# X68000 C toolchain: yunkya2's elf2x68k (m68k-xelf gcc 13.4.0, binutils
# 2.44, newlib 4.5.0), prebuilt for Linux x86-64. m68k-xelf-gcc emits ELF;
# its linker script runs elf2x68k.py (python3) to produce Human68k .X files.
compDescription="X68000 compiler: elf2x68k 20260602 (m68k-xelf gcc 13.4.0)"
compGroups="toolchain:x68000"
compRequires="host"
compHostDeps="bzip2 python3 gconv-cp932"
compUrl="https://github.com/yunkya2/elf2x68k/releases/download/20260602/elf2x68k-Linux-20260602.tar.bz2"
compSha256="625968297d140b5b5a47edbc67d69a0b505b0891adb152b45adeee5329fd9832"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -x "$compDir/bin/m68k-xelf-gcc" ]
}
compVerify() {
verifyLinked "$compDir/bin" "$compDir/m68k-elf/bin" "$compDir"/libexec/gcc/*/*
verifyCompiles "$compDir/bin/m68k-xelf-gcc" -m68000 -O2
}
compEnv() {
envVar X68K_ROOT "$compDir"
envPath "$compDir/bin"
}

View file

@ -0,0 +1,36 @@
# EmuTOS 1.4 (GPL): a free TOS replacement for the Atari ST emulator, the
# 512 KB US-English image. The gates and joey-run boot it instead of
# Atari's copyrighted TOS.
compDescription="Atari ST firmware: EmuTOS 1.4 (free TOS replacement)"
compGroups="emulator:atarist"
compRequires="host"
compHostDeps="unzip"
compUrl="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1.4.zip/download"
compSha256="1ef7bd25f61bcfc66d19debc2f0ebb0d5e5ba811ccd7f0fddfcb105bb72d1663"
compPin="$compSha256"
imgPath="$JOEY_FIRMWARE/atarist/emutos-512k.img"
compInstall() {
local tmp
tmp="$(freshBuildDir "$compId")"
unzip -oq "$(fetchFile "$compUrl" "$compSha256" emutos-512k-1.4.zip)" 'emutos-512k-1.4/etos512us.img' -d "$tmp"
install -D -m 644 "$tmp/emutos-512k-1.4/etos512us.img" "$imgPath"
dropBuildDir "$compId"
}
compInstalled() {
[ -f "$imgPath" ]
}
compVerify() {
[ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ]
}
compEnv() {
envVar EMUTOS_IMG "$imgPath"
}

View file

@ -0,0 +1,58 @@
# FS-UAE 3.1.66: the Amiga emulator. Debian and Ubuntu package exactly this
# version; Fedora does not package FS-UAE at all, so on dnf systems it is
# built from the upstream release tarball into toolchains/opt/fsuae.
compDescription="emulator: FS-UAE 3.1.66 (Amiga; distribution package, source build on dnf)"
compGroups="emulator:amiga"
compRequires="host"
compUrl="https://fs-uae.net/files/FS-UAE/Stable/3.1.66/fs-uae-3.1.66.tar.xz"
compSha256="606e1868b500413d69bd33bb469f8fd08d6c08988801f17b7dd022f3fbe23832"
if [ "$(distroFamily)" = "dnf" ]; then
fsuaeBin="$compDir/bin/fs-uae"
compHostDeps="build-tools make pkg-config gettext zip glib-dev openal-dev png-dev sdl2-dev zlib-dev gl-dev x11-dev xi-dev"
compPin="$compSha256"
else
fsuaeBin="$(command -v fs-uae || true)"
compHostDeps="fs-uae"
compPin="distro"
fi
compInstall() {
local src
local archive
if [ "$compPin" = "distro" ]; then
[ -n "$fsuaeBin" ] || die "fs-uae is not on PATH after installing host packages"
return 0
fi
src="$(freshBuildDir "$compId")"
archive="$(fetchFile "$compUrl" "$compSha256")"
extractArchive "$archive" "$src" 1
# libmpeg2 only plays CD32 FMV; nothing here needs it.
(cd "$src" && ./configure --prefix="$compDir" --without-libmpeg2)
make -C "$src" -j"$JOEY_JOBS"
rm -rf "$compDir"
make -C "$src" install
dropBuildDir "$compId"
}
compInstalled() {
[ -n "$fsuaeBin" ] && [ -x "$fsuaeBin" ]
}
compVerify() {
local version
version="$("$fsuaeBin" --version 2>&1)" || return 1
version="${version%%$'\n'*}"
# Another version still runs programs; only the gates may disagree.
if [ "$version" != "3.1.66" ]; then
warn "found FS-UAE '$version'; the Amiga gates were written against 3.1.66"
fi
}
compEnv() {
envVar FSUAE "$fsuaeBin"
}

View file

@ -0,0 +1,58 @@
# Hatari 2.4.1: the Atari ST emulator. The ST gates parse its trace and
# debugger output as Hatari 2.4 prints them. Debian and Ubuntu package
# 2.4.1; Fedora ships a different version, so on dnf systems 2.4.1 is built
# from source into toolchains/opt/hatari.
compDescription="emulator: Hatari 2.4.1 (Atari ST; distribution package, source build on dnf)"
compGroups="emulator:atarist"
compRequires="host"
compUrl="https://framagit.org/hatari/hatari.git"
compCommit="4371dcd647fc85d31c0629400adaeaa4212040d9"
if [ "$(distroFamily)" = "dnf" ]; then
hatariBin="$compDir/bin/hatari"
compHostDeps="build-tools make cmake git sdl2-dev zlib-dev png-dev readline-dev"
compPin="$compCommit"
else
hatariBin="$(command -v hatari || true)"
compHostDeps="hatari"
compPin="distro"
fi
compInstall() {
local src
if [ "$compPin" = "distro" ]; then
[ -n "$hatariBin" ] || die "hatari is not on PATH after installing host packages"
return 0
fi
src="$(freshBuildDir "$compId")"
gitFetchCommit "$compUrl" "$compCommit" "$src"
cmake -S "$src" -B "$src/build" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$compDir"
cmake --build "$src/build" -j"$JOEY_JOBS"
rm -rf "$compDir"
cmake --install "$src/build"
dropBuildDir "$compId"
}
compInstalled() {
[ -n "$hatariBin" ] && [ -x "$hatariBin" ]
}
compVerify() {
local version
# It prints a blank line first and exits 1, so match anywhere.
version="$("$hatariBin" --version 2>&1 || true)"
[[ "$version" == *"Hatari v"* ]] || return 1
# Another version still runs programs; only the gates may disagree.
if [[ "$version" != *"Hatari v2.4"* ]]; then
version="${version#$'\n'}"
warn "found '${version%%$'\n'*}'; the ST gates were written against Hatari 2.4.x"
fi
}
compEnv() {
envVar HATARI "$hatariBin"
}

View file

@ -0,0 +1,29 @@
# What the test gates need to run the SDL/X11 emulators without a screen:
# a virtual X server, key injection, screenshots, and PulseAudio's
# capture tools for the audio gates.
compDescription="test gates: Xvfb, xdotool, ImageMagick, pactl/parec (headless runs)"
compGroups="emulator:amiga emulator:atarist emulator:dos"
compRequires="host"
compHostDeps="xvfb xdotool imagemagick pulse-utils"
compPin="distro"
compInstall() {
:
}
compInstalled() {
command -v Xvfb >/dev/null && command -v xdotool >/dev/null && command -v import >/dev/null && command -v parec >/dev/null
}
compVerify() {
compInstalled
}
compEnv() {
:
}

View file

@ -0,0 +1,67 @@
# Host tools every JoeyLib setup needs: a native compiler for the in-repo
# tools, GNU make, git with LFS (the sample assets live in LFS), the
# archive tools the other components unpack with, and Python 3 with
# Pillow for the asset converters.
compDescription="host tools: make, host compiler, git-lfs, python3 + Pillow, archivers"
# Not in any port group: every other component requires it.
compGroups="host"
compRequires=""
compHostDeps="build-tools make git git-lfs curl tar xz bzip2 unzip lhasa python3 python3-pil procps diffutils"
compPin="1"
# Print the LFS-tracked files that are still pointer stubs.
lfsPointerFiles() {
local file
local names
local start
names="$(git -C "$JOEY_REPO" lfs ls-files --name-only 2>/dev/null || true)"
while IFS= read -r file; do
[ -n "$file" ] || continue
# A pointer stub starts with this 23-byte line; read just that much
# (binary assets hold NUL bytes a command substitution would warn about).
start=""
IFS= read -r -N 23 start < "$JOEY_REPO/$file" 2>/dev/null || true
if [[ "$start" == "version https://git-lfs"* ]]; then
printf '%s\n' "$file"
fi
done <<< "$names"
}
compInstall() {
mkdir -p "$compDir"
# A clone made without git-lfs holds pointer stubs instead of the
# sample assets; fetch the real files only in that case.
if [ -d "$JOEY_REPO/.git" ] && [ -n "$(lfsPointerFiles)" ]; then
info "fetching Git LFS assets"
git -C "$JOEY_REPO" lfs install --local
git -C "$JOEY_REPO" lfs pull
fi
}
compInstalled() {
command -v make >/dev/null && command -v cc >/dev/null && command -v python3 >/dev/null && command -v git-lfs >/dev/null
}
compVerify() {
local makeVersion
local stubs
# No pipes into head here: under pipefail an early-exiting reader makes
# the writer die of SIGPIPE, and the check fails at random.
makeVersion="$(make --version 2>/dev/null || true)"
[[ "$makeVersion" =~ ([0-9]+\.[0-9]+) ]] && makeVersion="${BASH_REMATCH[1]}"
# Grouped targets (&:) in the port makefiles need GNU make 4.3.
printf '%s\n4.3\n' "$makeVersion" | sort -V -C -r || { warn "GNU make $makeVersion is older than 4.3"; return 1; }
python3 -c 'import PIL' || { warn "python3 cannot import PIL (Pillow)"; return 1; }
stubs="$(lfsPointerFiles | wc -l)"
[ "$stubs" = "0" ] || { warn "$stubs Git LFS files are still pointer stubs"; return 1; }
}
compEnv() {
:
}

View file

@ -0,0 +1,61 @@
# 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 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"
compGroups="emulator:iigs"
compRequires="host"
compHostDeps="unzip"
romBase="https://archive.org/download/mame-0.240-roms-split_202201/MAME%200.240%20ROMs%20(split)"
cffaUrl="https://dreher.net/projects/CFforAppleII/downloads/Run6_CDROM.zip"
gsosUrl="https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/gs/os/gsos/Apple_IIGS_System_6.0.4.zip"
fwDir="$JOEY_FIRMWARE/iigs"
# file sha256
romSets=(
"apple2gs.zip eddce4a9f9f6e73ffed06c5aede6fbcdaa01dda5568b81f90d865858a3d7d9bc"
"apple2gsr1.zip 1c16fec7b9e785c4118909e409e61dcc38d0921f774b8708bae176bc5eb27307"
)
cffaSha256="48242b9e2e87d87bfef1f1b1d64d1091bbfca182d745c1d031690fac0f80c0fe"
gsosSha256="e42a4f1f50c1aae0cc17dc555415a2fbf21dc0b7b6b3e33e0d73bf22ef97506a"
compPin="${romSets[*]} $cffaSha256 $gsosSha256"
compInstall() {
local entry
local name
local sha
local tmp
for entry in "${romSets[@]}"; do
read -r name sha <<< "$entry"
install -D -m 644 "$(fetchFile "$romBase/$name" "$sha")" "$fwDir/roms/$name"
done
tmp="$(freshBuildDir "$compId")"
unzip -oq "$(fetchFile "$cffaUrl" "$cffaSha256")" 'Firmware/V2.0/CFFA20EEC02.BIN' -d "$tmp"
install -D -m 644 "$tmp/Firmware/V2.0/CFFA20EEC02.BIN" "$fwDir/roms/a2cffa2/cffa20eec02.bin"
unzip -oq "$(fetchFile "$gsosUrl" "$gsosSha256")" 'Apple_IIGS_System_6.0.4/PO Disk Images/System.Disk.po' -d "$tmp"
install -D -m 644 "$tmp/Apple_IIGS_System_6.0.4/PO Disk Images/System.Disk.po" "$fwDir/gsos-system.po"
dropBuildDir "$compId"
}
compInstalled() {
[ -f "$fwDir/roms/apple2gs.zip" ] && [ -f "$fwDir/roms/apple2gsr1.zip" ] &&
[ -f "$fwDir/roms/a2cffa2/cffa20eec02.bin" ] && [ -f "$fwDir/gsos-system.po" ]
}
compVerify() {
[ "$(sha1sum "$fwDir/roms/a2cffa2/cffa20eec02.bin" | cut -d' ' -f1)" = "080ff88f19de22328e162954ee2b51ee65f9d5cd" ] &&
[ "$(sha1sum "$fwDir/gsos-system.po" | cut -d' ' -f1)" = "a57940a0b724fc7dc545236a4f9d4c6dd68f3e81" ]
}
compEnv() {
envVar IIGS_ROMPATH "$fwDir/roms"
envVar GSOS_SYSTEM_DISK "$fwDir/gsos-system.po"
}

View file

@ -0,0 +1,31 @@
# Amiga Kickstart 3.1 ROM. Cloanto still sells it (Amiga Forever), so it is
# never downloaded from a public source: it comes from the private bundle
# or `install.sh import`. The Amiga gates need it; AROS, FS-UAE's free
# fallback, is not reliable enough for them.
compDescription="Amiga firmware: Kickstart 3.1 (private: bundle or import)"
compGroups="emulator:amiga"
compRequires=""
compHostDeps=""
compPrivateFile="toolchains/firmware/amiga/kickstart.rom"
compPin="$(privateSha1For "$compPrivateFile")"
compInstall() {
privateEnsure "$compPrivateFile"
}
compInstalled() {
privatePresent "$compPrivateFile"
}
compVerify() {
privatePresent "$compPrivateFile"
}
compEnv() {
envVar KICKSTART_ROM "$JOEY_REPO/$compPrivateFile"
}

View file

@ -0,0 +1,33 @@
# libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST and X68000 audio
# HALs compile in. Each port's makefile builds it from this source tree.
compDescription="audio: libxmp-lite 4.7.0 source (DOS, Atari ST, X68000)"
compGroups="toolchain:dos toolchain:atarist toolchain:x68000"
compRequires="host"
compHostDeps="tar"
compUrl="https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.0/libxmp-lite-4.7.0.tar.gz"
compSha256="69967a7802d2d2f938347f6f9cd95232bf45a4623cbfcc196e37cdaefadc3974"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -f "$compDir/include/libxmp-lite/xmp.h" ]
}
compVerify() {
[ -d "$compDir/src" ]
}
compEnv() {
envVar LIBXMP_DIR "$compDir"
}

View file

@ -0,0 +1,68 @@
# Apple IIgs C toolchain: llvm816, a W65816 backend for llvm-mos, plus its
# linker (link816 + omfEmit) and runtime. Cloned at a pinned commit and
# built by the repo's own setup.sh; the first build is ~30-60 minutes.
#
# `install.sh --llvm816-dir PATH` links an existing checkout instead (for
# working on llvm816 and JoeyLib together). A linked checkout is managed
# by its owner: the installer only checks it is built.
compDescription="IIgs compiler: llvm816 (clang/llc/llvm-mc for w65816, link816, runtime)"
compGroups="toolchain:iigs"
compRequires="host"
compUrl="https://forge.duensing.digital/AI_Slop/65816-llvm-mos.git"
compCommit="9ca7f1d0de6e23282bed4546441ed9bcced30a5d"
linkedDir="$(cat "$JOEY_STATE/llvm816Dir" 2>/dev/null || true)"
# A linked checkout is built by its owner, so it needs no build packages.
if [ -n "$linkedDir" ]; then
compPin="linked:$linkedDir"
compHostDeps=""
else
compPin="$compCommit"
compHostDeps="build-tools cmake ninja lld git python3 zlib-dev zstd-dev libedit-dev libxml2-dev ncurses-dev"
fi
compInstall() {
if [ -n "$linkedDir" ]; then
[ -x "$linkedDir/tools/llvm-mos-build/bin/clang" ] || die "$linkedDir has no built clang; run its setup.sh first"
rm -rf "$compDir"
ln -s "$linkedDir" "$compDir"
return 0
fi
if [ -L "$compDir" ]; then
rm -f "$compDir"
fi
gitFetchCommit "$compUrl" "$compCommit" "$compDir"
# Host packages come from install.sh (--skip-deps); JoeyLib brings its
# own MAME and does not use Calypsi or GNO/ME.
(cd "$compDir" && bash setup.sh --skip-deps --skip-mame --skip-calypsi --skip-gno --skip-verify)
}
compInstalled() {
[ -x "$compDir/tools/llvm-mos-build/bin/clang" ] &&
[ -x "$compDir/tools/llvm-mos-build/bin/llvm-ar" ] &&
[ -x "$compDir/tools/link816" ] &&
[ -x "$compDir/tools/omfEmit" ] &&
[ -f "$compDir/runtime/crt0Gsos.o" ]
}
compVerify() {
local tmp
local status=1
tmp="$(mktemp -d)"
printf 'int answer(void) { return 42; }\n' > "$tmp/t.c"
if "$compDir/tools/llvm-mos-build/bin/clang" --target=w65816 -O2 -c "$tmp/t.c" -o "$tmp/t.o"; then
[[ "$("$compDir/tools/llvm-mos-build/bin/llvm-objdump" --triple=w65816 -d "$tmp/t.o" 2>/dev/null)" == *lda* ]] && status=0
fi
rm -rf "$tmp"
return $status
}
compEnv() {
envVar LLVM816_ROOT "$compDir"
envVar IIGS_CLANG_BUILD "$JOEY_BUILD_SUPPORT/iigs/clang-build.sh"
}

View file

@ -0,0 +1,49 @@
# MAME 0.264 with JoeyLib's patches (installer/patches/mame), built from
# source as one binary holding only the two drivers the ports need: apple2gs
# (Apple IIgs) and x68k (Sharp X68000). Always built, never taken from the
# distribution: the test gates need the patches. ~20-40 minutes.
compDescription="emulator: MAME 0.264 + JoeyLib patches (apple2gs and x68k drivers)"
compGroups="emulator:iigs emulator:x68000"
compRequires="host"
compHostDeps="build-tools make python3 patch sdl2-dev sdl2-ttf-dev fontconfig-dev pulse-dev asound-dev x11-dev xi-dev xext-dev xinerama-dev gl-dev"
compUrl="https://github.com/mamedev/mame/archive/refs/tags/mame0264.tar.gz"
compSha256="3d9f69ed3ef7c1628d5714c8ae2695ea77b1a652a93347b2703f7c862299376e"
patchDir="$JOEY_PATCHES/mame"
# Editing a patch rebuilds MAME, so the patches are part of the pin.
compPin="$compSha256 $(cat "$patchDir"/*.patch | sha256sum | cut -d' ' -f1)"
compInstall() {
local src
local archive
local p
src="$(freshBuildDir "$compId")"
archive="$(fetchFile "$compUrl" "$compSha256" mame0264.tar.gz)"
extractArchive "$archive" "$src" 1
for p in "$patchDir"/*.patch; do
info "applying $(basename "$p")"
patch -d "$src" -p1 -s -F0 --no-backup-if-mismatch < "$p"
done
make -C "$src" SUBTARGET=joey SOURCES=src/mame/apple/apple2gs.cpp,src/mame/sharp/x68k.cpp \
REGENIE=1 USE_QTDEBUG=0 NOWERROR=1 TOOLS=0 -j"$JOEY_JOBS"
install -D -m 755 "$src/joey" "$compDir/bin/mame"
dropBuildDir "$compId"
}
compInstalled() {
[ -x "$compDir/bin/mame" ]
}
compVerify() {
local drivers
drivers="$("$compDir/bin/mame" -listfull apple2gs x68000 2>/dev/null)"
[[ "$drivers" == *apple2gs* && "$drivers" == *x68000* ]]
}
compEnv() {
envVar MAME "$compDir/bin/mame"
}

View file

@ -0,0 +1,41 @@
# Merlin32 1.1: 65816 assembler, used only for the NinjaTrackerPlus audio
# replayer (third-party Merlin source). The release zip carries a prebuilt
# x86-64 Linux binary.
compDescription="IIgs assembler: Merlin32 1.1 (for the NinjaTrackerPlus replayer)"
compGroups="toolchain:iigs"
compRequires="host"
compHostDeps="unzip"
compUrl="https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32_v1.1.zip"
compSha256="6a4ebd09ed8800a3874b15e35b5d16469a8ad08dd477b9f84cbd5ce8832dc108"
compPin="$compSha256"
compInstall() {
local zip
local src
zip="$(fetchFile "$compUrl" "$compSha256")"
src="$(freshBuildDir "$compId")"
extractArchive "$zip" "$src"
install -D -m 755 "$src/Merlin32_v1.1/Linux/Merlin32" "$compDir/bin/merlin32"
dropBuildDir "$compId"
}
compInstalled() {
[ -x "$compDir/bin/merlin32" ]
}
compVerify() {
local banner
verifyLinked "$compDir/bin"
# Run with no arguments it prints its banner and exits non-zero.
banner="$("$compDir/bin/merlin32" 2>&1 || true)"
[[ "$banner" == *"v 1.1,"* ]]
}
compEnv() {
envVar IIGS_AS "$compDir/bin/merlin32"
}

View file

@ -0,0 +1,52 @@
# Atari ST C toolchain: Vincent Riviere's m68k-atari-mint gcc 15.2 +
# binutils 2.45, with MiNTLib 0.60.1, as prebuilt Linux x86-64 tarballs
# from Thorsten Otto's mirror. Each tarball is rooted at usr/, so stripping
# one component lays all three into one relocatable prefix.
compDescription="Atari ST compiler: m68k-atari-mint gcc 15.2 + binutils 2.45 + MiNTLib 0.60.1"
compGroups="toolchain:atarist"
compRequires="host"
compHostDeps="xz"
compBase="https://tho-otto.de/download/mint"
# file sha256
tarballs=(
"gcc-15.2.0-mint-20250810-bin-linux64.tar.xz 82cce07b1ce3c9f176854e3c5f75ca64ae67d5924da82fc55390d0e59014ae84"
"binutils-2.45-mint-20250812-bin-linux64.tar.xz 0f70905ccec202cb6fa7809ce9749c3fa7bda08a80b06e4efed7e5cfd5f58a5e"
"mintlib-0.60.1-mint-20240718-dev.tar.xz 60e5205cf11d94eaa024a4f11a39107833add073abdddbe1cb8bf46fc3cd0548"
)
compPin="${tarballs[*]}"
compInstall() {
local entry
local name
local sha
local archive
rm -rf "$compDir"
for entry in "${tarballs[@]}"; do
read -r name sha <<< "$entry"
archive="$(fetchFile "$compBase/$name" "$sha")"
extractArchive "$archive" "$compDir" 1
done
}
compInstalled() {
[ -x "$compDir/bin/m68k-atari-mint-gcc" ] && [ -d "$compDir/m68k-atari-mint/sys-root" ]
}
compVerify() {
verifyLinked "$compDir/bin" "$compDir/m68k-atari-mint/bin" "$compDir"/lib64/gcc/*/*
verifyCompiles "$compDir/bin/m68k-atari-mint-gcc" -m68000 -O2
}
compEnv() {
envVar ST_PREFIX "$compDir"
envVar ST_CC "$compDir/bin/m68k-atari-mint-gcc"
envVar ST_AR "$compDir/bin/m68k-atari-mint-ar"
envVar ST_LD "$compDir/bin/m68k-atari-mint-ld"
envPath "$compDir/bin"
}

View file

@ -0,0 +1,36 @@
# NASM: assembler for the DOS port's x86 sources. Any 2.x release works,
# so it comes from the distribution.
compDescription="DOS assembler: NASM (distribution package)"
compGroups="toolchain:dos"
compRequires="host"
compHostDeps="nasm"
compPin="distro"
compInstall() {
command -v nasm >/dev/null || die "nasm is not on PATH after installing host packages"
}
compInstalled() {
command -v nasm >/dev/null
}
compVerify() {
local tmp
local status=1
tmp="$(mktemp -d)"
printf 'bits 32\nret\n' > "$tmp/t.asm"
if nasm -f coff "$tmp/t.asm" -o "$tmp/t.o" && [ -s "$tmp/t.o" ]; then
status=0
fi
rm -rf "$tmp"
return $status
}
compEnv() {
envVar DOS_AS "$(command -v nasm)"
}

View file

@ -0,0 +1,41 @@
# NinjaTrackerPlus: Ninjaforce's IIgs MOD replayer, shipped as Merlin
# source plus the PHP converter tools/joeymod uses for .MOD -> .NTP.
# Redistribution is granted by the copyright holder. The URL is not
# versioned, so the pin is the archive's checksum.
compDescription="IIgs audio: NinjaTrackerPlus replayer source + ntpconverter.php"
compGroups="toolchain:iigs"
compRequires="host"
compHostDeps="unzip php-cli"
compUrl="https://www.ninjaforce.com/downloads/ntpsources.zip"
compSha256="461260e35dc0b10176c05865d01c4e31694f704c0c3a9e5760fc093623dc52fb"
compPin="$compSha256"
compInstall() {
local zip
local s
zip="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$zip" "$compDir"
# The sources ship with CRLF line endings, which Merlin32 rejects as
# "Unknown line".
for s in "$compDir"/*.s; do
sed -i 's/\r$//' "$s"
done
}
compInstalled() {
[ -f "$compDir/ninjatrackerplus.s" ] && [ -f "$compDir/ntpconverter.php" ]
}
compVerify() {
! grep -q $'\r' "$compDir/ninjatrackerplus.s"
}
compEnv() {
envVar IIGS_NTP_DIR "$compDir"
}

View file

@ -0,0 +1,34 @@
# PTPlayer 6.4: Frank Wille's ProTracker replayer for the Amiga, used by
# the Amiga audio HAL. Aminet's URL is not versioned, so the pin is the
# archive's checksum.
compDescription="Amiga audio: PTPlayer 6.4 (ProTracker replayer)"
compGroups="toolchain:amiga"
compRequires="host"
compHostDeps="lhasa"
compUrl="http://aminet.net/mus/play/ptplayer.lha"
compSha256="5ba1285e91413f6b6cb72768c0c31cff90029af6920870d9f589689aa2e2161a"
compPin="$compSha256"
compInstall() {
local lha
lha="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$lha" "$compDir"
}
compInstalled() {
[ -f "$compDir/ptplayer.asm" ]
}
compVerify() {
grep -qi 'ptplayer' "$compDir/ptplayer.asm"
}
compEnv() {
envVar AMIGA_PTPLAYER "$compDir"
}

View file

@ -0,0 +1,30 @@
# Space Taxi's level source: a memory image of the original Commodore 64
# game. Optional -- without it every port still builds, skipping only the
# Space Taxi data -- so it is not part of `all`; install it by name.
compDescription="Space Taxi C64 memory image (private, optional: bundle or import)"
compGroups="staxi optional"
compRequires=""
compHostDeps=""
compPrivateFile="stuff/spacetaxi/raw.bin"
compPin="$(privateSha1For "$compPrivateFile")"
compInstall() {
privateEnsure "$compPrivateFile"
}
compInstalled() {
privatePresent "$compPrivateFile"
}
compVerify() {
privatePresent "$compPrivateFile"
}
compEnv() {
:
}

View file

@ -0,0 +1,31 @@
# X68000 character-generator (font) ROM. Not part of Sharp's free release,
# so it comes from the private bundle or `install.sh import`. MAME's x68000
# driver will not start without it (it warns about the checksum of this
# dump, which is expected).
compDescription="X68000 firmware: CG (font) ROM (private: bundle or import)"
compGroups="emulator:x68000"
compRequires=""
compHostDeps=""
compPrivateFile="toolchains/firmware/x68000/roms/x68000/cgrom.dat"
compPin="$(privateSha1For "$compPrivateFile")"
compInstall() {
privateEnsure "$compPrivateFile"
}
compInstalled() {
privatePresent "$compPrivateFile"
}
compVerify() {
privatePresent "$compPrivateFile"
}
compEnv() {
:
}

View file

@ -0,0 +1,42 @@
# Sharp X68000 firmware from Sharp's own free release (2000, published by
# the X68000 LIBRARY): the original IPL-ROM (all `-bios ipl10` needs) and
# the Human68k 3.02 system disk. The CG (font) ROM is not part of that
# release; it is the private component x68kCgrom.
compDescription="X68000 firmware: Sharp's free IPL-ROM + Human68k 3.02 system disk"
compGroups="emulator:x68000"
compRequires="host"
compHostDeps="lhasa"
compBase="http://retropc.net/x68000/software/sharp"
biosSha256="286564f316e1615c46a23b25536ebd403ab60979f6c4eee21d2287f005a3b309"
humanSha256="da68be5c5cbea871ecb5588010a4d3fe52f26c8c686401027cecc0d1a27dca1a"
compPin="$biosSha256 $humanSha256"
fwDir="$JOEY_FIRMWARE/x68000"
compInstall() {
local tmp
tmp="$(freshBuildDir "$compId")"
extractArchive "$(fetchFile "$compBase/x68bios/X68BIOSE.LZH" "$biosSha256")" "$tmp/bios"
extractArchive "$(fetchFile "$compBase/human302/HUMN302I.LZH" "$humanSha256")" "$tmp/human"
# Name case depends on the extractor (lhasa lowercases MS-DOS archives).
install -D -m 644 "$(find "$tmp/bios" -maxdepth 1 -iname iplrom.dat)" "$fwDir/roms/x68000/iplrom.dat"
install -D -m 644 "$(find "$tmp/human" -maxdepth 1 -iname human302.xdf)" "$fwDir/HUMAN302.XDF"
dropBuildDir "$compId"
}
compInstalled() {
[ -f "$fwDir/roms/x68000/iplrom.dat" ] && [ -f "$fwDir/HUMAN302.XDF" ]
}
compVerify() {
[ "$(sha1sum "$fwDir/roms/x68000/iplrom.dat" | cut -d' ' -f1)" = "0ed038ed2133b9f78c6e37256807424e0d927560" ] &&
[ "$(sha1sum "$fwDir/HUMAN302.XDF" | cut -d' ' -f1)" = "bdee51e80a9ef28d3c336d79ac80e9749f475608" ]
}
compEnv() {
envVar X68K_DIR "$fwDir"
}

View file

@ -0,0 +1,89 @@
# Host packages the components need, by logical name.
#
# Columns: logical-name apt-packages dnf-packages
# Several packages are comma-separated; "-" means the family has no package
# for it (the component must then build it itself or cannot be installed).
# Components list logical names in compHostDeps; `install.sh deps` maps
# them through this table.
# ---- basics every setup needs
build-tools build-essential gcc,gcc-c++,make
make make make
git git git
git-lfs git-lfs git-lfs
curl curl,ca-certificates curl,ca-certificates
tar tar tar
xz xz-utils xz
bzip2 bzip2 bzip2
unzip unzip unzip
wget wget wget
lhasa lhasa lhasa
python3 python3 python3
python3-pil python3-pil python3-pillow
php-cli php-cli php-cli
patch patch patch
# ---- runtime libraries the prebuilt toolchains link against
libfl libfl2 libfl2
# elf2x68k's gcc converts strings to cp932 (Shift-JIS) through iconv;
# Debian ships every converter in libc6, Fedora splits most of them off.
gconv-cp932 libc6 glibc-gconv-extra
# ---- GNU toolchain builds (amiga-gcc)
bison bison bison
flex flex flex
m4 m4 m4
texinfo texinfo texinfo
autoconf autoconf autoconf
automake automake automake
gettext gettext gettext
rsync rsync rsync
procps procps procps-ng
which debianutils which
diffutils diffutils diffutils
gmp-dev libgmp-dev gmp-devel
mpfr-dev libmpfr-dev mpfr-devel
mpc-dev libmpc-dev libmpc-devel
ncurses-dev libncurses-dev ncurses-devel
readline-dev libreadline-dev readline-devel
# ---- LLVM builds (llvm816)
cmake cmake cmake
ninja ninja-build ninja-build
lld lld lld
zlib-dev zlib1g-dev zlib-devel
zstd-dev libzstd-dev libzstd-devel
libedit-dev libedit-dev libedit-devel
libxml2-dev libxml2-dev libxml2-devel
# ---- assemblers
nasm nasm nasm
# ---- MAME build
sdl2-dev libsdl2-dev SDL2-devel
sdl2-ttf-dev libsdl2-ttf-dev SDL2_ttf-devel
fontconfig-dev libfontconfig-dev fontconfig-devel
pulse-dev libpulse-dev pulseaudio-libs-devel
asound-dev libasound2-dev alsa-lib-devel
x11-dev libx11-dev libX11-devel
xi-dev libxi-dev libXi-devel
xext-dev libxext-dev libXext-devel
xinerama-dev libxinerama-dev libXinerama-devel
gl-dev libgl-dev mesa-libGL-devel
# ---- emulators packaged by the distribution
hatari hatari hatari
fs-uae fs-uae -
# ---- running emulators headless (the test gates)
xvfb xvfb xorg-x11-server-Xvfb
xdotool xdotool xdotool
imagemagick imagemagick ImageMagick
pulse-utils pulseaudio-utils pulseaudio-utils
# ---- FS-UAE source build (dnf systems, which do not package it)
pkg-config pkg-config pkgconf-pkg-config
zip zip zip
glib-dev libglib2.0-dev glib2-devel
openal-dev libopenal-dev openal-soft-devel
png-dev libpng-dev libpng-devel

9
installer/files.sha1 Normal file
View file

@ -0,0 +1,9 @@
# Files JoeyLib needs that are not downloaded from any public source. They
# come from the project owner's private bundle (JOEY_BUNDLE_URL, set in
# installer/bundle.conf) or from `./install.sh import PATH...`,
# which finds them by checksum in any files or .zip archives you point it at.
#
# sha1 destination (repository-relative) what it is
3b7f1493b27e212830f989f26ca76c02049f09ca toolchains/firmware/amiga/kickstart.rom Amiga Kickstart 3.1 r40.63 (A500/A600/A2000)
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)

140
installer/lib/bundle.sh Normal file
View file

@ -0,0 +1,140 @@
# Private files: the ones in files.sha1, which are never fetched from a
# public source. They arrive from the owner's bundle (JOEY_BUNDLE_URL) or
# from `install.sh import`. Sourced after lib/common.sh.
JOEY_FILES_TABLE="$JOEY_INSTALLER/files.sha1"
JOEY_BUNDLE_CONF="$JOEY_INSTALLER/bundle.conf"
# privateTable -- print "sha1 destination" for every private file.
privateTable() {
awk '!/^#/ && NF >= 2 { print $1, $2 }' "$JOEY_FILES_TABLE"
}
# privateSha1For DEST -- the checksum files.sha1 expects at DEST.
privateSha1For() {
awk -v d="$1" '!/^#/ && $2 == d { print $1 }' "$JOEY_FILES_TABLE"
}
# privatePresent DEST -- true when DEST holds the expected file.
privatePresent() {
local want
want="$(privateSha1For "$1")"
[ -n "$want" ] || die "no files.sha1 entry for $1"
[ -f "$JOEY_REPO/$1" ] && [ "$(sha1sum "$JOEY_REPO/$1" | cut -d' ' -f1)" = "$want" ]
}
# importOneFile FILE -- install FILE if its checksum is a private file's.
# Prints the destination it was installed to.
importOneFile() {
local sum
local dest
sum="$(sha1sum "$1" | cut -d' ' -f1)"
dest="$(privateTable | awk -v s="$sum" '$1 == s { print $2 }')"
[ -n "$dest" ] || return 0
# Importing a file that is already in place is a no-op, not an error.
if [ "$(realpath "$1")" != "$(realpath -m "$JOEY_REPO/$dest")" ]; then
install -D -m 644 "$1" "$JOEY_REPO/$dest"
fi
printf '%s\n' "$dest"
}
# importPaths PATH... -- scan files, directories and .zip archives and
# install every private file found. Prints what was installed.
importPaths() {
local path
local file
local tmp
tmp="$(mktemp -d)"
for path in "$@"; do
[ -e "$path" ] || die "no such file or directory: $path"
while IFS= read -r -d '' file; do
case "$file" in
*.zip|*.ZIP)
rm -rf "$tmp/zip"
mkdir -p "$tmp/zip"
if unzip -qq -o "$file" -d "$tmp/zip" 2>/dev/null; then
find "$tmp/zip" -type f -print0 | while IFS= read -r -d '' inner; do
importOneFile "$inner"
done
fi
;;
*)
importOneFile "$file"
;;
esac
done < <(find "$path" -type f -print0)
done
rm -rf "$tmp"
}
# bundleUrl -- the configured bundle URL, or nothing.
bundleUrl() {
if [ -n "${JOEY_BUNDLE_URL:-}" ]; then
printf '%s\n' "$JOEY_BUNDLE_URL"
elif [ -f "$JOEY_BUNDLE_CONF" ]; then
(. "$JOEY_BUNDLE_CONF" && printf '%s\n' "${JOEY_BUNDLE_URL:-}")
fi
}
# bundleFetch -- download the owner's bundle (if one is configured) and
# import what it contains. Its checksum is not pinned -- the owner updates
# it -- but every file taken from it is checked against files.sha1.
bundleFetch() {
local url
local tmp
url="$(bundleUrl)"
[ -n "$url" ] || return 1
tmp="$(mktemp -d)"
info "fetching the private bundle"
curl -fL --retry 3 --connect-timeout 30 --progress-bar -o "$tmp/bundle.tar.gz" "$url" || { rm -rf "$tmp"; die "could not download the private bundle from $url"; }
mkdir -p "$tmp/x"
tar -xzf "$tmp/bundle.tar.gz" -C "$tmp/x" || { rm -rf "$tmp"; die "the private bundle is not a .tar.gz"; }
importPaths "$tmp/x" >/dev/null
rm -rf "$tmp"
}
# privateEnsure DEST -- make DEST present, from the bundle if configured,
# or stop and explain how to supply it.
privateEnsure() {
local what
privatePresent "$1" && return 0
bundleFetch || true
privatePresent "$1" && return 0
what="$(awk -v d="$1" '!/^#/ && $2 == d { $1 = ""; $2 = ""; sub(/^ +/, ""); print }' "$JOEY_FILES_TABLE")"
die "$1 is missing: $what (sha1 $(privateSha1For "$1")).
This file is not downloaded from public sources. Supply your copy with
./install.sh import PATH (files, folders or .zip archives)
or set JOEY_BUNDLE_URL in installer/bundle.conf to your private bundle."
}
# bundleCreate OUTFILE -- pack every private file present into OUTFILE
# (a .tar.gz) for the owner to host.
bundleCreate() {
local out="$1"
local dest
local sum
local tmp
local count=0
tmp="$(mktemp -d)"
while read -r sum dest; do
if privatePresent "$dest"; then
install -D -m 644 "$JOEY_REPO/$dest" "$tmp/$dest"
count=$((count + 1))
else
warn "not bundled (not present here): $dest"
fi
done < <(privateTable)
[ "$count" -gt 0 ] || { rm -rf "$tmp"; die "none of the private files are present to bundle"; }
tar -czf "$out" -C "$tmp" .
rm -rf "$tmp"
ok "bundled $count private files into $out"
}

339
installer/lib/common.sh Normal file
View file

@ -0,0 +1,339 @@
# Shared helpers for install.sh and the component files in
# installer/components/. Sourced, not executed.
#
# Tracked (the recipes):
# install.sh the command line
# installer/lib/ these helpers
# installer/components/<id> one recipe per component
# installer/patches/ patches applied to sources the installer builds
# make/support/ build helper scripts the makefiles call
# Generated (everything under toolchains/, which can be deleted at any time
# and re-created by install.sh):
# toolchains/cache/downloads/ sha256-verified downloads
# toolchains/cache/build/<id>/ scratch build trees, deleted after install
# toolchains/opt/<id>/ installed components
# toolchains/firmware/ ROM / OS images for the emulators
# toolchains/state/ stamps and settings
# toolchains/env.sh, config.mk the tool paths, for shells and for make
JOEY_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
JOEY_INSTALLER="$JOEY_REPO/installer"
JOEY_COMPONENTS="$JOEY_INSTALLER/components"
JOEY_PATCHES="$JOEY_INSTALLER/patches"
JOEY_BUILD_SUPPORT="$JOEY_REPO/make/support"
JOEY_TOOLCHAINS="$JOEY_REPO/toolchains"
JOEY_CACHE="$JOEY_TOOLCHAINS/cache/downloads"
JOEY_BUILD_ROOT="$JOEY_TOOLCHAINS/cache/build"
JOEY_OPT="$JOEY_TOOLCHAINS/opt"
JOEY_FIRMWARE="$JOEY_TOOLCHAINS/firmware"
JOEY_STATE="$JOEY_TOOLCHAINS/state"
JOEY_STAMPS="$JOEY_STATE/stamps"
JOEY_JOBS="${JOEY_JOBS:-$(nproc 2>/dev/null || echo 2)}"
JOEY_KEEP_BUILD="${JOEY_KEEP_BUILD:-0}"
JOEY_ASSUME_YES="${JOEY_ASSUME_YES:-0}"
JOEY_FORCE="${JOEY_FORCE:-0}"
# Never prompt for credentials: a missing or private repo must fail fast
# with a clear message instead of hanging on a username prompt.
export GIT_TERMINAL_PROMPT=0
if [ -t 1 ]; then
JOEY_C_RESET=$'\033[0m'
JOEY_C_BOLD=$'\033[1m'
JOEY_C_RED=$'\033[31m'
JOEY_C_GREEN=$'\033[32m'
JOEY_C_YELLOW=$'\033[33m'
JOEY_C_CYAN=$'\033[36m'
else
JOEY_C_RESET=""
JOEY_C_BOLD=""
JOEY_C_RED=""
JOEY_C_GREEN=""
JOEY_C_YELLOW=""
JOEY_C_CYAN=""
fi
info() {
printf '%s[*]%s %s\n' "$JOEY_C_CYAN" "$JOEY_C_RESET" "$*"
}
ok() {
printf '%s[ok]%s %s\n' "$JOEY_C_GREEN" "$JOEY_C_RESET" "$*"
}
warn() {
printf '%s[!!]%s %s\n' "$JOEY_C_YELLOW" "$JOEY_C_RESET" "$*" >&2
}
die() {
printf '%s[xx]%s %s\n' "$JOEY_C_RED" "$JOEY_C_RESET" "$*" >&2
exit 1
}
header() {
printf '\n%s== %s ==%s\n' "$JOEY_C_BOLD" "$*" "$JOEY_C_RESET"
}
sha256Of() {
sha256sum "$1" | cut -d' ' -f1
}
# fetchFile URL SHA256 [NAME] -- download URL into the cache as NAME
# (default: the URL's basename), verify it, and print its path. A cached
# file is reused only if its checksum still matches; downloads go to a
# .part file first, so an interrupted fetch is never mistaken for a
# complete one.
fetchFile() {
local url="$1"
local want="$2"
local name="${3:-$(basename "$url")}"
local dest="$JOEY_CACHE/$name"
local got
mkdir -p "$JOEY_CACHE"
if [ -f "$dest" ]; then
got="$(sha256Of "$dest")"
if [ "$got" = "$want" ]; then
printf '%s\n' "$dest"
return 0
fi
warn "cached $name has the wrong checksum; downloading again"
rm -f "$dest"
fi
info "downloading $name" >&2
curl -fL --retry 3 --connect-timeout 30 --progress-bar -o "$dest.part" "$url" \
|| die "download failed: $url"
got="$(sha256Of "$dest.part")"
if [ "$got" != "$want" ]; then
rm -f "$dest.part"
die "checksum mismatch for $url
expected $want
got $got
The upstream file changed. Check it, then update the pin in the component file."
fi
mv "$dest.part" "$dest"
printf '%s\n' "$dest"
}
# gitFetchCommit URL SHA DIR -- make DIR a shallow checkout of exactly SHA.
# Reuses an existing clone when it can.
gitFetchCommit() {
local url="$1"
local sha="$2"
local dir="$3"
if [ -d "$dir/.git" ] && [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$sha" ]; then
return 0
fi
if [ ! -d "$dir/.git" ]; then
rm -rf "$dir"
mkdir -p "$dir"
git -C "$dir" init -q
git -C "$dir" remote add origin "$url"
else
git -C "$dir" remote set-url origin "$url"
fi
git -C "$dir" fetch -q --depth=1 origin "$sha" || die "could not fetch $sha from $url"
git -C "$dir" checkout -q -f --detach FETCH_HEAD
}
# lhaCommand -- the LHA extractor: the lhasa package installs it as lhasa
# on Debian and Ubuntu but as lha on Fedora.
lhaCommand() {
command -v lhasa || command -v lha || die "no lhasa or lha command (install the lhasa package)"
}
# extractArchive ARCHIVE DEST [STRIP] -- unpack ARCHIVE into DEST,
# dropping STRIP leading path components (tar formats only).
extractArchive() {
local archive="$1"
local dest="$2"
local strip="${3:-0}"
mkdir -p "$dest"
case "$archive" in
*.tar.gz|*.tgz) tar -xzf "$archive" -C "$dest" --strip-components="$strip" ;;
*.tar.bz2) tar -xjf "$archive" -C "$dest" --strip-components="$strip" ;;
*.tar.xz) tar -xJf "$archive" -C "$dest" --strip-components="$strip" ;;
*.zip) unzip -oq "$archive" -d "$dest" ;;
*.lha|*.lzh|*.LZH) (cd "$dest" && "$(lhaCommand)" xq "$archive") ;;
*) die "don't know how to extract $archive" ;;
esac
}
# freshBuildDir ID -- print an empty scratch directory for building ID.
freshBuildDir() {
local dir="$JOEY_BUILD_ROOT/$1"
rm -rf "$dir"
mkdir -p "$dir"
printf '%s\n' "$dir"
}
# dropBuildDir ID -- delete ID's scratch tree unless --keep-build.
dropBuildDir() {
if [ "$JOEY_KEEP_BUILD" = "1" ]; then
info "keeping build tree $JOEY_BUILD_ROOT/$1"
else
rm -rf "${JOEY_BUILD_ROOT:?}/$1"
fi
}
# verifyCompiles CC [FLAGS...] -- compile a trivial C file to an object;
# the smoke test the cross-compiler components share.
verifyCompiles() {
local tmp
local status=1
tmp="$(mktemp -d)"
printf 'int answer(int x) { return x * 42; }\n' > "$tmp/t.c"
if "$@" -c "$tmp/t.c" -o "$tmp/t.o" && [ -s "$tmp/t.o" ]; then
status=0
fi
rm -rf "$tmp"
return $status
}
# verifyLinked DIR... -- every dynamically linked executable directly in
# the DIRs finds its shared libraries. A prebuilt toolchain can need one the
# host lacks (djgpp's ar wants the flex runtime, libfl.so.2), and a
# compile-only smoke test never runs ar.
verifyLinked() {
local dir
local f
local out
local line
local status=0
for dir in "$@"; do
for f in "$dir"/*; do
[ -f "$f" ] && [ -x "$f" ] || continue
# Scripts and static binaries are not dynamic executables.
out="$(ldd "$f" 2>/dev/null)" || continue
while IFS= read -r line; do
if [[ "$line" == *"not found"* ]]; then
warn "${f#"$JOEY_REPO"/} needs ${line#"${line%%[![:space:]]*}"}"
status=1
fi
done <<< "$out"
done
done
return $status
}
# ---- stamps ---------------------------------------------------------------
# A stamp records the pin a component was installed from, so moving the pin
# in the component file makes the component out of date.
stampRead() {
cat "$JOEY_STAMPS/$1" 2>/dev/null || true
}
stampWrite() {
mkdir -p "$JOEY_STAMPS"
printf '%s\n' "$2" > "$JOEY_STAMPS/$1"
}
stampClear() {
rm -f "$JOEY_STAMPS/$1"
}
# ---- components -------------------------------------------------------------
# Every component file defines:
# compDescription one line for `list`
# compGroups groups it belongs to, e.g. "toolchain:amiga"
# compRequires components that must be installed first
# compHostDeps logical host package names (see distroPackages.txt)
# compPin anything that changes when the component must be rebuilt
# compInstall installs into $compDir
# compInstalled cheap check that the install is present
# compVerify smoke test for `doctor`
# compEnv emits the variables it provides (envVar / envPath)
# and a private-file component (lib/bundle.sh) also sets compPrivateFile.
# The file is always sourced in a subshell, so components can use the same
# function names without colliding.
componentIds() {
local f
for f in "$JOEY_COMPONENTS"/*.sh; do
basename "$f" .sh
done
}
componentFile() {
local file="$JOEY_COMPONENTS/$1.sh"
[ -f "$file" ] || die "unknown component: $1"
printf '%s\n' "$file"
}
# compQuery ID VAR -- print a component file's metadata variable.
compQuery() {
local file
file="$(componentFile "$1")"
(
compId="$1"
compDir="$JOEY_OPT/$1"
# shellcheck disable=SC1090
source "$file"
printf '%s\n' "${!2:-}"
)
}
# compRun ID FUNCTION [ARGS...] -- run one of a component's functions.
# It runs in a separate bash PROCESS, not a subshell: callers check the
# result (compRun ... || die), and bash disables set -e inside anything
# whose status is being checked, subshells included -- a failing step in
# the middle of compInstall would then be skipped silently.
compRun() {
local file
file="$(componentFile "$1")"
JOEY_JOBS="$JOEY_JOBS" JOEY_KEEP_BUILD="$JOEY_KEEP_BUILD" \
JOEY_ASSUME_YES="$JOEY_ASSUME_YES" JOEY_FORCE="$JOEY_FORCE" \
bash -c '
set -euo pipefail
source "$1/common.sh"
source "$1/distro.sh"
source "$1/bundle.sh"
compId="$2"
compDir="$JOEY_OPT/$2"
mkdir -p "$JOEY_OPT"
source "$3"
shift 3
"$@"
' compRun "$JOEY_INSTALLER/lib" "$1" "$file" "${@:2}"
}
# compIsCurrent ID -- installed from the current pin and still present.
compIsCurrent() {
[ -n "$(stampRead "$1")" ] || return 1
[ "$(stampRead "$1")" = "$(compQuery "$1" compPin)" ] || return 1
compRun "$1" compInstalled >/dev/null 2>&1
}
# Used inside compEnv to publish a variable / a PATH directory.
envVar() {
printf 'var\t%s\t%s\n' "$1" "$2"
}
envPath() {
printf 'path\t%s\n' "$1"
}

61
installer/lib/config.sh Normal file
View file

@ -0,0 +1,61 @@
# Generates toolchains/env.sh (for shells and scripts) and
# toolchains/config.mk (for make) from the compEnv output of every installed
# component, so both see exactly the same tool paths. Sourced after
# lib/common.sh.
# writeConfig -- regenerate both files from the installed components.
writeConfig() {
local id
local kind
local name
local value
local vars=()
local paths=()
local envTmp
local mkTmp
case "$JOEY_REPO" in
*" "*) die "the repository path contains a space ($JOEY_REPO); make cannot build from there" ;;
esac
for id in $(componentIds); do
[ -n "$(stampRead "$id")" ] || continue
while IFS=$'\t' read -r kind name value; do
case "$kind" in
var) vars+=("$name=$value") ;;
path) paths+=("$name") ;;
esac
done < <(compRun "$id" compEnv)
done
mkdir -p "$JOEY_TOOLCHAINS"
envTmp="$(mktemp "$JOEY_TOOLCHAINS/env.sh.XXXXXX")"
mkTmp="$(mktemp "$JOEY_TOOLCHAINS/config.mk.XXXXXX")"
{
printf '# Generated by install.sh -- do not edit. For a shell:\n'
printf '# source toolchains/env.sh\n'
printf "export JOEY_TOOLCHAINS='%s'\n" "$JOEY_TOOLCHAINS"
for value in "${vars[@]}"; do
printf "export %s='%s'\n" "${value%%=*}" "${value#*=}"
done
# Prepend each directory once, even when this file is sourced again.
for value in "${paths[@]}"; do
printf 'case ":$PATH:" in *":%s:"*) ;; *) PATH='"'"'%s'"'"':"$PATH" ;; esac\n' "$value" "$value"
done
printf 'export PATH\n'
} > "$envTmp"
{
printf '# Generated by install.sh -- do not edit. make reads it by itself.\n'
printf 'export JOEY_TOOLCHAINS := %s\n' "$JOEY_TOOLCHAINS"
for value in "${vars[@]}"; do
printf 'export %s := %s\n' "${value%%=*}" "${value#*=}"
done
if [ ${#paths[@]} -gt 0 ]; then
printf 'export PATH := %s:$(PATH)\n' "$(IFS=:; printf '%s' "${paths[*]}")"
fi
} > "$mkTmp"
chmod 644 "$envTmp" "$mkTmp"
mv "$envTmp" "$JOEY_TOOLCHAINS/env.sh"
mv "$mkTmp" "$JOEY_TOOLCHAINS/config.mk"
}

103
installer/lib/distro.sh Normal file
View file

@ -0,0 +1,103 @@
# Host package handling: maps the logical names in distroPackages.txt to
# apt or dnf packages, reports which are missing, and installs them.
# Sourced after lib/common.sh.
JOEY_PACKAGE_MAP="$JOEY_INSTALLER/distroPackages.txt"
# distroFamily -- print apt, dnf or unknown.
distroFamily() {
local id=""
local like=""
if [ -r /etc/os-release ]; then
id="$(. /etc/os-release && printf '%s' "${ID:-}")"
like="$(. /etc/os-release && printf '%s' "${ID_LIKE:-}")"
fi
case " $id $like " in
*" debian "*|*" ubuntu "*) printf 'apt\n' ;;
*" fedora "*|*" rhel "*|*" centos "*|*" rocky "*) printf 'dnf\n' ;;
*) printf 'unknown\n' ;;
esac
}
# distroPackageNames LOGICAL... -- print the real package names, one per line.
distroPackageNames() {
local family
local column
local name
local row
family="$(distroFamily)"
case "$family" in
apt) column=2 ;;
dnf) column=3 ;;
*) die "unsupported Linux distribution (need apt or dnf); install host packages by hand: $*" ;;
esac
for name in "$@"; do
row="$(awk -v n="$name" -v c="$column" '$1 == n { print $c }' "$JOEY_PACKAGE_MAP")"
[ -n "$row" ] || die "no entry for host dependency '$name' in distroPackages.txt"
[ "$row" != "-" ] || die "host dependency '$name' has no $family package"
printf '%s\n' "${row//,/$'\n'}"
done
}
# distroMissing PACKAGE... -- print the packages that are not installed.
distroMissing() {
local family
local pkg
local native=""
family="$(distroFamily)"
[ "$family" = "apt" ] && native="$(dpkg --print-architecture)"
for pkg in "$@"; do
case "$family" in
apt)
aptInstalled "$pkg" "$native" || printf '%s\n' "$pkg"
;;
dnf)
rpm -q "$pkg" >/dev/null 2>&1 || printf '%s\n' "$pkg"
;;
esac
done
}
# aptInstalled PACKAGE NATIVE-ARCH -- PACKAGE is installed for this machine.
# dpkg-query prints one line per architecture present (libc6 on a system
# with i386 multiarch has two), so look for a native or arch-all line.
aptInstalled() {
local arch
local status
while read -r arch status; do
if [ "$status" = "installed" ] && { [ "$arch" = "$2" ] || [ "$arch" = "all" ]; }; then
return 0
fi
done <<< "$(dpkg-query -W -f='${Architecture} ${db:Status-Status}\n' "$1" 2>/dev/null || true)"
return 1
}
# distroInstall PACKAGE... -- install with one sudo call (none when already
# root, as in containers and CI, which often have no sudo at all).
distroInstall() {
local family
local yes=()
local asRoot=()
[ $# -gt 0 ] || return 0
family="$(distroFamily)"
[ "$JOEY_ASSUME_YES" = "1" ] && yes=(-y)
[ "$(id -u)" = "0" ] || asRoot=(sudo)
info "installing host packages: $*"
case "$family" in
apt)
"${asRoot[@]}" apt-get update
"${asRoot[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install "${yes[@]}" --no-install-recommends "$@"
;;
dnf)
"${asRoot[@]}" dnf install "${yes[@]}" "$@"
;;
*)
die "unsupported Linux distribution; install by hand: $*"
;;
esac
}

View file

@ -1,5 +1,5 @@
--- /tmp/apple2gs.cpp.orig 2026-08-13 15:22:16.445448309 -0500 --- a/src/mame/apple/apple2gs.cpp
+++ toolchains/cache/mame-mame0264/src/mame/apple/apple2gs.cpp 2026-08-13 15:22:16.497448131 -0500 +++ b/src/mame/apple/apple2gs.cpp
@@ -3839,8 +3839,13 @@ @@ -3839,8 +3839,13 @@
ADDRESS_MAP_BANK(config, A2GS_C300_TAG).set_map(&apple2gs_state::c300bank_map).set_options(ENDIANNESS_LITTLE, 8, 32, 0x100); ADDRESS_MAP_BANK(config, A2GS_C300_TAG).set_map(&apple2gs_state::c300bank_map).set_options(ENDIANNESS_LITTLE, 8, 32, 0x100);

View file

@ -0,0 +1,26 @@
--- a/src/devices/machine/z80scc.cpp
+++ b/src/devices/machine/z80scc.cpp
@@ -1045,7 +1045,7 @@
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
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 @@
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.
+ 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
//int m_rx_fifo // receive FIFO pointer
int m_rx_fifo_rp; // receive FIFO read pointer

View file

@ -1,5 +1,5 @@
--- a/src/mame/sharp/x68k_crtc.h 2026-08-04 16:07:13.182675756 -0500 --- a/src/mame/sharp/x68k_crtc.h
+++ b/src/mame/sharp/x68k_crtc.h 2026-08-04 16:32:58.889623301 -0500 +++ b/src/mame/sharp/x68k_crtc.h
@@ -61,7 +61,7 @@ @@ -61,7 +61,7 @@
private: private:
// internal helpers // internal helpers
@ -30,8 +30,8 @@
}; };
class vinas_device : public x68k_crtc_device class vinas_device : public x68k_crtc_device
--- a/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:07:13.181675761 -0500 --- a/src/mame/sharp/x68k_crtc.cpp
+++ b/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:32:58.889623301 -0500 +++ b/src/mame/sharp/x68k_crtc.cpp
@@ -25,6 +25,8 @@ @@ -25,6 +25,8 @@
, m_clock_69m(0) , m_clock_69m(0)
, m_clock_50m(0) , m_clock_50m(0)

View file

@ -1,19 +1,108 @@
From ab6b6fc9e7971b367ec54d565cf10f04e71109ec Mon Sep 17 00:00:00 2001 --- a/src/mame/sharp/x68k.h
From: Patrick Mackinlay <pmackinlay@hotmail.com> +++ b/src/mame/sharp/x68k.h
Date: Tue, 14 Jan 2025 13:53:33 +0700 @@ -20,7 +20,7 @@
Subject: [PATCH] x68k: add serial mouse #include "cpu/m68000/m68000.h"
#include "cpu/m68000/m68030.h"
#include "imagedev/floppy.h"
-#include "machine/8530scc.h"
+#include "machine/z80scc.h"
#include "machine/hd63450.h"
#include "machine/i8255.h"
#include "machine/mb87030.h"
@@ -64,9 +64,6 @@
, m_expansion(*this, "exp%u", 1U)
, m_adpcm_out(*this, {"adpcm_outl", "adpcm_outr"})
, m_options(*this, "options")
- , m_mouse1(*this, "mouse1")
- , m_mouse2(*this, "mouse2")
- , m_mouse3(*this, "mouse3")
, m_eject_drv_out(*this, "eject_drv%u", 0U)
, m_ctrl_drv_out(*this, "ctrl_drv%u", 0U)
, m_access_drv_out(*this, "access_drv%u", 0U)
@@ -100,7 +97,7 @@
required_device<palette_device> m_pcgpalette;
required_device<mc68901_device> m_mfpdev;
required_device<rp5c15_device> m_rtc;
- required_device<scc8530_legacy_device> m_scc;
+ required_device<scc8530_device> m_scc;
required_device<ym2151_device> m_ym2151;
required_device<i8255_device> m_ppi;
required_device<screen_device> m_screen;
@@ -111,9 +108,6 @@
required_device_array<filter_volume_device, 2> m_adpcm_out;
--- required_ioport m_options;
src/mame/sharp/x68k.cpp | 169 ++++------------------------------ - required_ioport m_mouse1;
src/mame/sharp/x68k.h | 24 +---- - required_ioport m_mouse2;
src/mame/sharp/x68k_mouse.cpp | 103 +++++++++++++++++++++ - required_ioport m_mouse3;
src/mame/sharp/x68k_mouse.h | 36 ++++++++
4 files changed, 158 insertions(+), 174 deletions(-)
create mode 100644 src/mame/sharp/x68k_mouse.cpp
create mode 100644 src/mame/sharp/x68k_mouse.h
diff --git a/src/mame/sharp/x68k.cpp b/src/mame/sharp/x68k.cpp output_finder<4> m_eject_drv_out;
index 8e5def0a501a6..c6787eeef393d 100644 output_finder<4> m_ctrl_drv_out;
@@ -178,24 +172,21 @@
uint8_t hdcvector = 0;
uint8_t prnvector = 0;
} m_ioc;
- struct
- {
- int inputtype = 0; // determines which input is to be received
- bool irqactive = false; // true if IRQ is being serviced
- uint8_t irqvector = 0;
- char last_mouse_x = 0; // previous mouse x-axis value
- char last_mouse_y = 0; // previous mouse y-axis value
- int bufferempty = 0; // non-zero if buffer is empty
- } m_mouse;
uint8_t m_ppi_portc = 0;
bool m_dmac_int = false;
bool m_mfp_int = false;
+ bool m_scc_int = false;
bool m_exp_irq2[2]{};
bool m_exp_irq4[2]{};
bool m_exp_nmi[2]{};
uint8_t m_current_ipl = 0;
int m_led_state = 0;
- emu_timer* m_mouse_timer = nullptr;
+ // Extra cycles charged per VRAM access, over and above a main-memory
+ // access, in 16.16 fixed point. Set from measured values in machine_start
+ // for the machines those measurements cover; 0 (unmodelled) otherwise.
+ uint32_t m_gvram_wait = 0;
+ uint32_t m_tvram_wait = 0;
+ uint32_t m_vram_wait_accum = 0; // 16.16 carry between accesses
emu_timer* m_led_timer = nullptr;
unsigned char m_scc_prev = 0;
emu_timer* m_fdc_tc = nullptr;
@@ -233,7 +224,6 @@
void dma_irq(int state);
void dma_end(offs_t offset, uint8_t data);
- int read_mouse();
void set_adpcm();
void fm_irq(int state);
@@ -241,8 +231,6 @@
template <int N> void irq4_line(int state);
template <int N> void nmi_line(int state);
- void scc_w(offs_t offset, uint16_t data);
- uint16_t scc_r(offs_t offset);
void fdc_w(offs_t offset, uint16_t data);
uint16_t fdc_r(offs_t offset);
void ioc_w(offs_t offset, uint16_t data);
@@ -265,6 +253,15 @@
uint16_t exp_r(offs_t offset, uint16_t mem_mask = ~0);
void exp_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+ // VRAM wait states. The X68000's video RAM is slower than main memory;
+ // MAME charged nothing for it, so VRAM-bound code ran optimistically
+ // fast. These trampolines charge the difference and forward to the CRTC.
+ void charge_vram_wait(uint32_t increment);
+ uint16_t gvram_wait_r(offs_t offset);
+ void gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+ uint16_t tvram_wait_r(offs_t offset);
+ void tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+
uint16_t spritereg_r(offs_t offset);
void spritereg_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
uint16_t spriteram_r(offs_t offset);
--- a/src/mame/sharp/x68k.cpp --- a/src/mame/sharp/x68k.cpp
+++ b/src/mame/sharp/x68k.cpp +++ b/src/mame/sharp/x68k.cpp
@@ -119,6 +119,7 @@ @@ -119,6 +119,7 @@
@ -24,7 +113,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
#include "machine/nvram.h" #include "machine/nvram.h"
@@ -174,127 +175,6 @@ TIMER_CALLBACK_MEMBER(x68k_state::led_callback) @@ -174,127 +175,6 @@
} }
@ -152,7 +241,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
void x68k_state::set_adpcm() void x68k_state::set_adpcm()
{ {
uint32_t rate = adpcm_div[m_adpcm.rate]; uint32_t rate = adpcm_div[m_adpcm.rate];
@@ -833,7 +713,7 @@ void x68k_state::update_ipl() @@ -837,7 +717,7 @@
new_ipl = 7; new_ipl = 7;
else if (m_mfp_int) else if (m_mfp_int)
new_ipl = 6; new_ipl = 6;
@ -161,7 +250,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
new_ipl = 5; new_ipl = 5;
else if (m_exp_irq4[0] || m_exp_irq4[1]) else if (m_exp_irq4[0] || m_exp_irq4[1])
new_ipl = 4; new_ipl = 4;
@@ -946,18 +826,6 @@ uint8_t x68k_state::iack4() @@ -950,18 +830,6 @@
return 0x18; // spurious interrupt return 0x18; // spurious interrupt
} }
@ -180,7 +269,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
void x68k_state::cpu_space_map(address_map &map) void x68k_state::cpu_space_map(address_map &map)
{ {
map.global_mask(0xffffff); map.global_mask(0xffffff);
@@ -965,7 +833,7 @@ void x68k_state::cpu_space_map(address_map &map) @@ -969,7 +837,7 @@
map(0xfffff5, 0xfffff5).r(FUNC(x68k_state::iack2)); map(0xfffff5, 0xfffff5).r(FUNC(x68k_state::iack2));
map(0xfffff7, 0xfffff7).r(m_hd63450, FUNC(hd63450_device::iack)); map(0xfffff7, 0xfffff7).r(m_hd63450, FUNC(hd63450_device::iack));
map(0xfffff9, 0xfffff9).r(FUNC(x68k_state::iack4)); map(0xfffff9, 0xfffff9).r(FUNC(x68k_state::iack4));
@ -189,7 +278,90 @@ index 8e5def0a501a6..c6787eeef393d 100644
map(0xfffffd, 0xfffffd).r(m_mfpdev, FUNC(mc68901_device::get_vector)); map(0xfffffd, 0xfffffd).r(m_mfpdev, FUNC(mc68901_device::get_vector));
map(0xffffff, 0xffffff).lr8(NAME([] () { return m68000_base_device::autovector(7); })); map(0xffffff, 0xffffff).lr8(NAME([] () { return m68000_base_device::autovector(7); }));
} }
@@ -1005,7 +873,7 @@ void x68k_state::x68k_base_map(address_map &map) @@ -992,12 +860,80 @@
// Is this an undocumented MB89352 feature, an ASIC register, an original code bug or a bad dump?
}
+// VRAM wait states.
+//
+// The 68000 in an X68000 does not reach video RAM as fast as it reaches main
+// memory, and MAME modelled no wait at all -- so any VRAM-bound inner loop ran
+// optimistically fast, which is exactly the code a graphics library cares
+// about. The figures used here are Makoto Kamada's real-hardware measurements
+// distributed with XEiJ (credited there to uchopon, tnb and ita), taken by
+// timing tst.w against each region on five 10 MHz X68000 variants:
+//
+// region tst.w time (us) derived wait (cycles)
+// main memory 1.642 .. 1.663 0.123
+// GVRAM 1.734 .. 1.770 1.145
+// TVRAM 1.829 .. 1.850 2.046
+//
+// MAME charges nothing for main memory, so what is added here is the wait
+// RELATIVE to main memory: 1.145 - 0.123 = 1.022 cycles for GVRAM and
+// 2.046 - 0.123 = 1.923 for TVRAM. Cross-check against the raw measurements:
+// GVRAM - main memory averages ~1.03 cycles and TVRAM - main memory ~1.92
+// across the five machines, which agrees.
+//
+// Main memory's own 0.123 cycles (DRAM refresh) is deliberately NOT modelled:
+// it would mean interposing on the hottest path in the driver to recover ~0.1
+// cycles per access, and it is a uniform offset that does not change the
+// relative cost of VRAM versus RAM, which is the thing being measured.
+//
+// The wait is fractional, so it accumulates in 16.16 fixed point and whole
+// cycles are charged as they carry out -- the same technique the apple2gs
+// driver uses for its 1 MHz cycles (see slow_cycle() there).
+#define X68K_GVRAM_WAIT 0x000105a2 // 1.022 cycles, 16.16
+#define X68K_TVRAM_WAIT 0x0001ec4a // 1.923 cycles, 16.16
+
+void x68k_state::charge_vram_wait(uint32_t increment)
+{
+ // side_effects_disabled() covers debugger and Lua reads, which must not
+ // perturb the timing they are measuring.
+ if (increment == 0 || machine().side_effects_disabled())
+ return;
+ m_vram_wait_accum += increment;
+ int const cycles = m_vram_wait_accum >> 16;
+ m_vram_wait_accum &= 0xffff;
+ if (cycles != 0)
+ m_maincpu->adjust_icount(-cycles);
+}
+
+uint16_t x68k_state::gvram_wait_r(offs_t offset)
+{
+ charge_vram_wait(m_gvram_wait);
+ return m_crtc->gvram_r(offset);
+}
+
+void x68k_state::gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+ charge_vram_wait(m_gvram_wait);
+ m_crtc->gvram_w(offset, data, mem_mask);
+}
+
+uint16_t x68k_state::tvram_wait_r(offs_t offset)
+{
+ charge_vram_wait(m_tvram_wait);
+ return m_crtc->tvram_r(offset);
+}
+
+void x68k_state::tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+ charge_vram_wait(m_tvram_wait);
+ m_crtc->tvram_w(offset, data, mem_mask);
+}
+
void x68k_state::x68k_base_map(address_map &map)
{
map(0x000000, 0xbffffb).rw(FUNC(x68k_state::emptyram_r), FUNC(x68k_state::emptyram_w));
map(0xbffffc, 0xbfffff).rw(FUNC(x68k_state::rom0_r), FUNC(x68k_state::rom0_w));
- map(0xc00000, 0xdfffff).rw(m_crtc, FUNC(x68k_crtc_device::gvram_r), FUNC(x68k_crtc_device::gvram_w));
- map(0xe00000, 0xe7ffff).rw(m_crtc, FUNC(x68k_crtc_device::tvram_r), FUNC(x68k_crtc_device::tvram_w));
+ map(0xc00000, 0xdfffff).rw(FUNC(x68k_state::gvram_wait_r), FUNC(x68k_state::gvram_wait_w));
+ map(0xe00000, 0xe7ffff).rw(FUNC(x68k_state::tvram_wait_r), FUNC(x68k_state::tvram_wait_w));
map(0xe80000, 0xe81fff).rw(m_crtc, FUNC(x68k_crtc_device::crtc_r), FUNC(x68k_crtc_device::crtc_w));
map(0xe82400, 0xe83fff).rw(FUNC(x68k_state::vid_r), FUNC(x68k_state::vid_w));
map(0xe84000, 0xe85fff).rw(m_hd63450, FUNC(hd63450_device::read), FUNC(hd63450_device::write));
@@ -1009,7 +945,7 @@
map(0xe90000, 0xe91fff).rw(m_ym2151, FUNC(ym2151_device::read), FUNC(ym2151_device::write)).umask16(0x00ff); map(0xe90000, 0xe91fff).rw(m_ym2151, FUNC(ym2151_device::read), FUNC(ym2151_device::write)).umask16(0x00ff);
map(0xe94000, 0xe94003).m(m_upd72065, FUNC(upd72065_device::map)).umask16(0x00ff); map(0xe94000, 0xe94003).m(m_upd72065, FUNC(upd72065_device::map)).umask16(0x00ff);
map(0xe94004, 0xe94007).rw(FUNC(x68k_state::fdc_r), FUNC(x68k_state::fdc_w)); map(0xe94004, 0xe94007).rw(FUNC(x68k_state::fdc_r), FUNC(x68k_state::fdc_w));
@ -198,7 +370,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
map(0xe9a000, 0xe9bfff).rw(FUNC(x68k_state::ppi_r), FUNC(x68k_state::ppi_w)); map(0xe9a000, 0xe9bfff).rw(FUNC(x68k_state::ppi_r), FUNC(x68k_state::ppi_w));
map(0xe9c000, 0xe9dfff).rw(FUNC(x68k_state::ioc_r), FUNC(x68k_state::ioc_w)); map(0xe9c000, 0xe9dfff).rw(FUNC(x68k_state::ioc_r), FUNC(x68k_state::ioc_w));
map(0xe9e000, 0xe9e3ff).rw(FUNC(x68k_state::exp_r), FUNC(x68k_state::exp_w)); // FPU (Optional) map(0xe9e000, 0xe9e3ff).rw(FUNC(x68k_state::exp_r), FUNC(x68k_state::exp_w)); // FPU (Optional)
@@ -1068,16 +936,6 @@ static INPUT_PORTS_START( x68000 ) @@ -1072,16 +1008,6 @@
PORT_CONFNAME( 0x02, 0x02, "Enable fake bus errors") PORT_CONFNAME( 0x02, 0x02, "Enable fake bus errors")
PORT_CONFSETTING( 0x00, DEF_STR( Off )) PORT_CONFSETTING( 0x00, DEF_STR( Off ))
PORT_CONFSETTING( 0x02, DEF_STR( On )) PORT_CONFSETTING( 0x02, DEF_STR( On ))
@ -215,18 +387,34 @@ index 8e5def0a501a6..c6787eeef393d 100644
INPUT_PORTS_END INPUT_PORTS_END
void x68k_state::floppy_load_unload(bool load, floppy_image_device *dev) void x68k_state::floppy_load_unload(bool load, floppy_image_device *dev)
@@ -1151,10 +1009,6 @@ void x68k_state::machine_start() @@ -1155,9 +1081,23 @@
m_spriteram = (uint16_t*)(memregion("user1")->base()); m_spriteram = (uint16_t*)(memregion("user1")->base());
space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer()); space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer());
- // start mouse timer - // start mouse timer
- m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now - m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now
- m_mouse.inputtype = 0; - m_mouse.inputtype = 0;
- + // VRAM wait states, applied only to the machine the measurements cover.
+ // The published figures are for a 10 MHz 68000 X68000; XEiJ's table also
+ // lists a 25 MHz X68030 (GVRAM 6.377, TVRAM 6.623) but those machines are
+ // MACHINE_NOT_WORKING here and cannot be validated, so they are left
+ // unmodelled rather than given invented numbers.
+ if (m_maincpu->clock() == 10'000'000)
+ {
+ m_gvram_wait = X68K_GVRAM_WAIT;
+ m_tvram_wait = X68K_TVRAM_WAIT;
+ }
+ else
+ {
+ m_gvram_wait = 0;
+ m_tvram_wait = 0;
+ }
+ m_vram_wait_accum = 0;
+ save_item(NAME(m_vram_wait_accum));
// start LED timer // start LED timer
m_led_timer->adjust(attotime::zero, 0, attotime::from_msec(400)); m_led_timer->adjust(attotime::zero, 0, attotime::from_msec(400));
@@ -1177,11 +1117,11 @@
@@ -1173,11 +1027,11 @@ void x68k_state::machine_start()
m_dmac_int = false; m_dmac_int = false;
m_mfp_int = false; m_mfp_int = false;
@ -239,7 +427,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
m_current_ipl = 0; m_current_ipl = 0;
m_adpcm.rate = 0; m_adpcm.rate = 0;
m_adpcm.clock = 0; m_adpcm.clock = 0;
@@ -1204,7 +1058,6 @@ void x68k_state::driver_start() @@ -1208,7 +1148,6 @@
// copy last half of BIOS to a user region, to use for initial startup // copy last half of BIOS to a user region, to use for initial startup
memcpy(user2,(rom+0xff0000),0x10000); memcpy(user2,(rom+0xff0000),0x10000);
@ -247,7 +435,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
m_led_timer = timer_alloc(FUNC(x68ksupr_state::led_callback), this); m_led_timer = timer_alloc(FUNC(x68ksupr_state::led_callback), this);
m_fdc_tc = timer_alloc(FUNC(x68ksupr_state::floppy_tc_tick), this); m_fdc_tc = timer_alloc(FUNC(x68ksupr_state::floppy_tc_tick), this);
m_adpcm_timer = timer_alloc(FUNC(x68ksupr_state::adpcm_drq_tick), this); m_adpcm_timer = timer_alloc(FUNC(x68ksupr_state::adpcm_drq_tick), this);
@@ -1249,6 +1102,11 @@ static void keyboard_devices(device_slot_interface &device) @@ -1253,6 +1192,11 @@
device.option_add("x68k", X68K_KEYBOARD); device.option_add("x68k", X68K_KEYBOARD);
} }
@ -259,7 +447,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
void x68k_state::x68000_base(machine_config &config) void x68k_state::x68000_base(machine_config &config)
{ {
config.set_maximum_quantum(attotime::from_hz(60)); config.set_maximum_quantum(attotime::from_hz(60));
@@ -1281,7 +1139,12 @@ void x68k_state::x68000_base(machine_config &config) @@ -1285,7 +1229,26 @@
m_hd63450->dma_read<0>().set("upd72065", FUNC(upd72065_device::dma_r)); m_hd63450->dma_read<0>().set("upd72065", FUNC(upd72065_device::dma_r));
m_hd63450->dma_write<0>().set("upd72065", FUNC(upd72065_device::dma_w)); m_hd63450->dma_write<0>().set("upd72065", FUNC(upd72065_device::dma_w));
@ -270,97 +458,62 @@ index 8e5def0a501a6..c6787eeef393d 100644
+ rs232_port_device &mouse(RS232_PORT(config, "mouse_port", mouse_devices, "x68k")); + rs232_port_device &mouse(RS232_PORT(config, "mouse_port", mouse_devices, "x68k"));
+ mouse.rxd_handler().set(m_scc, FUNC(scc8530_device::rxb_w)); + mouse.rxd_handler().set(m_scc, FUNC(scc8530_device::rxb_w));
+ m_scc->out_rtsb_callback().set(mouse, FUNC(rs232_port_device::write_rts)); + m_scc->out_rtsb_callback().set(mouse, FUNC(rs232_port_device::write_rts));
+
+ // RS-232C -- SCC channel A. Only channel B (the mouse) was ever wired, so
+ // the machine's actual serial port had no connection at all and could not
+ // be exercised. Exposing it as a standard rs232 slot makes
+ // `-rs232c null_modem` (TCP socket) and `-rs232c pty` available, which is
+ // what an automated both-directions serial test needs.
+ m_scc->out_txda_callback().set("rs232c", FUNC(rs232_port_device::write_txd));
+ m_scc->out_dtra_callback().set("rs232c", FUNC(rs232_port_device::write_dtr));
+ m_scc->out_rtsa_callback().set("rs232c", FUNC(rs232_port_device::write_rts));
+
+ rs232_port_device &rs232c(RS232_PORT(config, "rs232c", default_rs232_devices, nullptr));
+ rs232c.rxd_handler().set(m_scc, FUNC(scc8530_device::rxa_w));
+ rs232c.dcd_handler().set(m_scc, FUNC(scc8530_device::dcda_w));
+ rs232c.cts_handler().set(m_scc, FUNC(scc8530_device::ctsa_w));
RP5C15(config, m_rtc, 32.768_kHz_XTAL); RP5C15(config, m_rtc, 32.768_kHz_XTAL);
m_rtc->alarm().set(m_mfpdev, FUNC(mc68901_device::i0_w)); m_rtc->alarm().set(m_mfpdev, FUNC(mc68901_device::i0_w));
diff --git a/src/mame/sharp/x68k.h b/src/mame/sharp/x68k.h --- /dev/null
index 2c1dc8552af67..b5c40b213b6bb 100644 +++ b/src/mame/sharp/x68k_mouse.h
--- a/src/mame/sharp/x68k.h @@ -0,0 +1,36 @@
+++ b/src/mame/sharp/x68k.h +// license:BSD-3-Clause
@@ -20,7 +20,7 @@ +// copyright-holders:Patrick Mackinlay
#include "cpu/m68000/m68000.h" +
#include "cpu/m68000/m68030.h" +#ifndef MAME_SHARP_X68K_MOUSE_H
#include "imagedev/floppy.h" +#define MAME_SHARP_X68K_MOUSE_H
-#include "machine/8530scc.h" +
+#include "machine/z80scc.h" +#pragma once
#include "machine/hd63450.h" +
#include "machine/i8255.h" +#include "bus/rs232/rs232.h"
#include "machine/mb87030.h" +
@@ -64,9 +64,6 @@ class x68k_state : public driver_device +class x68k_mouse_device : public buffered_rs232_device<3>
, m_expansion(*this, "exp%u", 1U) +{
, m_adpcm_out(*this, {"adpcm_outl", "adpcm_outr"}) +public:
, m_options(*this, "options") + x68k_mouse_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock = 0);
- , m_mouse1(*this, "mouse1") +
- , m_mouse2(*this, "mouse2") + virtual void input_rts(int state) override; // MSCTRL (active low)
- , m_mouse3(*this, "mouse3") +
, m_eject_drv_out(*this, "eject_drv%u", 0U) +protected:
, m_ctrl_drv_out(*this, "ctrl_drv%u", 0U) + virtual ioport_constructor device_input_ports() const override ATTR_COLD;
, m_access_drv_out(*this, "access_drv%u", 0U) + virtual void device_start() override ATTR_COLD;
@@ -100,7 +97,7 @@ class x68k_state : public driver_device +
required_device<palette_device> m_pcgpalette; + virtual void received_byte(u8 byte) override {}
required_device<mc68901_device> m_mfpdev; +
required_device<rp5c15_device> m_rtc; +private:
- required_device<scc8530_legacy_device> m_scc; + required_ioport m_buttons;
+ required_device<scc8530_device> m_scc; + required_ioport m_x_axis;
required_device<ym2151_device> m_ym2151; + required_ioport m_y_axis;
required_device<i8255_device> m_ppi; +
required_device<screen_device> m_screen; + u8 m_b;
@@ -111,9 +108,6 @@ class x68k_state : public driver_device + u16 m_x;
required_device_array<filter_volume_device, 2> m_adpcm_out; + u16 m_y;
+};
required_ioport m_options; +
- required_ioport m_mouse1; +DECLARE_DEVICE_TYPE(X68K_MOUSE, x68k_mouse_device)
- required_ioport m_mouse2; +
- required_ioport m_mouse3; +#endif // MAME_SHARP_X68K_MOUSE_H
output_finder<4> m_eject_drv_out;
output_finder<4> m_ctrl_drv_out;
@@ -178,24 +172,15 @@ class x68k_state : public driver_device
uint8_t hdcvector = 0;
uint8_t prnvector = 0;
} m_ioc;
- struct
- {
- int inputtype = 0; // determines which input is to be received
- bool irqactive = false; // true if IRQ is being serviced
- uint8_t irqvector = 0;
- char last_mouse_x = 0; // previous mouse x-axis value
- char last_mouse_y = 0; // previous mouse y-axis value
- int bufferempty = 0; // non-zero if buffer is empty
- } m_mouse;
uint8_t m_ppi_portc = 0;
bool m_dmac_int = false;
bool m_mfp_int = false;
+ bool m_scc_int = false;
bool m_exp_irq2[2]{};
bool m_exp_irq4[2]{};
bool m_exp_nmi[2]{};
uint8_t m_current_ipl = 0;
int m_led_state = 0;
- emu_timer* m_mouse_timer = nullptr;
emu_timer* m_led_timer = nullptr;
unsigned char m_scc_prev = 0;
emu_timer* m_fdc_tc = nullptr;
@@ -233,7 +218,6 @@ class x68k_state : public driver_device
void dma_irq(int state);
void dma_end(offs_t offset, uint8_t data);
- int read_mouse();
void set_adpcm();
void fm_irq(int state);
@@ -241,8 +225,6 @@ class x68k_state : public driver_device
template <int N> void irq4_line(int state);
template <int N> void nmi_line(int state);
- void scc_w(offs_t offset, uint16_t data);
- uint16_t scc_r(offs_t offset);
void fdc_w(offs_t offset, uint16_t data);
uint16_t fdc_r(offs_t offset);
void ioc_w(offs_t offset, uint16_t data);
diff --git a/src/mame/sharp/x68k_mouse.cpp b/src/mame/sharp/x68k_mouse.cpp
new file mode 100644
index 0000000000000..f30cfc7d25d54
--- /dev/null --- /dev/null
+++ b/src/mame/sharp/x68k_mouse.cpp +++ b/src/mame/sharp/x68k_mouse.cpp
@@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
@ -467,45 +620,3 @@ index 0000000000000..f30cfc7d25d54
+{ +{
+ return INPUT_PORTS_NAME(x68k); + return INPUT_PORTS_NAME(x68k);
+} +}
diff --git a/src/mame/sharp/x68k_mouse.h b/src/mame/sharp/x68k_mouse.h
new file mode 100644
index 0000000000000..9028c831fe47e
--- /dev/null
+++ b/src/mame/sharp/x68k_mouse.h
@@ -0,0 +1,36 @@
+// license:BSD-3-Clause
+// copyright-holders:Patrick Mackinlay
+
+#ifndef MAME_SHARP_X68K_MOUSE_H
+#define MAME_SHARP_X68K_MOUSE_H
+
+#pragma once
+
+#include "bus/rs232/rs232.h"
+
+class x68k_mouse_device : public buffered_rs232_device<3>
+{
+public:
+ x68k_mouse_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock = 0);
+
+ virtual void input_rts(int state) override; // MSCTRL (active low)
+
+protected:
+ virtual ioport_constructor device_input_ports() const override ATTR_COLD;
+ virtual void device_start() override ATTR_COLD;
+
+ virtual void received_byte(u8 byte) override {}
+
+private:
+ required_ioport m_buttons;
+ required_ioport m_x_axis;
+ required_ioport m_y_axis;
+
+ u8 m_b;
+ u16 m_x;
+ u16 m_y;
+};
+
+DECLARE_DEVICE_TYPE(X68K_MOUSE, x68k_mouse_device)
+
+#endif // MAME_SHARP_X68K_MOUSE_H

View file

@ -0,0 +1,27 @@
# MAME 0.264 patches
install.sh (component `mame`) applies these to the MAME 0.264
release in filename order, then builds one binary with the apple2gs and x68k
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 |
| 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 |
## Why the X68000 changes are one patch
They were first kept as three separate patches, but the upstream mouse
backport no longer applied cleanly on top of the other two in any order, and
the sources the working MAME was built from had been fixed up by hand. These
four files were regenerated on 2026-09-27 by diffing that working tree
against the pristine 0.264 release, so they reproduce it exactly (verified:
every file applies with zero fuzz and matches byte for byte).
## Probes
`lua/` holds the MAME Lua probes used while developing the X68000 patches
(boot check, CRTC timing, wait states). Run one with
`mame x68000 -bios ipl10 -autoboot_script lua/<probe>.lua`.

547
joey-run Executable file
View file

@ -0,0 +1,547 @@
#!/usr/bin/env bash
#
# joey-run: run a built JoeyLib program in the right emulator.
#
# Usage:
# ./joey-run [options] PROGRAM [-- ARGS...]
#
# PROGRAM is any of:
# a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG,
# build/amiga/bin/Draw, build/x68000/bin/DRAW.X,
# build/iigs/bin/DRAW
# an app folder build/amiga/bin/Taxi (the program plus its DATA)
# an IIgs disk image build/iigs/bin/STAXI.2mg (make iigs-game-disk), or
# a data disk such as joey.2mg (boots GS/OS with it)
# an example name draw, staxi, uber, ... together with --port
# The machine is recognised from the file itself.
#
# Options:
# --port PORT iigs, amiga, atarist, dos or x68000
# --data DIR copy DIR's files into the program's DATA folder for
# this run (for example an AGI game's files)
# --headless no window and no sound, for scripts and CI; stops
# after 60 seconds unless --seconds says otherwise
# --seconds N stop after N seconds
# --snapshot FILE save a PNG of the screen when the run stops
# (needs --seconds or --headless)
# --machine rom01 IIgs: the ROM 01 machine instead of ROM 03
# --ram SIZE IIgs: memory size for MAME (default 4M; AGI wants 8M)
# --debug IIgs and X68000: start MAME's debugger
# -h, --help this text
#
# Arguments after -- are passed to the program (DOS only: the other
# machines' autostart cannot pass arguments).
#
# The emulators and firmware come from ./install.sh
# (install emulator:PORT). Interactive runs use the program's own folder
# or disk image, so saves and logs persist; --headless and --data runs
# work on a private copy. Nothing is written to your home directory.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib/joeyEnv.sh
source "$repo/scripts/lib/joeyEnv.sh"
port=""
dataDir=""
headless=0
seconds=""
snapshot=""
machine="apple2gs"
ramSize="4M"
debug=0
program=""
progArgs=()
emuPid=""
xvfbPid=""
work=""
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
}
fail() {
echo "joey-run: $*" >&2
exit 1
}
cleanup() {
joeyStop "$emuPid"
if [ -n "$xvfbPid" ]; then
kill "$xvfbPid" 2>/dev/null || true
fi
if [ -n "$work" ]; then
rm -rf "$work"
fi
}
# ---- finding the program ----------------------------------------------------
# magicPort FILE -- print the machine FILE was built for, or nothing.
magicPort() {
local file="$1"
local magic
case "$file" in
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
printf 'iigs\n'
return 0
;;
esac
magic="$(head -c 16 "$file" | od -An -tx1 | tr -d ' \n')"
case "$magic" in
000003f3*) printf 'amiga\n' ;;
601a*) printf 'atarist\n' ;;
4d5a*) printf 'dos\n' ;;
4855*) printf 'x68000\n' ;;
*)
# Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and
# byte 15 (VERSION) is 2.
if [ "${magic:28:4}" = "0402" ]; then
printf 'iigs\n'
fi
;;
esac
}
# 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
local found=""
while IFS= read -r -d '' file; do
if [ -n "$(magicPort "$file")" ]; then
[ -z "$found" ] || fail "$1 holds more than one program; name the file"
found="$file"
fi
done < <(find "$1" -maxdepth 1 -type f -print0)
[ -n "$found" ] || fail "no program found in $1"
printf '%s\n' "$found"
}
# ---- shared run plumbing ------------------------------------------------------
# stageFolder DEST -- copy the program's folder (its files and DATA tree)
# plus --data into DEST, for runs that must not touch the build output.
stageFolder() {
local dest="$1"
local progDir
progDir="$(dirname "$program")"
mkdir -p "$dest"
find "$progDir" -maxdepth 1 -type f -exec cp -t "$dest" {} +
if [ -d "$progDir/DATA" ]; then
cp -r "$progDir/DATA" "$dest/"
fi
if [ -n "$dataDir" ]; then
mkdir -p "$dest/DATA"
cp -r "$dataDir/." "$dest/DATA/"
fi
}
# startDisplay -- a virtual X display for --headless SDL emulators.
startDisplay() {
local display
display=":$((300 + RANDOM % 500))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
xvfbPid=$!
sleep 1
export DISPLAY="$display"
export SDL_AUDIODRIVER=dummy
}
# waitOrStop -- wait for the emulator, or stop it after --seconds (taking
# the --snapshot first).
waitOrStop() {
local waited=0
local win
if [ -z "$seconds" ]; then
wait "$emuPid" || true
emuPid=""
return 0
fi
while [ "$waited" -lt "$seconds" ] && kill -0 "$emuPid" 2>/dev/null; do
sleep 1
waited=$((waited + 1))
done
if [ -n "$snapshot" ]; then
if [ "$headless" = "1" ]; then
import -window root "$snapshot"
elif win="$(joeyFindWindow "$DISPLAY" "$emuPid")"; then
import -window "$win" "$snapshot"
else
echo "joey-run: no emulator window to snapshot" >&2
fi
fi
}
# mameStopScript FILE -- MAME Lua that snapshots (if asked) and exits after
# --seconds of emulated time.
mameStopScript() {
cat > "$1" <<LUA
local done = false
emu.register_frame_done(function()
if not done and manager.machine.time:as_double() >= $seconds then
done = true
if $([ -n "$snapshot" ] && echo true || echo false) then
manager.machine.video:snapshot()
end
manager.machine:exit()
end
end)
LUA
}
# runMame DRIVER ARGS... -- start MAME with the per-run directories, the
# headless/debug/stop options, then collect the snapshot.
runMame() {
local driver="$1"
shift
local args=(-skip_gameinfo -cfg_directory "$work/cfg" -nvram_directory "$work/nvram"
-snapshot_directory "$work/snap" -snapname "shot")
if [ "$headless" = "1" ]; then
args+=(-video none -sound none -nothrottle)
export SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy QT_QPA_PLATFORM=offscreen
else
args+=(-window)
fi
if [ "$debug" = "1" ]; then
args+=(-debug)
fi
if [ -n "$seconds" ]; then
mameStopScript "$work/stop.lua"
args+=(-autoboot_script "$work/stop.lua")
fi
(cd "$work" && exec "$MAME" "$driver" "$@" "${args[@]}") &
emuPid=$!
wait "$emuPid" || true
emuPid=""
if [ -n "$snapshot" ]; then
if [ -f "$work/snap/shot.png" ]; then
cp "$work/snap/shot.png" "$snapshot"
else
echo "joey-run: MAME saved no snapshot" >&2
fi
fi
}
# ---- one function per machine -------------------------------------------------
runIigs() {
local name
local img
local bytes
local media=()
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
[ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a game disk (make -f make/iigs.mk iigs-game-disk-agi for AGI)"
case "$program" in
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
joeyNeed CADIUS cadius
img="$program"
if [ "$headless" = "1" ]; then
img="$work/$(basename "$program")"
cp "$program" "$img"
fi
bytes="$(stat -c %s "$img")"
if "$CADIUS" CATALOG "$img" 2>/dev/null | grep -qE '^ ProDOS +SYS'; then
# A bootable game disk: it launches its program by itself.
if [ "$bytes" -le 900000 ]; then
media=(-flop3 "$img")
else
media=(-sl7 cffa2 -hard1 "$img")
fi
else
# A data disk: boot GS/OS beside it and pick in the Finder.
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
if [ "$bytes" -le 900000 ]; then
media=(-flop3 "$work/boot.po" -flop4 "$img")
else
media=(-flop3 "$work/boot.po" -sl7 cffa2 -hard1 "$img")
fi
echo "joey-run: $(basename "$img") is not bootable; GS/OS boots beside it -- open it in the Finder."
fi
;;
*)
# A bare program: pack it onto a data disk and boot a GS/OS
# whose startup program launches it (the developer path; needs
# the IIgs toolchain).
joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
joeyNeed IIGS_CLANG_BUILD toolchain:iigs
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
name="$(basename "$program")"
BINDIR="$(dirname "$program")" JOEY_DISK_EXAMPLES="$name" \
"$repo/scripts/make-iigs-disk.sh" "$work/data.2mg" >/dev/null
"$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null
media=(-flop3 "$work/boot.po" -flop4 "$work/data.2mg")
;;
esac
echo "joey-run: booting the IIgs ($machine); the program comes up in about a minute. Close MAME to stop."
runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}"
}
runAmiga() {
local name
local hd="$work/hd"
local args=()
joeyNeed FSUAE fsuae
name="$(basename "$program")"
# FS-UAE boots the folder as a hard drive and writes to it, so it is
# always staged. s/startup-sequence runs the program.
stageFolder "$hd"
mkdir -p "$hd/s"
printf ':%s\n' "$name" > "$hd/s/startup-sequence"
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
--floppy_drive_speed=800 --base_dir="$work/fs-uae")
if [ -n "${KICKSTART_ROM:-}" ] && [ -f "$KICKSTART_ROM" ]; then
args+=(--kickstart_file="$KICKSTART_ROM")
else
echo "joey-run: no Kickstart installed (./install.sh install kickstart); booting FS-UAE's AROS, which may not run every program" >&2
fi
if [ "$headless" = "1" ]; then
startDisplay
# A real gamepad crashes FS-UAE when there is no screen to grab.
args+=(--joystick_port_0=none --joystick_port_1=none)
export ALSOFT_DRIVERS=null
fi
echo "joey-run: booting the Amiga; $name starts from the JOEYLIB hard drive. Close FS-UAE to stop."
"$FSUAE" "${args[@]}" >"$work/fs-uae.log" 2>&1 &
emuPid=$!
waitOrStop
# Keep the program's log next to it, as a direct run would.
find "$hd" -maxdepth 1 -iname 'joeylog.txt' -exec cp {} "$(dirname "$program")/" \; 2>/dev/null || true
}
runAtarist() {
local dir
local args=()
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
dir="$(dirname "$program")"
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
stageFolder "$work/hd"
dir="$work/hd"
fi
args=(--tos "$EMUTOS_IMG" --harddrive "$dir" --gemdos-drive C --auto "C:\\$(basename "$program")")
if [ "$headless" = "1" ]; then
startDisplay
args+=(--sound off)
fi
echo "joey-run: booting the Atari ST (EmuTOS); $(basename "$program") starts from drive C. Close Hatari to stop."
"$HATARI" "${args[@]}" >"$work/hatari.log" 2>&1 &
emuPid=$!
waitOrStop
}
runDos() {
local dir
local command
local args=()
joeyNeed DOSBOX dosbox
dir="$(dirname "$program")"
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
stageFolder "$work/hd"
dir="$work/hd"
fi
command="$(basename "$program")"
if [ ${#progArgs[@]} -gt 0 ]; then
command="$command ${progArgs[*]}"
fi
args=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf"
--set "mouse_capture=seamless" -c "MOUNT C \"$dir\"" -c "C:" -c "$command")
if [ "$headless" = "1" ]; then
startDisplay
else
# Leave the program's last screen up until a key is pressed.
args+=(-c "pause")
fi
args+=(--exit)
echo "joey-run: booting DOS (386SX-16); $command runs from drive C. Close DOSBox to stop."
XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${args[@]}" >"$work/dosbox.log" 2>&1 &
emuPid=$!
waitOrStop
}
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
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
rm -rf "$stage"
mkdir -p "$stage"
if [ -d "$(dirname "$program")/DATA" ]; then
cp -r "$(dirname "$program")/DATA" "$stage/"
fi
if [ -n "$dataDir" ]; then
mkdir -p "$stage/DATA"
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)
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"
}
# ---- main ---------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--port) port="${2:?--port needs iigs, amiga, atarist, dos or x68000}"; shift ;;
--data) dataDir="${2:?--data needs a folder}"; shift ;;
--headless) headless=1 ;;
--seconds) seconds="${2:?--seconds needs a number}"; shift ;;
--snapshot) snapshot="${2:?--snapshot needs a file name}"; shift ;;
--machine)
case "${2:-}" in
rom01) machine="apple2gsr1" ;;
rom03) machine="apple2gs" ;;
*) fail "--machine takes rom01 or rom03" ;;
esac
shift
;;
--ram) ramSize="${2:?--ram needs a size such as 4M}"; shift ;;
--debug) debug=1 ;;
-h|--help) usage; exit 0 ;;
--) shift; progArgs=("$@"); break ;;
-*) fail "unknown option $1 (see: joey-run --help)" ;;
*)
[ -z "$program" ] || fail "one program at a time (got '$program' and '$1')"
program="$1"
;;
esac
shift
done
[ -n "$program" ] || { usage; exit 2; }
[[ "$seconds" =~ ^[0-9]*$ ]] || fail "--seconds takes a whole number"
if [ "$headless" = "1" ] && [ -z "$seconds" ]; then
seconds=60
fi
if [ -n "$snapshot" ]; then
[ -n "$seconds" ] || fail "--snapshot needs --seconds (or --headless)"
snapshot="$(realpath -m "$snapshot")"
fi
if [ -n "$dataDir" ]; then
[ -d "$dataDir" ] || fail "--data: no such folder $dataDir"
dataDir="$(cd "$dataDir" && pwd)"
fi
# Resolve PROGRAM to a file and its machine.
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")"
[ -e "$program" ] || fail "$program is not built (make $port)"
fi
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"
detected="$(magicPort "$program")"
[ -n "$detected" ] || fail "cannot tell which machine $program is for"
if [ -n "$port" ] && [ "$port" != "$detected" ]; then
fail "$program is a $detected program, not $port"
fi
if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ]; then
fail "program arguments only work on DOS"
fi
work="$(mktemp -d -t joey-run.XXXXXX)"
trap cleanup EXIT INT TERM
case "$detected" in
iigs) runIigs ;;
amiga) runAmiga ;;
atarist) runAtarist ;;
dos) runDos ;;
x68000) runX68000 ;;
esac
if [ -n "$snapshot" ] && [ -f "$snapshot" ]; then
echo "joey-run: snapshot saved to $snapshot"
fi

View file

@ -2,17 +2,21 @@
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
$(call requireTool,$(AMIGA_CC),toolchain:amiga)
$(call requireTool,$(AMIGA_AS),toolchain:amiga)
$(call requireTool,$(AMIGA_PTPLAYER)/ptplayer.asm,toolchain:amiga)
PLATFORM := amiga PLATFORM := amiga
BUILD := $(REPO_DIR)/build/$(PLATFORM) BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
# PTPlayer is staged by toolchains/install.sh into PTPLAYER_DIR; we # PTPlayer is installed by ./install.sh (AMIGA_PTPLAYER); we
# reference its ptplayer.asm + header from there rather than copying # reference its ptplayer.asm + header from there rather than copying
# them into the source tree, so install.sh can refresh / version it # them into the source tree, so the installer can refresh / version it
# independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve # independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve
# <SDI_compiler.h> from the machine-local shim alongside our code. # <SDI_compiler.h> from the machine-local shim alongside our code.
PTPLAYER_DIR := $(REPO_DIR)/toolchains/amiga/ptplayer PTPLAYER_DIR := $(AMIGA_PTPLAYER)
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(CFLAGS_EXTRA) CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(CFLAGS_EXTRA)
# OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses # OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses
# CIA-B + audio.device interrupts via the OS rather than taking over # CIA-B + audio.device interrupts via the OS rather than taking over
@ -101,10 +105,7 @@ STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/amiga
STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA
# Level data is extracted from the C64 ROM (see dos.mk for the long # Level data is extracted from the C64 ROM (see dos.mk for the long
# explanation): 24 lettered levels A..X from scenes 0..23 plus the # explanation): 24 lettered levels A..X from scenes 0..23 plus the
# title from scene 24, via stuff/spacetaxi/romToLevel.py. # title from scene 24, via examples/spacetaxi/assets/romToLevel.py.
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/romToLevel.py
STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat $(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN)
@ -165,13 +166,13 @@ ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py
ASSETBAKE_TARGET := amiga ASSETBAKE_TARGET := amiga
# Game data lives under bin/DATA/, ready to be copied into the # Game data lives under bin/DATA/, ready to be copied into the
# scratch JOEYLIB hard-drive dir staged by scripts/run-amiga.sh. # scratch JOEYLIB hard-drive dir staged by joey-run.
# audio.c fopens "DATA/test.mod" etc. relative to the boot volume. # audio.c fopens "DATA/test.mod" etc. relative to the boot volume.
DATA_DIR := $(BINDIR)/DATA DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.PHONY: all amiga clean-amiga clean .PHONY: all amiga clean-amiga clean
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)

View file

@ -2,20 +2,24 @@
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
$(call requireTool,$(ST_CC),toolchain:atarist)
$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:atarist)
PLATFORM := atarist PLATFORM := atarist
ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim
BUILD := $(REPO_DIR)/build/$(PLATFORM) BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
# CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX) # CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX)
# so flag experiments never edit a tracked file (perf-hunt-2 rank 17). # so flag experiments never edit a tracked file (perf-hunt-2 rank 17).
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include -I$(REPO_DIR)/toolchains/atarist/include-shim -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP $(CFLAGS_EXTRA) CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(LIBXMP_DIR)/include -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP $(CFLAGS_EXTRA)
LDFLAGS := LDFLAGS :=
# libxmp-lite shared with the DOS port. Built as a static archive that # libxmp-lite shared with the DOS port. Built as a static archive that
# provides the Protracker pattern decoder for the ST audio HAL; the # provides the Protracker pattern decoder for the ST audio HAL; the
# 68k 4-channel mixer + YM2149 PWM output stage layer on top. # 68k 4-channel mixer + YM2149 PWM output stage layer on top.
LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite # LIBXMP_DIR comes from the installer's config.mk.
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) $(LIBXMP_OBJDIR)/math_shim.o LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) $(LIBXMP_OBJDIR)/math_shim.o
@ -85,10 +89,7 @@ STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/atarist
STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA
# Level data is extracted from the C64 ROM (see dos.mk for the long # Level data is extracted from the C64 ROM (see dos.mk for the long
# explanation): 24 lettered levels A..X from scenes 0..23 plus the # explanation): 24 lettered levels A..X from scenes 0..23 plus the
# title from scene 24, via stuff/spacetaxi/romToLevel.py. # title from scene 24, via examples/spacetaxi/assets/romToLevel.py.
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/romToLevel.py
STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat $(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN)
@ -151,7 +152,7 @@ DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.PHONY: all atarist clean-atarist clean .PHONY: all atarist clean-atarist clean
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -194,7 +195,7 @@ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@
$(LIBXMP_OBJDIR)/math_shim.o: $(REPO_DIR)/toolchains/atarist/include-shim/math_shim.c $(LIBXMP_OBJDIR)/math_shim.o: $(ST_SHIM)/math_shim.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) -c $< -o $@

View file

@ -3,6 +3,30 @@
# Source this only via the per-target make file (make/iigs.mk etc). # Source this only via the per-target make file (make/iigs.mk etc).
REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..)
# Tool paths (AMIGA_CC, LLVM816_ROOT, LIBXMP_DIR, ...) come from the file
# install.sh generates; it is the only place they are defined.
-include $(REPO_DIR)/toolchains/config.mk
# $(call requireTool,FILE,INSTALL-TARGET): stop with a clear message when a
# port's compiler is missing, instead of letting recipes run an empty
# command. Goals that only clean need no toolchain. The missing path is not
# printed: with nothing installed it is empty or a bare suffix, and
# `./install.sh doctor` names exactly what is wrong.
requireTool = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(wildcard $(1)),,$(error $(2) is not installed; run: ./install.sh install $(2))))
# Space Taxi's levels are extracted from the original C64 game image, which
# is not part of the repository (the owner's private bundle provides it; see
# ./install.sh import). Without it every port still builds the
# STAXI binary and skips only the data derived from the image.
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/examples/spacetaxi/assets/romToLevel.py
STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
HAVE_STAXI_DATA := $(if $(wildcard $(STAXI_RAW_DUMP)),1)
# $(call staxiData,TARGETS): TARGETS, or nothing (with a note, once per make
# process) without the image.
staxiData = $(if $(HAVE_STAXI_DATA),$(1),$(staxiNote))
staxiNote = $(if $(staxiNoted),,$(eval staxiNoted := 1)$(info note: Space Taxi data skipped: stuff/spacetaxi/raw.bin is not present))
INCLUDE_DIR := $(REPO_DIR)/include INCLUDE_DIR := $(REPO_DIR)/include
# Machine-specific code lives in src/<machine>/ (iigs, amiga, atarist, dos); # Machine-specific code lives in src/<machine>/ (iigs, amiga, atarist, dos);
# code shared across machines of one CPU lives in a processor folder (src/m68k). # code shared across machines of one CPU lives in a processor folder (src/m68k).

View file

@ -2,6 +2,11 @@
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
$(call requireTool,$(DOS_CC),toolchain:dos)
$(call requireTool,$(DOS_AS),toolchain:dos)
$(call requireTool,$(DOS_CWSDSTUB),toolchain:dos)
$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:dos)
PLATFORM := dos PLATFORM := dos
BUILD := $(REPO_DIR)/build/$(PLATFORM) BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib LIBDIR := $(BUILD)/lib
@ -12,15 +17,14 @@ BINDIR := $(BUILD)/bin
# DOSBox's per-instruction-charged core (perf-hunt-2 rank 2). # DOSBox's per-instruction-charged core (perf-hunt-2 rank 2).
# CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX) # CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX)
# so flag experiments never edit a tracked file (perf-hunt-2 rank 17). # so flag experiments never edit a tracked file (perf-hunt-2 rank 17).
CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include -MMD -MP $(CFLAGS_EXTRA) CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(LIBXMP_DIR)/include -MMD -MP $(CFLAGS_EXTRA)
ASFLAGS := -f coff ASFLAGS := -f coff
LDFLAGS := LDFLAGS :=
# libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends # libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends
# replayer + SMIX SFX overlay. Staged at toolchains/audio/libxmp-lite/ # replayer + SMIX SFX overlay. Installed by ./install.sh
# (currently checked in; install.sh wiring is a follow-up). Built as # (LIBXMP_DIR, from config.mk). Built as a static archive that the DOS
# a static archive that the DOS audio HAL links against. # audio HAL links against.
LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC))
@ -94,16 +98,13 @@ STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/dos
STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA
# Level data is extracted from the C64 ROM by # Level data is extracted from the C64 ROM by
# stuff/spacetaxi/romToLevel.py: 24 lettered levels (A..X) from scenes # examples/spacetaxi/assets/romToLevel.py: 24 lettered levels (A..X) from scenes
# 0..23 plus the title from scene 24. The script runs the captured # 0..23 plus the title from scene 24. The script runs the captured
# decompressor through a Python 6502 emulator and writes STL2 .dat # decompressor through a Python 6502 emulator and writes STL2 .dat
# files directly. Input is the raw RAM dump. mkstlevel + title.txt # files directly. Input is the raw RAM dump. mkstlevel + title.txt
# is no longer used: the C64 title encodes its screen with the game's # is no longer used: the C64 title encodes its screen with the game's
# custom charset (extracted into tbank0.png), so a hand-authored tile # custom charset (extracted into tbank0.png), so a hand-authored tile
# layout with a different glyph mapping renders as garbage. # layout with a different glyph mapping renders as garbage.
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/romToLevel.py
STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat $(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN)
@ -166,7 +167,7 @@ STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SP
MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel
.PHONY: all dos clean-dos clean .PHONY: all dos clean-dos clean
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS))
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)

View file

@ -1,6 +1,6 @@
# Apple IIgs build rules (llvm-mos w65816: clang + llvm-mc + link816). # Apple IIgs build rules (llvm-mos w65816: clang + llvm-mc + link816).
# #
# toolchains/iigs/clang-build.sh drives compile (clang) / assemble # make/support/iigs/clang-build.sh drives compile (clang) / assemble
# (llvm-mc) / link (link816) against the prebuilt w65816 runtime. # (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
# Hand-rolled asm lives in src/iigs/*.s (llvm-mc GAS syntax); # Hand-rolled asm lives in src/iigs/*.s (llvm-mc GAS syntax);
# bank placement is handled by -ffunction-sections + link816 # bank placement is handled by -ffunction-sections + link816
@ -9,8 +9,8 @@
# STATUS: the launchable-binary pipeline (multi-segment OMF emission + # STATUS: the launchable-binary pipeline (multi-segment OMF emission +
# disk packaging + GS/OS launch) is still being brought up. `iigs-lib` # disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
# compile-checks every library + example translation unit, and # compile-checks every library + example translation unit, and
# `iigs-clang-smoke` validates the full clang -> llvm-mc -> link816 -> # `iigs-clang-smoke` (not part of the default goal: it needs MAME)
# MAME path end to end. Per-example binary targets build via # validates the full clang -> llvm-mc -> link816 -> MAME path end to end. Per-example binary targets build via
# clang-build.sh; large examples need the multi-segment OMF wiring still # clang-build.sh; large examples need the multi-segment OMF wiring still
# in progress. # in progress.
@ -22,8 +22,11 @@ BINDIR := $(BUILD)/bin
OBJDIR := $(BUILD)/obj OBJDIR := $(BUILD)/obj
CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang
$(call requireTool,$(CLANG),toolchain:iigs)
$(call requireTool,$(IIGS_AS),toolchain:iigs)
$(call requireTool,$(IIGS_NTP_DIR)/ninjatrackerplus.s,toolchain:iigs)
LLVM_MC := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc LLVM_MC := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc
AR := $(LLVM816_ROOT)/tools/llvm-mos-sdk/bin/llvm-ar AR := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-ar
CCWRAP := $(LLVM816_ROOT)/scripts/ccRegallocFallback.sh CCWRAP := $(LLVM816_ROOT)/scripts/ccRegallocFallback.sh
RT_INC := $(LLVM816_ROOT)/runtime/include RT_INC := $(LLVM816_ROOT)/runtime/include
CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections $(EXTRA_CFLAGS) CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections $(EXTRA_CFLAGS)
@ -70,19 +73,17 @@ STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
# binary, then bake it into a dead-strippable GAS data object (gNtpPlayerBytes # binary that ships next to the program; jlpAudioInit loads "ntpplayer" from
# / _len, which audio_full.c externs). bin-to-s.sh is the clang-era replacement # disk at run time (see the $(BINDIR)/AUDIO rule).
# for the deleted bin-to-asm.sh. Only the AUDIO binary pulls it in; link816 NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s
# dead-strips it everywhere else.
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_BIN := $(BUILD)/audio/ntpplayer.bin
NTP_ASM := $(BUILD)/audio/ntpdata.s IIGS_MERLIN := $(IIGS_AS)
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
.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-shrtail clean-iigs clean
# Default: compile-check the library + run the end-to-end smoke test. # Default: build (compile-check) the library. The MAME smoke test is
all iigs: iigs-lib iigs-clang-smoke # `iigs-clang-smoke`; disks and examples are `iigs-disk`.
all iigs: iigs-lib
# Per-example launchable binaries. STAXI and AUDIO are INCLUDED: the # Per-example launchable binaries. STAXI and AUDIO are INCLUDED: the
# old exclusion comment ("blocked on deeper toolchain features") went # old exclusion comment ("blocked on deeper toolchain features") went
@ -95,8 +96,8 @@ iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL
$(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \ $(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \
$(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO $(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO
# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that run-iigs-mame.sh / # Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that the gates, and
# run-iigs.sh mount on flop4. Packs the launchable examples as ProDOS S16 apps # `joey-run build/iigs/bin/joey.2mg`, mount beside GS/OS. Packs the launchable examples as ProDOS S16 apps
# (an 800KB floppy can't hold every example; see make-iigs-disk.sh). # (an 800KB floppy can't hold every example; see make-iigs-disk.sh).
# NTP_BIN rides along so jlpAudioInit's runtime load of "ntpplayer" finds it # NTP_BIN rides along so jlpAudioInit's runtime load of "ntpplayer" finds it
# on the volume -- without it every example logs "audioInit failed" (Phase 1 # on the volume -- without it every example logs "audioInit failed" (Phase 1
@ -120,10 +121,18 @@ STAXI_IIGS_SPC := $(STAXICELS_SPR:.spr=.spc)
STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21
STAXILEVEL_IIGS_SPC := $(foreach n,$(STAXI_CEL_LEVELS),$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level$(n).spc) STAXILEVEL_IIGS_SPC := $(foreach n,$(STAXI_CEL_LEVELS),$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level$(n).spc)
# Speech baked to 1-bit PCM per passenger pitch (assets/genSpeechPcm.py); # Speech baked to 1-bit PCM per passenger pitch (assets/genSpeechPcm.py);
# make-iigs-disk.sh picks it up from the generated tree like font.tbk. # make-iigs-disk.sh picks it up from the generated tree.
STAXI_IIGS_SPEECH := $(EXAMPLES)/spacetaxi/generated/iigs/speech.bin STAXI_IIGS_SPEECH := $(EXAMPLES)/spacetaxi/generated/iigs/speech.bin
$(STAXI_IIGS_SPEECH): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $(STAXI_IIGS_SPEECH): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py
@mkdir -p $(dir $@)
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --biased python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --biased
# Level files: extracted from the C64 game image exactly as on the other
# ports (see make/dos.mk), then found by make-iigs-disk.sh in the tree.
STAXI_IIGS_LEVELS := $(foreach n,$(STAXI_LEVEL_NUMS),$(EXAMPLES)/spacetaxi/generated/iigs/levels/level$(n).dat) \
$(EXAMPLES)/spacetaxi/generated/iigs/levels/title.dat
$(STAXI_IIGS_LEVELS) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(EXAMPLES)/spacetaxi/generated/iigs/levels
python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(EXAMPLES)/spacetaxi/generated/iigs/levels
SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs
STAXIBAKE := $(REPO_DIR)/build/tools/staxibake STAXIBAKE := $(REPO_DIR)/build/tools/staxibake
@ -144,7 +153,7 @@ $(EXAMPLES)/spacetaxi/generated/iigs/sprites/level%.spr: $(EXAMPLES)/spacetaxi/g
$(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spr $(SPRITEBAKE_IIGS) $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spr $(SPRITEBAKE_IIGS)
$(SPRITEBAKE_IIGS) $< $@ $(SPRITEBAKE_IIGS) $< $@
iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH) iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
# ONE self-booting volume per game: a GS/OS system stripped to what JoeyLib # ONE self-booting volume per game: a GS/OS system stripped to what JoeyLib
@ -162,7 +171,7 @@ iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(S
# (see make-iigs-disk.sh's JOEY_DISK_EXAMPLES), and the gates depend on # (see make-iigs-disk.sh's JOEY_DISK_EXAMPLES), and the gates depend on
# joey.2mg's exact contents, so this must not rewrite it. # joey.2mg's exact contents, so this must not rewrite it.
GAME ?= STAXI GAME ?= STAXI
iigs-game-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH) iigs-game-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$(GAME)" \ BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$(GAME)" \
$(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$(GAME)-data.2mg $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$(GAME)-data.2mg
JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \ JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \
@ -195,7 +204,7 @@ iigs-verify-game-disk: iigs-game-disk
# ProDOS hard-disk image (joey-all.2mg): boot it in MAME with # ProDOS hard-disk image (joey-all.2mg): boot it in MAME with
# `-sl7 cffa2 -hard1 <GS/OS> -hard2 build/iigs/bin/joey-all.2mg` and launch # `-sl7 cffa2 -hard1 <GS/OS> -hard2 build/iigs/bin/joey-all.2mg` and launch
# any example from the JOEYLIB volume in the Finder. # any example from the JOEYLIB volume in the Finder.
iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH) iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \
JOEY_DISK_SIZE="16MB" \ JOEY_DISK_SIZE="16MB" \
JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \ JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \
@ -256,9 +265,6 @@ $(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@ mv $(BUILD)/audio/ntpplayer $@
$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-s.sh
$(REPO_DIR)/toolchains/iigs/bin-to-s.sh $(NTP_BIN) $@ gNtpPlayerBytes gNtpPlayerBytes_len
# Compile-check every library and example translation unit with clang, # Compile-check every library and example translation unit with clang,
# and assemble every .s with llvm-mc. This is the canonical "does the # and assemble every .s with llvm-mc. This is the canonical "does the
# IIgs source still build" gate while the launchable pipeline is wired. # IIgs source still build" gate while the launchable pipeline is wired.
@ -291,7 +297,7 @@ iigs-lib: $(LIB)
# End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating # End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating
# the ported asm computes correctly on emulated hardware. # the ported asm computes correctly on emulated hardware.
iigs-clang-smoke: iigs-clang-smoke:
$(REPO_DIR)/toolchains/iigs/clang-smoke.sh $(REPO_DIR)/make/support/iigs/clang-smoke.sh
# Per-example binaries via clang-build.sh. clang-build.sh mirrors the # Per-example binaries via clang-build.sh. clang-build.sh mirrors the
# old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These # old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These
@ -376,8 +382,8 @@ $(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB) $(IIGS_CLANG_BUILD)
# AUDIO loads the NTP replayer from disk at runtime: audio_full.c reads # AUDIO loads the NTP replayer from disk at runtime: audio_full.c reads
# ntpplayer.bin via NTP_PATH instead of baking the 34KB replayer as rodata # ntpplayer.bin via NTP_PATH instead of baking the 34KB replayer as rodata
# (which overflowed bank 0, and clang's IMM16+$BE near form can't reach a far # (which overflowed bank 0, and clang's IMM16+$BE near form can't reach a far
# rodata segment anyway). The binary no longer links $(NTP_ASM); $(NTP_BIN) is # rodata segment anyway). $(NTP_BIN) is shipped on the application disk next
# still built so it can be shipped on the application disk next to the binary. # to the binary.
$(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD) $(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AUDIO.d $(INCLUDES) -o $@ $(AUDIO_SRC) $(LIB) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AUDIO.d $(INCLUDES) -o $@ $(AUDIO_SRC) $(LIB)

View file

@ -0,0 +1,25 @@
// Minimal math.h shim for the m68k-atari-mint toolchain (mintlib
// doesn't ship <math.h> or libm). Declares only what libxmp-lite
// reaches for; the implementations live in math_shim.c next to this
// header. Accuracy is sufficient for libxmp-lite's note-period
// conversion, not for general-purpose use.
#ifndef JOEY_ATARIST_MATH_SHIM_H
#define JOEY_ATARIST_MATH_SHIM_H
#define M_LN2 0.69314718055994530942
#define M_PI 3.14159265358979323846
double ceil(double x);
double floor(double x);
double log(double x);
double pow(double base, double exp);
double trunc(double x);
float ceilf(float x);
float floorf(float x);
float logf(float x);
float powf(float base, float exp);
float truncf(float x);
#endif

View file

@ -0,0 +1,100 @@
// Tiny libm replacement used by libxmp-lite on the m68k-atari-mint
// toolchain. Only the functions libxmp-lite calls are implemented.
// Accuracy targets: a few hundred parts per million, plenty for
// note-frequency math.
#include "math.h"
double ceil(double x) {
long i = (long)x;
if ((double)i < x) {
i++;
}
return (double)i;
}
double floor(double x) {
long i = (long)x;
if ((double)i > x) {
i--;
}
return (double)i;
}
// Natural log via 2*atanh((x-1)/(x+1)) after a power-of-two range
// reduction into [1, 2). The atanh series converges fast on that
// interval.
static double logSeries(double m) {
double y;
double y2;
y = (m - 1.0) / (m + 1.0);
y2 = y * y;
return 2.0 * y * (1.0
+ y2 * (1.0 / 3.0
+ y2 * (1.0 / 5.0
+ y2 * (1.0 / 7.0
+ y2 * (1.0 / 9.0)))));
}
double log(double x) {
int k;
if (x <= 0.0) {
return 0.0;
}
k = 0;
while (x >= 2.0) { x *= 0.5; k++; }
while (x < 1.0) { x *= 2.0; k--; }
return (double)k * M_LN2 + logSeries(x);
}
// 2^f for f in [0, 1) -- 5-term polynomial good to ~1e-6.
static double exp2Frac(double f) {
return 1.0 + f * (0.69314718
+ f * (0.24022650
+ f * (0.05550410
+ f * (0.00961813
+ f * 0.00133335))));
}
double pow(double base, double exp) {
double k;
long ki;
double frac;
double result;
if (base <= 0.0) {
return 0.0;
}
if (exp == 0.0) {
return 1.0;
}
k = exp * (log(base) / M_LN2);
ki = (long)k;
if (k < 0.0 && (double)ki != k) {
ki--;
}
frac = k - (double)ki;
result = exp2Frac(frac);
while (ki > 0) { result *= 2.0; ki--; }
while (ki < 0) { result *= 0.5; ki++; }
return result;
}
double trunc(double x) {
return (double)(long)x;
}
float ceilf (float x) { return (float)ceil ((double)x); }
float floorf(float x) { return (float)floor((double)x); }
float logf (float x) { return (float)log ((double)x); }
float powf (float b, float e) { return (float)pow ((double)b, (double)e); }
float truncf(float x) { return (float)(long)x; }

69
make/support/dos/embed-dpmi.sh Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Embed the CWSDPMI DPMI server into a DJGPP-built DOS executable so
# the resulting .EXE is self-contained and does not require a separate
# CWSDPMI.EXE on the target machine. Run as a post-link step from the
# DOS Makefile.
#
# Usage: embed-dpmi.sh program.EXE
#
# Mechanism: DJGPP .EXE files are COFF binaries prefixed with a small
# MZ stub. Stripping the stub via exe2coff leaves a pure COFF image.
# CWSDSTUB.EXE is a larger MZ stub that contains a full DPMI host;
# concatenating CWSDSTUB.EXE with the COFF image produces a new .EXE
# with the DPMI server built in.
#
# Requires $DOS_EXE2COFF and $DOS_CWSDSTUB (exported by make from the
# installer's toolchains/config.mk; toolchains/env.sh for a shell).
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 program.EXE" >&2
exit 2
fi
exe=$1
for v in DOS_EXE2COFF DOS_CWSDSTUB; do
if [[ -z "${!v:-}" ]]; then
echo "$v not set; run ./install.sh install toolchain:dos" >&2
exit 1
fi
done
if [[ ! -x $DOS_EXE2COFF ]]; then
echo "exe2coff not executable: $DOS_EXE2COFF" >&2
exit 1
fi
if [[ ! -f $DOS_CWSDSTUB ]]; then
echo "CWSDSTUB.EXE not found: $DOS_CWSDSTUB" >&2
exit 1
fi
if [[ ! -f $exe ]]; then
echo "executable not found: $exe" >&2
exit 1
fi
coff=${exe%.*}
coff_tmp=${coff}.coff.tmp
trap 'rm -f "$coff_tmp" "$coff"' EXIT
# djgpp-gcc on a case-sensitive host writes its output to both case
# variants (e.g. PATTERN.EXE and PATTERN.exe) as distinct files.
# Remove the lowercase sibling so DOSBox's case-folded lookup cannot
# pick up the pre-embed copy after we rewrite the uppercase one.
lower_exe=${coff}.exe
if [[ $exe != "$lower_exe" && -f $lower_exe ]]; then
rm -f "$lower_exe"
fi
"$DOS_EXE2COFF" "$exe"
if [[ ! -f $coff ]]; then
echo "exe2coff did not produce expected output: $coff" >&2
exit 1
fi
cat "$DOS_CWSDSTUB" "$coff" > "$coff_tmp"
mv "$coff_tmp" "$exe"
trap - EXIT
rm -f "$coff"

342
make/support/iigs/clang-build.sh Executable file
View file

@ -0,0 +1,342 @@
#!/usr/bin/env bash
# clang-build.sh - Build an Apple IIgs binary with the llvm-mos w65816
# toolchain (clang + llvm-mc + link816).
#
# Usage:
# clang-build.sh [-I path]... [-Dname[=val]]... [-s stacksize] [-b] \
# [-M depfile] -o OUTPUT MAIN.c [EXTRA.c|.s ...]
#
# -I path extra include dir (repeatable)
# -Dname preprocessor define, passed to clang (probe builds)
# -s SIZE accepted for compatibility; llvm-mos sizes the stack in
# crt0, so this is a no-op here.
# -b accepted for compatibility; no-op (clang pointers are
# already 32-bit).
# -M depfile emit a combined header-dependency file (clang -MM) so make
# rebuilds the binary when a shared header changes.
# -o OUTPUT output path for the GS/OS S16 OMF (ExpressLoad-wrapped,
# relocatable) that the System Loader / Finder can launch.
#
# C sources are compiled with clang; .s / .asm sources are assembled with
# llvm-mc; everything links against the prebuilt w65816 runtime (with the
# GS/OS-aware crt0Gsos). link816 dead-strips unreferenced sections, then
# omfEmit wraps the relocatable image as an S16 OMF so the Loader can place
# it at any Memory-Manager-chosen address.
#
# Requires LLVM816_ROOT (toolchain root) with a built runtime
# ($LLVM816_ROOT/runtime/*.o). make exports it; from a shell, source
# toolchains/env.sh.
set -euo pipefail
: "${LLVM816_ROOT:?clang-build.sh: LLVM816_ROOT unset; run ./install.sh install toolchain:iigs, then use make or source toolchains/env.sh}"
CLANG="$LLVM816_ROOT/tools/llvm-mos-build/bin/clang"
LLVM_MC="$LLVM816_ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$LLVM816_ROOT/tools/link816"
OMFEMIT="$LLVM816_ROOT/tools/omfEmit"
RT="$LLVM816_ROOT/runtime"
RTINC="$RT/include"
for t in "$CLANG" "$LLVM_MC" "$LINK" "$OMFEMIT"; do
[ -x "$t" ] || { echo "clang-build.sh: missing tool: $t" >&2; exit 1; }
done
[ -f "$RT/crt0Gsos.o" ] || { echo "clang-build.sh: runtime not built ($RT/crt0Gsos.o missing); run runtime/build.sh" >&2; exit 1; }
includes=()
output=""
depfile=""
srcs=()
while [ $# -gt 0 ]; do
case "$1" in
-I) includes+=(-I "$2"); shift 2 ;;
-I*) includes+=("$1"); shift ;; # combined -Ipath form
-D*) includes+=("$1"); shift ;; # preprocessor define (probe builds)
-o) output="$2"; shift 2 ;;
-s) shift 2 ;; # stack size: no-op (crt0 handles it)
-b) shift ;; # accepted for compatibility: no-op
-M) depfile="$2"; shift 2 ;;
-*) echo "clang-build.sh: unknown flag $1" >&2; exit 2 ;;
*) srcs+=("$1"); shift ;;
esac
done
[ -n "$output" ] || { echo "clang-build.sh: -o OUTPUT required" >&2; exit 2; }
[ ${#srcs[@]} -gt 0 ] || { echo "clang-build.sh: no source files" >&2; exit 2; }
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
CFLAGS=(--target=w65816 -O2 -ffreestanding -ffunction-sections -I"$RTINC" "${includes[@]}")
# Curated runtime object set for GS/OS-launched OMFs. crt0Gsos.o (the
# System-Loader-aware crt) MUST be first so __start lands at --text-base.
# link816 dead-strips whatever a given binary doesn't use, so offering the
# whole runtime (libc helpers, soft-float, toolbox, GS/OS) is free.
runtimeObjs=(
"$RT/crt0Gsos.o" "$RT/libgcc.o" "$RT/libc.o"
"$RT/strtol.o" "$RT/snprintf.o" "$RT/sscanf.o" "$RT/qsort.o"
"$RT/strtok.o" "$RT/extras.o" "$RT/timeExt.o" "$RT/math.o"
"$RT/softFloat.o" "$RT/softDouble.o"
"$RT/iigsToolbox.o" "$RT/iigsToolboxExtra.o" "$RT/iigsGsos.o"
)
objs=()
depsrcs=()
objIdx=0
for s in "${srcs[@]}"; do
# Unique object name per source. A plain basename collides when two
# sources share a name (e.g. src/core/input.c and src/iigs/
# input.c both -> input.o), silently clobbering one and dropping its
# symbols. Index-prefix to guarantee uniqueness.
b="$(basename "$s")"
o="$tmpdir/${objIdx}_${b%.*}.o"
objIdx=$((objIdx + 1))
case "$s" in
*.c)
# Route through the regalloc fallback so a register-heavy TU
# (greedy "ran out of registers" on the single-accumulator
# 65816 -- e.g. AGI's main()) auto-retries with -regalloc=basic
# instead of breaking the build, exactly like runtime/build.sh.
"$LLVM816_ROOT/scripts/ccRegallocFallback.sh" "$CLANG" "${CFLAGS[@]}" -c "$s" -o "$o"
depsrcs+=("$s")
;;
*.s|*.asm)
"$LLVM_MC" -arch=w65816 -filetype=obj "$s" -o "$o"
;;
*.o|*.a)
# Prebuilt object or static library (e.g. libjoey.a): link816 reads
# both directly and --gc-sections selects what's referenced. Pass
# the real path through untouched.
objs+=("$s")
continue
;;
*)
echo "clang-build.sh: skipping unrecognized source: $s" >&2
continue
;;
esac
objs+=("$o")
done
# Object order is no longer layout-sensitive. The multi-segment crash that
# made it matter was the OMF segment header carrying ALIGN=0, so the System
# Loader placed overflow segments at a non-bank-aligned address (e.g.
# $07:BB11). Code uses 16-bit self-references (computed-goto `rts` tables,
# intra-segment branches) whose operands equal the *link* offset, which only
# resolve when the segment sits at bank:0000. omfEmit now emits ALIGN=0x10000
# so every code segment is bank-aligned and runtime_offset == link_offset in
# ANY split. Objects therefore build in compile order (deterministic from the
# source list); no sort needed. (Root-caused via the MAME debugger: seg2 main
# landed at $07:BB11, jlInit at +0x1E02=$07:D913, computed-gotos jumped wild.)
# Relocatable GS/OS OMF build. The System Loader places the image at a
# Memory-Manager-chosen address, so the OMF carries relocation records
# (--reloc-out -> --relocs). --text-base 0 (NOT 0x1000): the Loader
# bank-aligns KIND=0x1000 segments, so link-time IMM16 operands already
# equal their runtime segment offsets and link816 skips the IMM16 cRELOC
# entries (50-90% fewer relocs -> proportionally faster Loader boot).
# --bss-base 0xA000 keeps BSS above the SHR shadow ($2000-$9FFF in bank 0)
# so global writes don't scribble the screen. crt0Gsos.o (first object)
# puts __start at the segment base.
linkBin="$tmpdir/link.bin"
linkMap="$tmpdir/link.map"
linkReloc="$tmpdir/link.reloc"
linkManifest="$tmpdir/link.json"
linkErr="$tmpdir/link.err"
# The GS/OS load name is the output basename, uppercased to <=8 alnum chars.
omfName="$(basename "$output" | tr '[:lower:]' '[:upper:]' | tr -cd 'A-Z0-9' | cut -c1-8)"
[ -n "$omfName" ] || omfName="JOEYAPP"
# link816 emits an ExpressLoad S16 OMF. Two distinct overflows force the
# --full-bank: a GS/OS OMF image is loaded into a Memory-Manager-chosen
# bank, not bank 0, so there is no IO window or language card to dodge and
# all 64K is usable. link816 defaults to the bank-0-safe layout because its
# other consumers (clang-smoke.sh and the bench/probe scripts) really do
# raw-load into bank 0 -- see the fullBank comment in link816.cpp.
#
# MULTI-segment path: (a) text overrunning the entry bank, or
# (b) BSS overflowing the entry bank's leftover space. crt0Gsos sets
# DBR = the entry segment's bank and C globals are reached DBR-relative, so
# BSS can NOT be moved to its own bank -- instead we push TEXT into overflow
# banks (a low --segment-cap) to free the entry bank for rodata + BSS, and
# raise --bss-base so BSS sits at the top of the entry bank. The GS/OS
# Loader places each bank-aligned segment + applies the per-seg relocations.
emitMultiSeg() { # $1 = segment-cap, $2 = bss-base
"$LINK" -o "$linkBin" --text-base 0 --bss-base "$2" --segment-cap "$1" \
--overflow-cap "$JOEY_OVERFLOW_CAP" --full-bank \
--manifest "$linkManifest" --map "$linkMap" --reloc-out "$linkReloc" \
--multi-seg-expressload "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1
"$OMFEMIT" --manifest "$linkManifest" --expressload --relocs "$linkReloc" \
--output "$output"
}
# The C heap only has to be LARGE ENOUGH. It used to also have to sit below
# "the bank-0 IO window ($C000)", which was wrong: the program does not run
# in bank 0. Measured under MAME, joeylib's examples execute in banks
# $08-$0B, where there is no IO window and no language card, and link816's
# map is bank-relative (--text-base 0), so that test was comparing a bank
# OFFSET against a bank-0 address. link816's __heap_start comment has the
# full reasoning; the heap now runs to the top of the entry bank.
#
# This used to test only that the heap STARTED below $C000, which let the
# descending-cap search stop at the first layout that merely cleared the
# window -- even when almost nothing was left under it. A program that
# REDUCED its bss could therefore get a worse image: shrinking bss moves
# __heap_start down, so a layout that previously failed the test now
# passes it with a few hundred bytes of heap, and the search stops there.
# Space Taxi hit exactly this, linking a 778-byte heap (vs 4802 before);
# it drew its first screen and then froze, because every allocation after
# that failed. The big buffers (codegen arena, stage pixels, audio) come
# from the Memory Manager, but stdio FILE buffers and codegen arena slots
# come from this heap, so "a few hundred bytes" is never enough.
#
# If no cap reaches the minimum the caller still falls back to the highest
# cap that linked, so this can only reject layouts that were broken.
# Minimum size for the link-time heap WINDOW, in bytes. DEFAULT 0, and
# no target needs to raise it any more.
#
# What this window is: the free space above bss in the ENTRY BANK. In a
# program that links libc it is NOT the heap -- __heapInitMM asks the
# Memory Manager for a handle (which carries no attrBank, so it can land
# in any bank) and malloc runs in that. The window is only addressed
# directly if that allocation fails.
#
# The old measurements below were read as "each program needs its own
# minimum heap", with the odd result that MORE heap could break a program:
#
# STAXI 778 B -> froze on its first allocation
# STAXI 2426 B -> still froze
# AGI 3337 B -> runs KQ3 (19 segments)
# STAXI 4802 B -> runs (25 segments)
# AGI 7689 B -> BROKE: launched, then did nothing
#
# That last row is not mysterious: libc used to pass the window straight
# to NewHandle when it fell in [0x2000, 0xC000] and substitute a 32 KB
# default otherwise. 3337 is below 0x2000, so AGI got 32 KB and ran; 7689
# is inside the range, so AGI asked for 7689 -- less than it needs -- and
# drew nothing. The freezes are the same story with the Memory Manager
# exhausted by a many-segment image, so NewHandle failed and malloc fell
# back to the tiny window. Reproduced exactly while fixing the IO-window
# assumption, and fixed in libc.c: the window is now a floor to beat, not
# the request, so a bigger window can never mean a smaller heap.
JOEY_MIN_HEAP="${JOEY_MIN_HEAP:-0}"
# How much text an OVERFLOW segment may hold. --segment-cap bounds the
# ENTRY bank, which also carries rodata, bss and the heap -- for Space
# Taxi only ~12K of its 64K is left for text. An
# overflow bank carries text ONLY, so capping it the same way wasted ~50K
# per bank: 177K of code needed 17 bank-aligned segments instead of 4.
# Each segment must still fit one bank (16-bit intra-segment addressing),
# so this stays below 0x10000.
#
# DEFAULT 0 = overflow segments use --segment-cap (the historical
# behaviour); each target opts in from make/iigs.mk. AGI links 5 segments
# instead of 19, STAXI 4 instead of 26.
#
# Raising this on STAXI used to produce a dead image, which looked like a
# segment-size limit. It was not -- AGI ships 28798-byte segments and runs.
# The real chain: 26 bank-aligned segments starved the Memory Manager, so
# NTP's 34K NewHandle failed, gNTPReady stayed 0 and every SFX call
# short-circuited. IIgs builds were simply SILENT. Freeing that memory let
# audio initialise, and a latent bug fired the first time NTPstreamsound
# was ever reached (fixed in src/iigs/audio_full.c: SFX now requires a
# running replayer, as jlpAudioStopSfx already did).
#
# Each segment must still fit one bank (16-bit intra-segment addressing),
# so this stays below 0x10000.
JOEY_OVERFLOW_CAP="${JOEY_OVERFLOW_CAP:-0}"
heapUsable() {
local hs he
hs=$(grep -wE '__heap_start' "$linkMap" 2>/dev/null \
| grep -oE '0x[0-9a-fA-F]+' | head -1)
he=$(grep -wE '__heap_end' "$linkMap" 2>/dev/null \
| grep -oE '0x[0-9a-fA-F]+' | head -1)
[ -n "$hs" ] && [ -n "$he" ] || return 1
# No IO-window test: the heap is not in bank 0. See the __heap_start
# comment in link816.cpp -- GS/OS loads bank-aligned segments into
# free banks ($08-$0B here), where the whole 64K is plain RAM, and in
# a program that links libc this window is only a size request to the
# Memory Manager anyway.
[ $(( he - hs )) -ge $(( JOEY_MIN_HEAP )) ]
}
# Multi-seg, choosing the HIGHEST --segment-cap that BOTH links AND leaves a
# usable C heap. A lower cap pushes more TEXT into overflow banks, freeing
# entry-bank space for rodata + BSS + the heap; BSS packs
# tight (--bss-base 0x2000 -- safe, a relocatable multi-seg image loads high,
# never bank 0/1). The highest qualifying cap is preferred: fewest segments,
# fastest Loader boot.
#
# Do NOT "improve" this by falling back to whichever cap yields the BIGGEST
# heap. Tried 2026-09-19: for Space Taxi that selects cap 0x1000, and the
# resulting 49-segment image renders garbage (SCB byte reads back $DD). Low
# caps break for reasons this script cannot see, so the search stays anchored
# to the highest cap that qualifies, and the fallback stays the highest cap
# that merely linked.
# Any of these means seg 1 is too full for bank 0 and a smaller cap may
# fit: BSS, the trampoline table, or rodata + init_array (which follow
# seg 1's text) ran past the ceiling.
ENTRY_BANK_OVERFLOW='bss .*exceeds bank-0 ceiling|trampoline table pushes seg 1 over|rodata .*exceeds bank-0'
emitMultiSegFit() {
local cap
local fallback=""
for cap in 0xB000 0xA000 0x9000 0x8000 0x7000 0x6000 0x5000 0x4000 0x3000 0x2000 0x1000; do
if emitMultiSeg "$cap" 0x2000; then
[ -z "$fallback" ] && fallback="$cap"
heapUsable && return 0
else
# Keep shrinking only on a fixable entry-bank overflow; bail on
# anything else (unsplittable text, an unresolved symbol, ...).
grep -qE "$ENTRY_BANK_OVERFLOW" "$linkErr" || break
fi
done
# Nothing reached JOEY_MIN_HEAP; re-link the highest that linked and say so,
# because the image may well fail at run time for want of heap.
if [ -n "$fallback" ]; then
echo "clang-build: WARNING no segment cap reached a ${JOEY_MIN_HEAP}-byte" \
"heap; using $fallback -- the image may fail to allocate" >&2
emitMultiSeg "$fallback" 0x2000
return $?
fi
# Nothing linked at all. This used to fall off the end and return the
# status of the false `if` above -- 0 -- so the caller never printed
# the linker error and the build "succeeded" with no output file.
return 1
}
if "$LINK" -o "$linkBin" --text-base 0 --bss-base 0xA000 --full-bank \
--map "$linkMap" --reloc-out "$linkReloc" \
"${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then
# Single-segment AND the heap is big enough: wrap as-is.
"$OMFEMIT" --input "$linkBin" --map "$linkMap" --base 0 --entry __start \
--output "$output" --name "$omfName" --expressload --relocs "$linkReloc"
else
# Single-seg overflowed (text/BSS/trampoline) OR it linked with too
# small a heap -- go multi-seg with a heap-aware descending cap.
emitMultiSegFit || { cat "$linkErr" >&2; exit 1; }
fi
# The link map is the only record of where a symbol landed, and the OMF
# carries no symbols at all, so anything that inspects a running program
# (the PC profiler, the MAME state gates) needs it kept. Off by default.
if [ -n "${JOEY_LINK_MAP:-}" ] && [ -f "$linkMap" ]; then
mkdir -p "$(dirname "$JOEY_LINK_MAP")"
cp "$linkMap" "$JOEY_LINK_MAP"
fi
# Combined header-dependency file: union of clang -MM over the C sources,
# rewritten to a single target ($output) so make rebuilds the binary when
# any transitively-included header changes.
if [ -n "$depfile" ] && [ ${#depsrcs[@]} -gt 0 ]; then
mkdir -p "$(dirname "$depfile")"
{
printf '%s:' "$output"
for s in "${depsrcs[@]}"; do
"$CLANG" "${CFLAGS[@]}" -MM "$s" 2>/dev/null \
| sed -e 's/^[^:]*://' -e 's/\\$//' | tr -d '\n'
done
printf '\n'
} > "$depfile"
fi

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
# clang-smoke.sh - End-to-end smoke test of the llvm-mos w65816 IIgs
# build path. Compiles a tiny C main that calls JoeyLib's ported asm
# (iigsInitRowLut), links it with clang-build.sh, runs it under MAME's
# apple2gs, and verifies the LUT the asm computed.
#
# This proves the whole toolchain end to end: clang -> llvm-mc ->
# link816 -> runnable IIgs image -> correct runtime result. Exit 0 on
# success. Needs the toolchain:iigs install (make exports its paths).
set -euo pipefail
: "${LLVM816_ROOT:?LLVM816_ROOT unset: run ./install.sh install toolchain:iigs, then use make or source toolchains/env.sh}"
: "${MAME:?MAME unset: run ./install.sh install emulator:iigs}"
: "${IIGS_ROMPATH:?IIGS_ROMPATH unset: run ./install.sh install emulator:iigs}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../../.." && pwd)"
RUN_IN_MAME="$LLVM816_ROOT/scripts/runInMame.sh"
[ -x "$RUN_IN_MAME" ] || { echo "clang-smoke: $RUN_IN_MAME missing" >&2; exit 1; }
# runInMame.sh takes MAME and MAME_ROMPATH from the environment, so it
# runs the installer's patched MAME against the installer's ROM set.
export MAME
export MAME_ROMPATH="$IIGS_ROMPATH"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
# gRowOffsetLut[5] = 5 * 160 = 800 = 0x0320. Publish it to a 24-bit
# address MAME can read back (bank 2 is clear of IIgs ROM scribbling).
cat > "$tmp/smoke.c" <<'EOF'
#include <stdint.h>
extern void iigsInitRowLut(void);
extern uint16_t gRowOffsetLut[];
int main(void) {
iigsInitRowLut();
*(volatile uint16_t *)0x025000 = gRowOffsetLut[5];
return 0;
}
EOF
# runInMame.sh raw-loads the image at $00:1000 and jumps there, so the
# smoke needs a BARE binary -- NOT the ExpressLoad GS/OS OMF (crt0Gsos.o,
# Loader-relocated) that clang-build.sh now emits. Build it directly with
# the bare crt0.o at --text-base 0x1000. link816 dead-strips the unused
# runtime, so offering libgcc/libc/extras is free.
echo "[clang-smoke] building bare smoke.bin (crt0.o, --text-base 0x1000)"
RT="$LLVM816_ROOT/runtime"
CLANG="$LLVM816_ROOT/tools/llvm-mos-build/bin/clang"
LLVM_MC="$LLVM816_ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$LLVM816_ROOT/tools/link816"
"$CLANG" --target=w65816 -O2 -ffreestanding -ffunction-sections \
-I"$RT/include" -c "$tmp/smoke.c" -o "$tmp/smoke.o"
"$LLVM_MC" -arch=w65816 -filetype=obj \
"$REPO/src/iigs/joeyDraw.s" -o "$tmp/joeyDraw.o"
"$LINK" -o "$tmp/smoke.bin" --text-base 0x1000 \
"$RT/crt0.o" "$RT/libgcc.o" "$RT/libc.o" "$RT/extras.o" \
"$tmp/smoke.o" "$tmp/joeyDraw.o"
echo "[clang-smoke] running under MAME apple2gs, checking gRowOffsetLut[5] == 0x0320"
bash "$RUN_IN_MAME" "$tmp/smoke.bin" --check 0x025000=0320
echo "[clang-smoke] PASS"

View file

@ -4,8 +4,8 @@
# generic backend -- no native primitives, no audio, no sprite codegen yet. # generic backend -- no native primitives, no audio, no sprite codegen yet.
# See claudeDocs/x68000_port.md for what is and is not real. # See claudeDocs/x68000_port.md for what is and is not real.
# #
# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), staged into # The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), installed
# toolchains/x68000 by the tarball. m68k-xelf-gcc emits ELF; the driver's # by ./install.sh (X68K_ROOT). m68k-xelf-gcc emits ELF; the driver's
# -Wl,-elf2x68k pass converts it to a Human68k .X. # -Wl,-elf2x68k pass converts it to a Human68k .X.
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
@ -15,12 +15,14 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
# make/x68000.mk` reports only "libxmplite.a is up to date" and builds nothing). # make/x68000.mk` reports only "libxmplite.a is up to date" and builds nothing).
.DEFAULT_GOAL := all .DEFAULT_GOAL := all
$(call requireTool,$(X68K_ROOT)/bin/m68k-xelf-gcc,toolchain:x68000)
$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:x68000)
PLATFORM := x68000 PLATFORM := x68000
BUILD := $(REPO_DIR)/build/$(PLATFORM) BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
X68K_ROOT := $(REPO_DIR)/toolchains/x68000/m68k-xelf
X68K_CC := $(X68K_ROOT)/bin/m68k-xelf-gcc X68K_CC := $(X68K_ROOT)/bin/m68k-xelf-gcc
X68K_AR := $(X68K_ROOT)/bin/m68k-xelf-ar X68K_AR := $(X68K_ROOT)/bin/m68k-xelf-ar
@ -32,7 +34,7 @@ export PATH := $(X68K_ROOT)/bin:$(PATH)
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 \ -DJOEYLIB_PLATFORM_X68000 \
-I$(REPO_DIR)/toolchains/audio/libxmp-lite/include \ -I$(LIBXMP_DIR)/include \
-I$(SRC_DIR)/x68000 -I$(SRC_68K) -I$(REPO_DIR)/src/codegen -MMD -MP $(CFLAGS_EXTRA) -I$(SRC_DIR)/x68000 -I$(SRC_68K) -I$(REPO_DIR)/src/codegen -MMD -MP $(CFLAGS_EXTRA)
LDFLAGS := -lm LDFLAGS := -lm
@ -73,7 +75,7 @@ LIB_OBJS := \
# libxmp-lite: the Protracker decoder shared with the DOS and ST ports. Same # libxmp-lite: the Protracker decoder shared with the DOS and ST ports. Same
# build recipe as make/atarist.mk -- LIBXMP_CORE_DISABLE_IT keeps it off math.h, # build recipe as make/atarist.mk -- LIBXMP_CORE_DISABLE_IT keeps it off math.h,
# which this newlib does not usefully provide for a 68000 target. # which this newlib does not usefully provide for a 68000 target.
LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite # LIBXMP_DIR comes from the installer's config.mk.
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC))
@ -173,9 +175,6 @@ $(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR)
# expands prerequisites when it reads the rule, so these come first. # expands prerequisites when it reads the rule, so these come first.
STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000 STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000
STAXI_RUN_DIR := $(BINDIR)/DATA STAXI_RUN_DIR := $(BINDIR)/DATA
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/romToLevel.py
STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
STAXI_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ STAXI_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat $(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN))
@ -227,8 +226,8 @@ $(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN)
# Space Taxi. -s (strip) because the image it ships on is a 1232 KB # Space Taxi. -s (strip) because the image it ships on is a 1232 KB
# Human68k floppy: the unstripped binary is most of the volume on its # Human68k floppy: the unstripped binary is most of the volume on its
# own. The DATA tree next to it (levels + the precompiled cel bank) is # own. The DATA tree next to it (levels + the precompiled cel bank) is
# what scripts/run-x68000.sh copies onto the data floppy. # what joey-run copies onto the data floppy.
$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(STAXI_ASSET_DSTS) $(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(call staxiData,$(STAXI_ASSET_DSTS))
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@

View file

@ -1,53 +0,0 @@
--- a/src/mame/sharp/x68k.h 2026-08-04 18:08:43.712316105 -0500
+++ b/src/mame/sharp/x68k.h 2026-08-04 18:06:29.498097848 -0500
@@ -172,18 +172,10 @@
uint8_t hdcvector = 0;
uint8_t prnvector = 0;
} m_ioc;
- struct
- {
- int inputtype = 0; // determines which input is to be received
- bool irqactive = false; // true if IRQ is being serviced
- uint8_t irqvector = 0;
- char last_mouse_x = 0; // previous mouse x-axis value
- char last_mouse_y = 0; // previous mouse y-axis value
- int bufferempty = 0; // non-zero if buffer is empty
- } m_mouse;
uint8_t m_ppi_portc = 0;
bool m_dmac_int = false;
bool m_mfp_int = false;
+ bool m_scc_int = false;
bool m_exp_irq2[2]{};
bool m_exp_irq4[2]{};
bool m_exp_nmi[2]{};
@@ -195,7 +187,6 @@
uint32_t m_gvram_wait = 0;
uint32_t m_tvram_wait = 0;
uint32_t m_vram_wait_accum = 0; // 16.16 carry between accesses
- emu_timer* m_mouse_timer = nullptr;
emu_timer* m_led_timer = nullptr;
unsigned char m_scc_prev = 0;
emu_timer* m_fdc_tc = nullptr;
--- a/src/mame/sharp/x68k.cpp 2026-08-04 18:08:43.712316105 -0500
+++ b/src/mame/sharp/x68k.cpp 2026-08-04 18:06:57.240157174 -0500
@@ -1236,6 +1236,20 @@
mouse.rxd_handler().set(m_scc, FUNC(scc8530_device::rxb_w));
m_scc->out_rtsb_callback().set(mouse, FUNC(rs232_port_device::write_rts));
+ // RS-232C -- SCC channel A. Only channel B (the mouse) was ever wired, so
+ // the machine's actual serial port had no connection at all and could not
+ // be exercised. Exposing it as a standard rs232 slot makes
+ // `-rs232c null_modem` (TCP socket) and `-rs232c pty` available, which is
+ // what an automated both-directions serial test needs.
+ m_scc->out_txda_callback().set("rs232c", FUNC(rs232_port_device::write_txd));
+ m_scc->out_dtra_callback().set("rs232c", FUNC(rs232_port_device::write_dtr));
+ m_scc->out_rtsa_callback().set("rs232c", FUNC(rs232_port_device::write_rts));
+
+ rs232_port_device &rs232c(RS232_PORT(config, "rs232c", default_rs232_devices, nullptr));
+ rs232c.rxd_handler().set(m_scc, FUNC(scc8530_device::rxa_w));
+ rs232c.dcd_handler().set(m_scc, FUNC(scc8530_device::dcda_w));
+ rs232c.cts_handler().set(m_scc, FUNC(scc8530_device::ctsa_w));
+
RP5C15(config, m_rtc, 32.768_kHz_XTAL);
m_rtc->alarm().set(m_mfpdev, FUNC(mc68901_device::i0_w));
m_rtc->set_year_offset(20);

View file

@ -1,141 +0,0 @@
--- a/src/mame/sharp/x68k.h 2026-08-04 16:54:53.872522860 -0500
+++ b/src/mame/sharp/x68k.h 2026-08-04 16:54:53.876522842 -0500
@@ -195,6 +195,12 @@
bool m_exp_nmi[2]{};
uint8_t m_current_ipl = 0;
int m_led_state = 0;
+ // Extra cycles charged per VRAM access, over and above a main-memory
+ // access, in 16.16 fixed point. Set from measured values in machine_start
+ // for the machines those measurements cover; 0 (unmodelled) otherwise.
+ uint32_t m_gvram_wait = 0;
+ uint32_t m_tvram_wait = 0;
+ uint32_t m_vram_wait_accum = 0; // 16.16 carry between accesses
emu_timer* m_mouse_timer = nullptr;
emu_timer* m_led_timer = nullptr;
unsigned char m_scc_prev = 0;
@@ -265,6 +271,15 @@
uint16_t exp_r(offs_t offset, uint16_t mem_mask = ~0);
void exp_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+ // VRAM wait states. The X68000's video RAM is slower than main memory;
+ // MAME charged nothing for it, so VRAM-bound code ran optimistically
+ // fast. These trampolines charge the difference and forward to the CRTC.
+ void charge_vram_wait(uint32_t increment);
+ uint16_t gvram_wait_r(offs_t offset);
+ void gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+ uint16_t tvram_wait_r(offs_t offset);
+ void tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+
uint16_t spritereg_r(offs_t offset);
void spritereg_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
uint16_t spriteram_r(offs_t offset);
--- a/src/mame/sharp/x68k.cpp 2026-08-04 16:54:53.871522864 -0500
+++ b/src/mame/sharp/x68k.cpp 2026-08-04 16:54:53.876522842 -0500
@@ -992,12 +992,80 @@
// Is this an undocumented MB89352 feature, an ASIC register, an original code bug or a bad dump?
}
+// VRAM wait states.
+//
+// The 68000 in an X68000 does not reach video RAM as fast as it reaches main
+// memory, and MAME modelled no wait at all -- so any VRAM-bound inner loop ran
+// optimistically fast, which is exactly the code a graphics library cares
+// about. The figures used here are Makoto Kamada's real-hardware measurements
+// distributed with XEiJ (credited there to uchopon, tnb and ita), taken by
+// timing tst.w against each region on five 10 MHz X68000 variants:
+//
+// region tst.w time (us) derived wait (cycles)
+// main memory 1.642 .. 1.663 0.123
+// GVRAM 1.734 .. 1.770 1.145
+// TVRAM 1.829 .. 1.850 2.046
+//
+// MAME charges nothing for main memory, so what is added here is the wait
+// RELATIVE to main memory: 1.145 - 0.123 = 1.022 cycles for GVRAM and
+// 2.046 - 0.123 = 1.923 for TVRAM. Cross-check against the raw measurements:
+// GVRAM - main memory averages ~1.03 cycles and TVRAM - main memory ~1.92
+// across the five machines, which agrees.
+//
+// Main memory's own 0.123 cycles (DRAM refresh) is deliberately NOT modelled:
+// it would mean interposing on the hottest path in the driver to recover ~0.1
+// cycles per access, and it is a uniform offset that does not change the
+// relative cost of VRAM versus RAM, which is the thing being measured.
+//
+// The wait is fractional, so it accumulates in 16.16 fixed point and whole
+// cycles are charged as they carry out -- the same technique the apple2gs
+// driver uses for its 1 MHz cycles (see slow_cycle() there).
+#define X68K_GVRAM_WAIT 0x000105a2 // 1.022 cycles, 16.16
+#define X68K_TVRAM_WAIT 0x0001ec4a // 1.923 cycles, 16.16
+
+void x68k_state::charge_vram_wait(uint32_t increment)
+{
+ // side_effects_disabled() covers debugger and Lua reads, which must not
+ // perturb the timing they are measuring.
+ if (increment == 0 || machine().side_effects_disabled())
+ return;
+ m_vram_wait_accum += increment;
+ int const cycles = m_vram_wait_accum >> 16;
+ m_vram_wait_accum &= 0xffff;
+ if (cycles != 0)
+ m_maincpu->adjust_icount(-cycles);
+}
+
+uint16_t x68k_state::gvram_wait_r(offs_t offset)
+{
+ charge_vram_wait(m_gvram_wait);
+ return m_crtc->gvram_r(offset);
+}
+
+void x68k_state::gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+ charge_vram_wait(m_gvram_wait);
+ m_crtc->gvram_w(offset, data, mem_mask);
+}
+
+uint16_t x68k_state::tvram_wait_r(offs_t offset)
+{
+ charge_vram_wait(m_tvram_wait);
+ return m_crtc->tvram_r(offset);
+}
+
+void x68k_state::tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+ charge_vram_wait(m_tvram_wait);
+ m_crtc->tvram_w(offset, data, mem_mask);
+}
+
void x68k_state::x68k_base_map(address_map &map)
{
map(0x000000, 0xbffffb).rw(FUNC(x68k_state::emptyram_r), FUNC(x68k_state::emptyram_w));
map(0xbffffc, 0xbfffff).rw(FUNC(x68k_state::rom0_r), FUNC(x68k_state::rom0_w));
- map(0xc00000, 0xdfffff).rw(m_crtc, FUNC(x68k_crtc_device::gvram_r), FUNC(x68k_crtc_device::gvram_w));
- map(0xe00000, 0xe7ffff).rw(m_crtc, FUNC(x68k_crtc_device::tvram_r), FUNC(x68k_crtc_device::tvram_w));
+ map(0xc00000, 0xdfffff).rw(FUNC(x68k_state::gvram_wait_r), FUNC(x68k_state::gvram_wait_w));
+ map(0xe00000, 0xe7ffff).rw(FUNC(x68k_state::tvram_wait_r), FUNC(x68k_state::tvram_wait_w));
map(0xe80000, 0xe81fff).rw(m_crtc, FUNC(x68k_crtc_device::crtc_r), FUNC(x68k_crtc_device::crtc_w));
map(0xe82400, 0xe83fff).rw(FUNC(x68k_state::vid_r), FUNC(x68k_state::vid_w));
map(0xe84000, 0xe85fff).rw(m_hd63450, FUNC(hd63450_device::read), FUNC(hd63450_device::write));
@@ -1155,6 +1223,24 @@
m_spriteram = (uint16_t*)(memregion("user1")->base());
space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer());
+ // VRAM wait states, applied only to the machine the measurements cover.
+ // The published figures are for a 10 MHz 68000 X68000; XEiJ's table also
+ // lists a 25 MHz X68030 (GVRAM 6.377, TVRAM 6.623) but those machines are
+ // MACHINE_NOT_WORKING here and cannot be validated, so they are left
+ // unmodelled rather than given invented numbers.
+ if (m_maincpu->clock() == 10'000'000)
+ {
+ m_gvram_wait = X68K_GVRAM_WAIT;
+ m_tvram_wait = X68K_TVRAM_WAIT;
+ }
+ else
+ {
+ m_gvram_wait = 0;
+ m_tvram_wait = 0;
+ }
+ m_vram_wait_accum = 0;
+ save_item(NAME(m_vram_wait_accum));
+
// start mouse timer
m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now
m_mouse.inputtype = 0;

View file

@ -11,25 +11,23 @@
# scripts/bench-amiga.sh # default 150s timeout # scripts/bench-amiga.sh # default 150s timeout
# TIMEOUT=240 scripts/bench-amiga.sh # override # TIMEOUT=240 scripts/bench-amiga.sh # override
# #
# Real Kickstart (toolchains/emulators/support/kickstart.rom) is # Real Kickstart (installer component `kickstart`) is
# required -- the HAL drives OCS directly and AROS is unreliable for it. # required -- the HAL drives OCS directly and AROS is unreliable for it.
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed FSUAE fsuae
joeyNeed KICKSTART_ROM kickstart
bin_dir=$repo/build/amiga/bin bin_dir=$repo/build/amiga/bin
support=$repo/toolchains/emulators/support kickstart=$KICKSTART_ROM
kickstart=$support/kickstart.rom
timeout_s=${TIMEOUT:-150} timeout_s=${TIMEOUT:-150}
if [[ ! -f $bin_dir/Uber ]]; then if [[ ! -f $bin_dir/Uber ]]; then
echo "Uber not built. Run 'make amiga' first." >&2 echo "Uber not built. Run 'make amiga' first." >&2
exit 1 exit 1
fi fi
if [[ ! -f $kickstart ]]; then
echo "Kickstart ROM missing: $kickstart" >&2
exit 1
fi
work=$(mktemp -d -t joeylib-amiga-bench.XXXXXX) work=$(mktemp -d -t joeylib-amiga-bench.XXXXXX)
mkdir -p "$work/s" mkdir -p "$work/s"
@ -50,7 +48,7 @@ Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$! xvfb_pid=$!
sleep 0.4 sleep 0.4
cleanup() { cleanup() {
kill "$fs_pid" 2>/dev/null || true joeyStop "$fs_pid"
kill "$xvfb_pid" 2>/dev/null || true kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true wait 2>/dev/null || true
rm -rf "$work" rm -rf "$work"
@ -58,7 +56,7 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
echo "bench-amiga: running UBER headless (timeout ${timeout_s}s)..." >&2 echo "bench-amiga: running UBER headless (timeout ${timeout_s}s)..." >&2
DISPLAY=$display fs-uae \ DISPLAY=$display "$FSUAE" \
--amiga_model=A500 \ --amiga_model=A500 \
--fast_memory=2048 \ --fast_memory=2048 \
--hard_drive_0="$work" \ --hard_drive_0="$work" \

View file

@ -20,8 +20,11 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
bin_dir=$repo/build/atarist/bin bin_dir=$repo/build/atarist/bin
tos=$repo/toolchains/emulators/support/emutos-512k.img tos=$EMUTOS_IMG
log=$bin_dir/joeylog.txt log=$bin_dir/joeylog.txt
timeout_s=${TIMEOUT:-90} timeout_s=${TIMEOUT:-90}
@ -41,14 +44,14 @@ Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$! xvfb_pid=$!
sleep 0.4 sleep 0.4
cleanup() { cleanup() {
kill "$hat_pid" 2>/dev/null || true joeyStop "$hat_pid"
kill "$xvfb_pid" 2>/dev/null || true kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true wait 2>/dev/null || true
} }
trap cleanup EXIT trap cleanup EXIT
echo "bench-atarist: running UBER headless (timeout ${timeout_s}s)..." >&2 echo "bench-atarist: running UBER headless (timeout ${timeout_s}s)..." >&2
DISPLAY=$display hatari \ DISPLAY=$display "$HATARI" \
--tos "$tos" \ --tos "$tos" \
--harddrive "$bin_dir" \ --harddrive "$bin_dir" \
--gemdos-drive C \ --gemdos-drive C \

View file

@ -6,7 +6,7 @@
# that log for UBER's "press any key to exit" completion marker, then # that log for UBER's "press any key to exit" completion marker, then
# kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT. # kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT.
# #
# Uses the same dosbox-386sx16.conf as run-dos.sh so the cycle budget # Uses the same dosbox-386sx16.conf as joey-run so the cycle budget
# matches the claudeDocs/PERF.md reference machine. # matches the claudeDocs/PERF.md reference machine.
# #
# scripts/bench-dos.sh # default 90s timeout # scripts/bench-dos.sh # default 90s timeout
@ -15,6 +15,8 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed DOSBOX dosbox
bin_dir=$repo/build/dos/bin bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf conf=$repo/scripts/dosbox-386sx16.conf
log=$bin_dir/JOEYLOG.TXT log=$bin_dir/JOEYLOG.TXT
@ -26,15 +28,17 @@ if [[ ! -f $bin_dir/UBER.EXE ]]; then
fi fi
rm -f "$log" "$bin_dir/joeylog.txt" rm -f "$log" "$bin_dir/joeylog.txt"
work=$(mktemp -d -t joeylib-bench-dos.XXXXXX)
display=:$((90 + RANDOM % 9)) display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 & Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$! xvfb_pid=$!
sleep 0.4 sleep 0.4
cleanup() { cleanup() {
kill "$dosbox_pid" 2>/dev/null || true joeyStop "$dosbox_pid"
kill "$xvfb_pid" 2>/dev/null || true kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true wait 2>/dev/null || true
rm -rf "$work"
} }
trap cleanup EXIT trap cleanup EXIT
@ -44,12 +48,12 @@ echo "bench-dos: running UBER headless (timeout ${timeout_s}s)..." >&2
# "UBER.EXE" ran against a drive that did not exist yet ("Drive C does # "UBER.EXE" ran against a drive that did not exist yet ("Drive C does
# not exist!" / "Illegal command") and the run sat at Z:\> until the # not exist!" / "Illegal command") and the run sat at Z:\> until the
# timeout with no JOEYLOG.TXT. # timeout with no JOEYLOG.TXT.
DISPLAY=$display dosbox \ XDG_CONFIG_HOME="$work/xdg" DISPLAY=$display "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
-conf "$conf" \ --conf "$conf" \
-c "MOUNT C \"$bin_dir\"" \ -c "MOUNT C \"$bin_dir\"" \
-c "C:" \ -c "C:" \
-c "UBER.EXE" \ -c "UBER.EXE" \
>/tmp/bench-dos-dosbox.log 2>&1 & >"$work/dosbox.log" 2>&1 &
dosbox_pid=$! dosbox_pid=$!
waited=0 waited=0

View file

@ -15,23 +15,27 @@
# MAME_ROMPATH override ROM dir (default ~/.mame/roms) # MAME_ROMPATH override ROM dir (default ~/.mame/roms)
# BENCH_MAX_FRAMES hard frame cap before giving up (default 36000) # BENCH_MAX_FRAMES hard frame cap before giving up (default 36000)
# #
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg with UBER on # Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg with UBER on
# it (run 'make iigs-disk', ensure UBER was not skipped for space). # it (run 'make iigs-disk', ensure UBER was not skipped for space).
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?bench-iigs.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$GSOS_SYSTEM_DISK
# Overridable so a diagnostic build (e.g. the -DUBER_PROBE binary built by # Overridable so a diagnostic build (e.g. the -DUBER_PROBE binary built by
# `make iigs-verify-shrtail`) can be run off its own single-app disk into its # `make iigs-verify-shrtail`) can be run off its own single-app disk into its
# own log without disturbing the goldened joey.2mg / joeylog.txt pair. Both # own log without disturbing the goldened joey.2mg / joeylog.txt pair. Both
# default to the standard locations, so normal bench runs are unchanged. # default to the standard locations, so normal bench runs are unchanged.
data_disk="${BENCH_DATA_DISK:-$repo/build/iigs/bin/joey.2mg}" data_disk="${BENCH_DATA_DISK:-$repo/build/iigs/bin/joey.2mg}"
out_log="${BENCH_OUT_LOG:-$repo/build/iigs/bin/joeylog.txt}" out_log="${BENCH_OUT_LOG:-$repo/build/iigs/bin/joeylog.txt}"
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" rompath="$IIGS_ROMPATH"
maxFrames="${BENCH_MAX_FRAMES:-36000}" maxFrames="${BENCH_MAX_FRAMES:-36000}"
[ -x "$CADIUS" ] || { echo "bench-iigs: cadius not found at $CADIUS" >&2; exit 2; } [ -x "$CADIUS" ] || { echo "bench-iigs: cadius not found at $CADIUS" >&2; exit 2; }
@ -212,7 +216,7 @@ LUA
echo "bench-iigs: running UBER headless under MAME (cap $maxFrames frames)..." >&2 echo "bench-iigs: running UBER headless under MAME (cap $maxFrames frames)..." >&2
cd "$work" cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 1200 mame apple2gs \ timeout 1200 "$MAME" apple2gs \
-rompath "$rompath" \ -rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \

View file

@ -25,12 +25,16 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?bench-staxi-title.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$GSOS_SYSTEM_DISK
data_disk=$repo/build/iigs/bin/joey.2mg data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" rompath="$IIGS_ROMPATH"
maxFrames="${STAXI_MAX_FRAMES:-15000}" maxFrames="${STAXI_MAX_FRAMES:-15000}"
maxChanges="${STAXI_MAX_CHANGES:-80}" maxChanges="${STAXI_MAX_CHANGES:-80}"
label="${1:-run}" label="${1:-run}"
@ -148,7 +152,7 @@ LUA
echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2 echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2
cd "$work" cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${STAXI_WALL_TIMEOUT:-900}" mame apple2gs \ timeout "${STAXI_WALL_TIMEOUT:-900}" "$MAME" apple2gs \
-rompath "$rompath" \ -rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \

View file

@ -22,13 +22,15 @@
# larger than the gate's because a real measurement window runs each operation # larger than the gate's because a real measurement window runs each operation
# for hundreds of frames rather than one. # for hundreds of frames rather than one.
# #
# X68K_SCRATCH=<dir with x68mame/> bash scripts/bench-x68000.sh # bash scripts/bench-x68000.sh
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" source "$repo/scripts/lib/joeyEnv.sh"
MAME="$repo/toolchains/cache/mame-mame0264/x68k" joeyNeed MAME mame
TEMPLATE="$SP/x68mame/HUMAN302.XDF" joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
TEMPLATE="$X68K_DIR/HUMAN302.XDF"
FRAMES="${X68K_BENCH_FRAMES:-120000}" FRAMES="${X68K_BENCH_FRAMES:-120000}"
WALL="${X68K_BENCH_WALL:-7200}" WALL="${X68K_BENCH_WALL:-7200}"
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in # Which plane the built port presents into: must match X68K_PRESENT_TEXT in
@ -36,7 +38,9 @@ WALL="${X68K_BENCH_WALL:-7200}"
# the hashes are of the surface in RAM and are the same either way. # the hashes are of the surface in RAM and are the same either way.
PRESENT_PLANE="${X68K_PRESENT_PLANE:-text}" PRESENT_PLANE="${X68K_PRESENT_PLANE:-text}"
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH" joeyNeed X68K_ROOT toolchain:x68000
joeyNeed LIBXMP_DIR libxmp
export PATH="$X68K_ROOT/bin:$PATH"
work=$(mktemp -d -t joey-x68bench.XXXXXX) work=$(mktemp -d -t joey-x68bench.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
@ -48,7 +52,7 @@ trap 'rm -rf "$work"' EXIT
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 \ -DJOEYLIB_PLATFORM_X68000 \
-I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \ -I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \
-I"$repo/toolchains/audio/libxmp-lite/include" \ -I"$LIBXMP_DIR/include" \
"$repo/examples/uber/uber.c" \ "$repo/examples/uber/uber.c" \
"$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \ "$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \
-o "$work/UBER.X" || exit 1 -o "$work/UBER.X" || exit 1
@ -168,7 +172,7 @@ end)
LUA LUA
runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \
-autoboot_script "$work/bench.lua" </dev/null 2>&1 | grep '^BENCH') -autoboot_script "$work/bench.lua" </dev/null 2>&1 | grep '^BENCH')
echo "$runLog" echo "$runLog"

View file

@ -1,65 +0,0 @@
#!/usr/bin/env bash
# captureDos.sh -- run a built DOS example in headless DOSBox via Xvfb
# and snap a PNG of the simulated screen. Lets us verify the runtime
# output without an interactive emulator session.
#
# scripts/captureDos.sh staxi /tmp/staxi.png # default 5s wait
# scripts/captureDos.sh staxi /tmp/staxi.png 12 # 12s wait
#
# The wait time gives the program enough cycles to boot, draw, and
# settle on a steady frame. Increase for slow boots / cold caches.
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "usage: $0 <example-name> <out.png> [wait-seconds]" >&2
exit 2
fi
prog=$1
out=$2
wait_s=${3:-5}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
file=${prog^^}.EXE
appdir=${prog^^}
if [[ -f "$bin_dir/$appdir/$file" ]]; then
mount_dir="$bin_dir/$appdir"
elif [[ -f "$bin_dir/$file" ]]; then
mount_dir="$bin_dir"
else
echo "captureDos: $file not found in $bin_dir" >&2
exit 1
fi
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp &
xvfb_pid=$!
sleep 0.3
trap 'kill $xvfb_pid 2>/dev/null || true; kill $dosbox_pid 2>/dev/null || true' EXIT
# Mount with an explicit -c, not by handing DOSBox the directory as a
# positional argument: 0.74-3 ignored it here and dropped the program at
# "Drive C does not exist", which looks exactly like a program that
# failed to start. The cycles override has to be one argv entry too --
# unquoted it split into three words and ate the path.
dosboxArgs=(-conf "$conf" -set "mouse_capture=seamless")
if [[ -n ${CYCLES:-} ]]; then
dosboxArgs+=(-set "cpu cycles=${CYCLES}")
fi
dosboxArgs+=(-c "mount c \"$mount_dir\"" -c "C:" -c "$file")
DISPLAY=$display SDL_VIDEO_X11_VISUALID="" \
dosbox "${dosboxArgs[@]}" >/tmp/captureDos-dosbox.log 2>&1 &
dosbox_pid=$!
# Wait for DOSBox to boot the program and settle.
sleep "$wait_s"
DISPLAY=$display import -window root -screen "$out" 2>/dev/null
echo "captureDos: wrote $out"
# DOSBox is still running -- the trap will clean it up.

View file

@ -2,13 +2,17 @@
# check-blank.sh - Smoke-test the new-port template (src/blank/blank.c). # check-blank.sh - Smoke-test the new-port template (src/blank/blank.c).
# #
# A blank port has no toolchain, so we compile it with the HOST cc just to # A blank port has no toolchain, so we compile it with the HOST cc just to
# prove two things stay true as the library evolves: # prove three things stay true as the library evolves:
# 1. The template + the JOEYLIB_PLATFORM_BLANK blocks in platform.h still # 1. The template + the JOEYLIB_PLATFORM_BLANK blocks in platform.h still
# compile (catches a new platform-#if dispatch point that forgot a BLANK # compile (catches a new platform-#if dispatch point that forgot a BLANK
# case -- e.g. assetLoad.c's tile target, the sprite emitter selector). # case -- e.g. assetLoad.c's tile target, the sprite emitter selector).
# 2. blank.c + the generics LINK with no unresolved jlp* ops -- i.e. a new # 2. blank.c + the generics LINK with no unresolved jlp* ops -- i.e. a new
# chunky port really does get everything except the platform services for # chunky port really does get everything except the platform services for
# free from src/generic/. # free from src/generic/.
# 3. The same sources stay gnu89-clean: no C99-only construct except `//`
# comments. BeOS R5 builds with gcc 2.95 (x86) and Metrowerks mwcc (PPC),
# and neither speaks C99. This pass is a lint, so it always uses gcc
# (-Wc90-c99-compat is gcc-only) whatever CC says.
# #
# Source set mirrors a real blank-port build: core + ALL of src/generic # Source set mirrors a real blank-port build: core + ALL of src/generic
# (including the stub sprite emitter) + blank.c; NOT src/codegen/spriteCompile.c # (including the stub sprite emitter) + blank.c; NOT src/codegen/spriteCompile.c
@ -22,9 +26,10 @@ trap 'rm -rf "$work"' EXIT
inc=(-Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen) inc=(-Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen)
cd "$repo" cd "$repo"
srcs=(src/core/*.c src/generic/*.c src/blank/blank.c)
fail=0 fail=0
for f in src/core/*.c src/generic/*.c src/blank/blank.c; do for f in "${srcs[@]}"; do
o="$work/$(echo "$f" | tr / _).o" o="$work/$(echo "$f" | tr / _).o"
if ! "$cc" -DJOEYLIB_PLATFORM_BLANK "${inc[@]}" -Wall -c "$f" -o "$o" 2>"$work/err"; then if ! "$cc" -DJOEYLIB_PLATFORM_BLANK "${inc[@]}" -Wall -c "$f" -o "$o" 2>"$work/err"; then
echo "check-blank: COMPILE FAIL: $f" >&2 echo "check-blank: COMPILE FAIL: $f" >&2
@ -54,4 +59,26 @@ if [ -n "$missing" ]; then
exit 1 exit 1
fi fi
echo "check-blank: PASS (blank template compiles + links; all ops resolve via blank.c + generics)" # gnu89 dialect lint. gcc also reports `//` comments under -Wc90-c99-compat
# (once per file); gcc 2.95 and mwcc both accept them, so those are dropped.
# Any other warning, or a hard error such as a for-loop declaration, fails.
# LC_ALL=C: a translated gcc says "Warnung:", which the grep would miss --
# the lint would pass having checked nothing.
command -v gcc >/dev/null || { echo "check-blank: FAIL - gcc not found (the gnu89 lint needs it)" >&2; exit 1; }
lint=""
for f in "${srcs[@]}"; do
status=0
LC_ALL=C gcc -std=gnu89 -Wc90-c99-compat -Wdeclaration-after-statement -fno-diagnostics-show-caret -DJOEYLIB_PLATFORM_BLANK "${inc[@]}" -fsyntax-only "$f" 2>"$work/err" || status=$?
hits=$(grep -E 'warning:|error:' "$work/err" | grep -v 'C++ style comments are incompatible with C90' || true)
if [ "$status" != 0 ] && [ -z "$hits" ]; then
hits=$(cat "$work/err")
fi
[ -z "$hits" ] || lint+="$hits"$'\n'
done
if [ -n "$lint" ]; then
echo "check-blank: FAIL - shared code is not gnu89-clean (BeOS R5 needs gcc 2.95 / mwcc):" >&2
printf '%s' "$lint" | sort -u | sed 's/^/ /' >&2
exit 1
fi
echo "check-blank: PASS (blank template compiles + links; all ops resolve via blank.c + generics; shared code is gnu89-clean)"

View file

@ -7,12 +7,16 @@
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?diag-staxi-screen.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$GSOS_SYSTEM_DISK
data_disk=$repo/build/iigs/bin/joey.2mg data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" rompath="$IIGS_ROMPATH"
dumpFrame="${STAXI_DUMP_FRAME:-9000}" dumpFrame="${STAXI_DUMP_FRAME:-9000}"
work=$(mktemp -d -t joeylib-staxidiag.XXXXXX) work=$(mktemp -d -t joeylib-staxidiag.XXXXXX)
@ -120,7 +124,7 @@ LUA
echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2 echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2
cd "$work" cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 900 mame apple2gs \ timeout 900 "$MAME" apple2gs \
-rompath "$rompath" \ -rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \ -video none -sound none -nothrottle \

View file

@ -1,5 +1,5 @@
# DOSBox config: simulate an Intel 386SX-16 (1988), the slowest 386 # DOSBox Staging config: simulate an Intel 386SX-16 (1988), the slowest
# desktop CPU JoeyLib could realistically be run on. Use this floor # 386 desktop CPU JoeyLib could realistically be run on. Use this floor
# to verify the DOS port still hits its frame budget on the bottom of # to verify the DOS port still hits its frame budget on the bottom of
# the 386 stack rather than coasting on host CPU. # the 386 stack rather than coasting on host CPU.
# #
@ -14,15 +14,19 @@
# defeat slow-CPU simulation). # defeat slow-CPU simulation).
# cputype = 386 386 instruction set (no 486 BSWAP / # cputype = 386 386 instruction set (no 486 BSWAP /
# CMPXCHG, no Pentium MMX). # CMPXCHG, no Pentium MMX).
# cycles = fixed 2200 community-standard approximation for # cpu_cycles, cpu_cycles_protected = 2200
# 386SX-16 throughput in DOSBox. # community-standard approximation for
# DOSBox-Staging deprecates this in favor # 386SX-16 throughput. Staging splits real
# of cpu_cycles, but still accepts it. # and protected mode; JoeyLib's DJGPP
# Vanilla DOSBox and DOSBox-X only know # programs run in protected mode, so both
# the old key, so 'cycles' stays for # are pinned. The window title shows the
# cross-fork portability. # rate in force ("<PROG> - 2200 cycles/ms").
#
# Launch with --noprimaryconf --nolocalconf so nothing from the user's
# own DOSBox configuration leaks into a run.
[cpu] [cpu]
core = normal core = normal
cputype = 386 cputype = 386
cycles = fixed 2200 cpu_cycles = 2200
cpu_cycles_protected = 2200

83
scripts/lib/joeyEnv.sh Normal file
View file

@ -0,0 +1,83 @@
# Sourced by the scripts in scripts/ (and tools/) so every one of them finds
# toolchains the same way: from the environment ./install.sh generates. A script calls joeyNeed for each tool it uses, and gets a clear
# "install this" message (exit 2, which the gate runners treat as SKIP)
# instead of failing halfway through with a missing binary.
JOEY_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [ -f "$JOEY_REPO/toolchains/env.sh" ]; then
# shellcheck source=/dev/null
source "$JOEY_REPO/toolchains/env.sh"
fi
# joeyNeed VAR INSTALL-TARGET -- exit 2 unless VAR names an existing path.
joeyNeed() {
local var="$1"
local target="$2"
if [ -z "${!var:-}" ] || [ ! -e "${!var}" ]; then
echo "$(basename "$0"): $var is not available; run: ./install.sh install $target" >&2
exit 2
fi
}
# joeyNeedFile PATH INSTALL-TARGET -- exit 2 unless PATH exists.
joeyNeedFile() {
if [ ! -e "$1" ]; then
echo "$(basename "$0"): $1 is missing; run: ./install.sh install $2" >&2
exit 2
fi
}
# DOSBox Staging must never read or write the user's own DOSBox setup, even
# ~/.config/dosbox (it creates that tree even with --noprimaryconf). Launch
# it as
# XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" ...
JOEY_DOSBOX_ISOLATE=(--noprimaryconf --nolocalconf)
# joeyFindWindow DISPLAY PID -- print the X window owned by process PID.
# More robust than matching titles, which differ between emulator builds.
joeyFindWindow() {
local win
for win in $(DISPLAY="$1" xdotool search --name -- "." 2>/dev/null); do
if [ "$(DISPLAY="$1" xdotool getwindowpid "$win" 2>/dev/null)" = "$2" ]; then
printf '%s\n' "$win"
return 0
fi
done
return 1
}
# joeyAlive PID -- PID is running. A zombie (exited, not yet reaped) is not,
# although `kill -0` still succeeds on it.
joeyAlive() {
local stat
stat="$(ps -o stat= -p "$1" 2>/dev/null)" || return 1
[[ "$stat" != Z* ]]
}
# joeyStop PID... -- stop emulator processes and make sure they are gone.
# Hatari, MAME and DOSBox Staging ignore or are slow to act on SIGTERM; a
# plain `kill` left them running (and burning CPU) after their script had
# finished, so escalate to SIGKILL. Only ever pass PIDs this script started.
joeyStop() {
local pid
local tick
for pid in "$@"; do
[ -n "$pid" ] || continue
kill "$pid" 2>/dev/null || continue
for tick in 1 2 3 4 5 6 7 8 9 10; do
joeyAlive "$pid" || break
sleep 0.2
done
kill -9 "$pid" 2>/dev/null || true
# Reap it when it is this shell's child (a no-op otherwise), so no
# zombie outlives the script where nothing else would reap it.
wait "$pid" 2>/dev/null || true
done
}

View file

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# make-agi-iigs-disk.sh - Build the 2MB ProDOS data volume that holds the # make-agi-iigs-disk.sh - Build the 2MB ProDOS data volume that holds the
# IIgs AGI interpreter plus one AGI v2 game's resources. run-iigs.sh mounts # IIgs AGI interpreter plus one AGI v2 game's resources. The
# it as the CFFA2 card's second partition (-hard2); GS/OS boots from the # iigs-game-disk-agi make target merges it into the bootable AGI.2mg that
# first partition and auto-mounts this one. # joey-run starts (--ram 8M).
# #
# A single 800KB 3.5" floppy can't hold the ~240KB AGI binary plus KQ3's # A single 800KB 3.5" floppy can't hold the ~240KB AGI binary plus KQ3's
# ~644KB of required resources (878KB total), and MAME's IIgs 3.5 drive is # ~644KB of required resources (878KB total), and MAME's IIgs 3.5 drive is
@ -11,12 +11,13 @@
# #
# Usage: scripts/make-agi-iigs-disk.sh <output.2mg> [game-name] # Usage: scripts/make-agi-iigs-disk.sh <output.2mg> [game-name]
# game-name : subdirectory under examples/agi/gamedata/ (default kq3) # game-name : subdirectory under examples/agi/gamedata/ (default kq3)
# Requires: toolchains/env.sh sourced (LLVM816_ROOT), built build/iigs/bin/AGI. # Requires: ./install.sh install toolchain:iigs; built build/iigs/bin/AGI.
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-agi-iigs-disk.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
BINDIR="${BINDIR:-$repo/build/iigs/bin}" BINDIR="${BINDIR:-$repo/build/iigs/bin}"
out=${1:?usage: make-agi-iigs-disk.sh <output.2mg> [game]} out=${1:?usage: make-agi-iigs-disk.sh <output.2mg> [game]}

View file

@ -19,7 +19,7 @@
# Usage: scripts/make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume] # Usage: scripts/make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]
# EXAMPLE uppercase binary name as it appears on the data disk # EXAMPLE uppercase binary name as it appears on the data disk
# volume data volume name, default JOEYLIB # volume data volume name, default JOEYLIB
# Requires: toolchains/env.sh sourced. # Requires: ./install.sh install iigs (the toolchain and the GS/OS disk).
set -euo pipefail set -euo pipefail
out=${1:?usage: make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]} out=${1:?usage: make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]}
@ -27,9 +27,12 @@ prog=${2:?usage: make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]}
vol=${3:-JOEYLIB} vol=${3:-JOEYLIB}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-boot-disk.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
sysDisk="$repo/toolchains/emulators/support/gsos-system.po" joeyNeed CADIUS cadius
joeyNeed IIGS_CLANG_BUILD toolchain:iigs
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
sysDisk=$GSOS_SYSTEM_DISK
[ -x "$CADIUS" ] || { echo "make-iigs-boot-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } [ -x "$CADIUS" ] || { echo "make-iigs-boot-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
[ -f "$sysDisk" ] || { echo "make-iigs-boot-disk.sh: missing $sysDisk" >&2; exit 2; } [ -f "$sysDisk" ] || { echo "make-iigs-boot-disk.sh: missing $sysDisk" >&2; exit 2; }
@ -51,7 +54,7 @@ cp "$sysDisk" "$out"
# makes room: the stock disk has under 50KB free. # makes room: the stock disk has under 50KB free.
"$CADIUS" DELETEFILE "$out" "/$sysVol/System/Start" >/dev/null "$CADIUS" DELETEFILE "$out" "/$sysVol/System/Start" >/dev/null
"$repo/toolchains/iigs/clang-build.sh" \ "$IIGS_CLANG_BUILD" \
-DJOEY_START_PATH="\"/$vol/$prog\"" \ -DJOEY_START_PATH="\"/$vol/$prog\"" \
-o "$work/Start#B30000" \ -o "$work/Start#B30000" \
"$repo/tools/iigsStart/iigsStart.c" \ "$repo/tools/iigsStart/iigsStart.c" \

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# make-iigs-disk.sh - Build the JOEYLIB ProDOS disk image (joey.2mg) that # make-iigs-disk.sh - Build the JOEYLIB ProDOS disk image (joey.2mg) that
# run-iigs-mame.sh and run-iigs.sh mount as the data disk (flop4). It holds # the gates and joey-run mount as the data disk beside GS/OS. It holds
# the launchable example binaries as ProDOS type $B3 (S16 / GS-OS apps) in a # the launchable example binaries as ProDOS type $B3 (S16 / GS-OS apps) in a
# volume named JOEYLIB, so GS/OS Finder lists and launches them. # volume named JOEYLIB, so GS/OS Finder lists and launches them.
# #
@ -10,12 +10,13 @@
# the image. Override the set with JOEY_DISK_EXAMPLES="DRAW SPRITE ...". # the image. Override the set with JOEY_DISK_EXAMPLES="DRAW SPRITE ...".
# #
# Usage: scripts/make-iigs-disk.sh [output.2mg] # Usage: scripts/make-iigs-disk.sh [output.2mg]
# Requires: toolchains/env.sh sourced (LLVM816_ROOT), built IIgs examples. # Requires: ./install.sh install toolchain:iigs; built IIgs examples.
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
BINDIR="${BINDIR:-$repo/build/iigs/bin}" BINDIR="${BINDIR:-$repo/build/iigs/bin}"
# The NTP replayer. DEFAULTED, not optional: without it on the volume every # The NTP replayer. DEFAULTED, not optional: without it on the volume every
# IIgs example is silent except the ROM boot beep, because jlpAudioInit # IIgs example is silent except the ROM boot beep, because jlpAudioInit

View file

@ -40,7 +40,7 @@
# drive is 800KB GCR and REJECTS a 1440KB image, so anything # drive is 800KB GCR and REJECTS a 1440KB image, so anything
# larger has to boot from a CFFA2 card: # larger has to boot from a CFFA2 card:
# -sl7 cffa2 -hard1 <image>. # -sl7 cffa2 -hard1 <image>.
# Requires: toolchains/env.sh sourced. # Requires: ./install.sh install iigs (the toolchain and the GS/OS disk).
set -euo pipefail set -euo pipefail
out=${1:?usage: make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]} out=${1:?usage: make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]}
@ -48,10 +48,13 @@ prog=${2:?usage: make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]}
vol=${3:-$prog} vol=${3:-$prog}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-game-disk.sh: source toolchains/env.sh first}" source "$repo/scripts/lib/joeyEnv.sh"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
joeyNeed IIGS_CLANG_BUILD toolchain:iigs
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
data=${4:-$repo/build/iigs/bin/joey.2mg} data=${4:-$repo/build/iigs/bin/joey.2mg}
sysDisk="$repo/toolchains/emulators/support/gsos-system.po" sysDisk=$GSOS_SYSTEM_DISK
size=${JOEY_GAME_DISK_SIZE:-auto} size=${JOEY_GAME_DISK_SIZE:-auto}
[ -x "$CADIUS" ] || { echo "make-iigs-game-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } [ -x "$CADIUS" ] || { echo "make-iigs-game-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
@ -165,7 +168,7 @@ dd if="$staged" of="$work/boot.blk" bs=1 skip=$srcOff count=1024 status=none
dd if="$work/boot.blk" of="$out" bs=1 seek=$dstOff count=1024 conv=notrunc status=none dd if="$work/boot.blk" of="$out" bs=1 seek=$dstOff count=1024 conv=notrunc status=none
# ----- the launcher ----- # ----- the launcher -----
"$repo/toolchains/iigs/clang-build.sh" \ "$IIGS_CLANG_BUILD" \
-DJOEY_START_PATH="\"/$vol/$prog/$prog\"" \ -DJOEY_START_PATH="\"/$vol/$prog/$prog\"" \
-o "$work/Start#B30000" \ -o "$work/Start#B30000" \
"$repo/tools/iigsStart/iigsStart.c" \ "$repo/tools/iigsStart/iigsStart.c" \

View file

@ -23,7 +23,7 @@
# example .X base name in build/x68000/bin, default staxi # example .X base name in build/x68000/bin, default staxi
# seconds emulated seconds to run, default 240 # seconds emulated seconds to run, default 240
# skipSeconds ignore samples before this, default 130 (boot + menu) # skipSeconds ignore samples before this, default 130 (boot + menu)
# Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default toolchains/x68mame) # Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000)
# Requires: the example built WITH symbols (build/x68000/bin/<NAME>.X.elf). # Requires: the example built WITH symbols (build/x68000/bin/<NAME>.X.elf).
set -uo pipefail set -uo pipefail
@ -33,15 +33,17 @@ skip=${3:-130}
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]') PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
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 bin=$repo/build/x68000/bin/$PROG.X
elf=$bin.elf elf=$bin.elf
x68=${X68K_DIR:-$repo/toolchains/x68mame} x68=$X68K_DIR
template=$x68/HUMAN302.XDF template=$x68/HUMAN302.XDF
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH" joeyNeed X68K_ROOT toolchain:x68000
export PATH="$X68K_ROOT/bin:$PATH"
MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}"
[[ -x $MAME ]] || MAME="$(command -v mame || true)"
[[ -n ${MAME:-} && -x $MAME ]] || { echo "profile-x68000: no MAME (set MAME=...)" >&2; exit 2; }
[[ -f $bin ]] || { echo "profile-x68000: $bin not built" >&2; exit 2; } [[ -f $bin ]] || { echo "profile-x68000: $bin not built" >&2; exit 2; }
[[ -f $elf ]] || { echo "profile-x68000: $elf missing (need symbols)" >&2; exit 2; } [[ -f $elf ]] || { echo "profile-x68000: $elf missing (need symbols)" >&2; exit 2; }

View file

@ -1,277 +0,0 @@
#!/usr/bin/env bash
# Launch the AGI interpreter in the named platform's emulator with a
# staged AGI v2 game directory. The game's data files are copied flat
# alongside the binary so AGI's CWD-relative default ("./LOGDIR" etc.)
# works on platforms whose autostart can't pass argv.
#
# scripts/run-agi.sh <platform> [game] [room]
# platform : dos | amiga | atarist | iigs
# game : subdirectory under examples/agi/gamedata/ (default kq3)
# room : starting room number 1..255. Skips logic.0's hardcoded
# title-room target and jumps straight to the named room.
# Only forwarded to DOS today -- the Atari ST / Amiga
# autostart paths can't pass argv to the binary; their
# AGI starts at the title screen until we wire a stage-
# time config-file fallback or implement enough title
# opcodes for the title to advance on its own.
#
# Required files in the game directory (others are ignored):
# LOGDIR PICDIR VIEWDIR SNDDIR VOL.0 [VOL.1 ...] [OBJECT] [WORDS.TOK]
#
# IIgs delegates to run-iigs.sh's AGI mode, which packs AGI + the game
# onto a 2MB CFFA2 hard-disk volume (an 800KB floppy can't hold both) and
# boots GS/OS from a second CFFA2 partition. See make-agi-iigs-disk.sh.
set -euo pipefail
if [[ $# -lt 1 || $# -gt 3 ]]; then
echo "usage: $0 <dos|amiga|atarist|iigs> [game-name] [starting-room]" >&2
exit 2
fi
platform=$1
game=${2:-kq3}
room=${3:-0}
if ! [[ $room =~ ^[0-9]+$ ]] || (( room > 255 )); then
echo "starting-room must be 0..255 (got '$room')" >&2
exit 2
fi
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
gamedata=$repo/examples/agi/gamedata/$game
support=$repo/toolchains/emulators/support
if [[ ! -d $gamedata ]]; then
echo "$gamedata: not an AGI v2 game directory" >&2
if [[ -d $repo/examples/agi/gamedata ]]; then
echo "available games:" >&2
find "$repo/examples/agi/gamedata" -maxdepth 1 -mindepth 1 -type d -printf ' %f\n' >&2
fi
exit 1
fi
if [[ ! -f $gamedata/LOGDIR ]]; then
echo "$gamedata: no LOGDIR -- not an AGI v2 game directory" >&2
exit 1
fi
# stageGameData WORKDIR
# Copies the subset of files AGI v2 reads (DIR files, VOL.x, OBJECT,
# WORDS.TOK) into WORKDIR. Leaves Sierra's original AGI binary, OVL
# graphics drivers, and platform-specific launchers behind so a flat
# stage doesn't clash with our binary's name on case-insensitive
# filesystems (e.g. Amiga FFS: "AGI" vs our "Agi").
stageGameData() {
local destDir=$1
local dataDir=$destDir/DATA
local f
# JoeyLib forces every data-file open under DATA/ (jlDataOpen), so the
# AGI resources are staged into a DATA subdirectory of the mount root.
mkdir -p "$dataDir"
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
if [[ -f $gamedata/$f ]]; then
cp "$gamedata/$f" "$dataDir/$f"
fi
done
for f in "$gamedata"/VOL.*; do
[[ -f $f ]] && cp "$f" "$dataDir/$(basename "$f")"
done
}
runDos() {
local bin=$repo/build/dos/bin/AGI.EXE
local conf=$repo/scripts/dosbox-386sx16.conf
local work
if [[ ! -f $bin ]]; then
echo "$bin not built. Run 'make -f make/dos.mk $bin' first." >&2
exit 1
fi
# Sweep any leftover staging dirs from previous runs that
# didn't reach their trap (DOSBox window close, SIGKILL, etc.).
# We're about to make a new one anyway; freshly leftover dirs
# serve no purpose and just leak ~1 MB each.
rm -rf /tmp/joeylib-agi-dos.* 2>/dev/null || true
rm -f /tmp/joeylib-agi-dos-last.log 2>/dev/null || true
work=$(mktemp -d -t joeylib-agi-dos.XXXXXX)
# Cleanup trap: copy DJGPP's diagnostic log out (DOS uppercases
# so the file lands as JOEYLOG.TXT, not joeylog.txt), then nuke
# the staging dir. Hook EXIT plus the common interrupt signals
# so the cleanup fires even when the user closes the DOSBox
# window or Ctrl-Cs the terminal -- both bypass plain EXIT in
# some bash configurations.
cleanupDos() {
local log
for log in "$work/JOEYLOG.TXT" "$work/joeylog.txt"; do
if [[ -f $log ]]; then
cp "$log" /tmp/joeylib-agi-dos-last.log
break
fi
done
rm -rf "$work"
}
trap cleanupDos EXIT INT TERM HUP
cp "$bin" "$work/AGI.EXE"
stageGameData "$work"
# AGI.EXE reads game data from DATA/ under its CWD (we mount $work as C:
# and cd there first), so no game-directory argument is needed; the
# starting-room override is argv[1]. Skip it when room=0 so the binary
# exercises its default code path.
local agi_cmd="AGI.EXE"
if (( room != 0 )); then
agi_cmd="AGI.EXE $room"
fi
cat <<EOF
DOSBox: mounting $work as C:, running '$agi_cmd' for $game.
ESC quits the demo, then 'pause' waits for a keypress in the shell.
EOF
local dosboxArgs=(
-conf "$conf"
-set "mouse_capture=seamless"
)
# CYCLES=max (or any other DOSBox cycles value) overrides the conf's
# period-correct 386SX-16 cap. Use this to A/B test whether perf
# ceilings are coming from the simulated CPU or the code itself.
if [[ -n "${CYCLES:-}" ]]; then
dosboxArgs+=(-set "cpu cycles=$CYCLES")
fi
dosboxArgs+=(
-c "C:"
-c "$agi_cmd"
-c "pause"
--exit "$work"
)
# Avoid `exec`: we need bash to stay alive so the EXIT trap above
# actually runs and removes the staged $work dir. With `exec`,
# dosbox replaces the shell and the trap never fires, leaking
# ~1 MB of game data into /tmp on every launch.
dosbox "${dosboxArgs[@]}"
}
runAtariST() {
local bin=$repo/build/atarist/bin/AGI.PRG
local tos=$support/emutos-512k.img
local work
if [[ ! -f $bin ]]; then
echo "$bin not built. Run 'make -f make/atarist.mk $bin' first." >&2
exit 1
fi
if [[ ! -f $tos ]]; then
echo "TOS ROM missing: $tos" >&2
echo "Run ./toolchains/install.sh (EmuTOS should have been staged)." >&2
exit 1
fi
rm -rf /tmp/joeylib-agi-atarist.* 2>/dev/null || true
work=$(mktemp -d -t joeylib-agi-atarist.XXXXXX)
trap 'rm -rf "$work"' EXIT INT TERM HUP
cp "$bin" "$work/AGI.PRG"
stageGameData "$work"
cat <<EOF
Hatari: mounting $work as GEMDOS C:, autostarting AGI.PRG for $game.
EOF
# Run hatari as a child so the EXIT trap removes the staged
# game data dir; `exec` would have hatari replace bash and
# leak $work into /tmp on every launch.
hatari \
--tos "$tos" \
--harddrive "$work" \
--gemdos-drive C \
--auto "C:\\AGI.PRG"
}
runAmiga() {
local bin=$repo/build/amiga/bin/Agi
local kickstart=$support/kickstart.rom
local workbench=$support/workbench.adf
local work
if [[ ! -f $bin ]]; then
echo "$bin not built. Run 'make -f make/amiga.mk $bin' first." >&2
exit 1
fi
rm -rf /tmp/joeylib-agi-amiga.* 2>/dev/null || true
work=$(mktemp -d -t joeylib-agi-amiga.XXXXXX)
trap 'rm -rf "$work"' EXIT INT TERM HUP
mkdir -p "$work/s"
cp "$bin" "$work/Agi"
stageGameData "$work"
# ':' prefix anchors to volume root so AmigaDOS doesn't search C:.
echo ":Agi" > "$work/s/startup-sequence"
local fsargs=(
--amiga_model=A500
--fast_memory=2048
--hard_drive_0="$work"
--hard_drive_0_label=JOEYLIB
--automatic_input_grab=0
--middle_click_ungrab=1
--mouse_integration=1
--floppy_drive_speed=800
)
if [[ -f $kickstart ]]; then
fsargs+=(--kickstart_file="$kickstart")
fi
if [[ -f $workbench ]]; then
# If a Workbench floppy is staged it boots from DF0: instead of
# the HD, so the startup-sequence never fires. Skip Workbench
# for AGI to keep the launch one-shot; user can still launch
# manually with run-amiga.sh if they want the GUI.
echo "(skipping workbench.adf so the JOEYLIB HD boots Agi directly)"
fi
cat <<EOF
FS-UAE booting JOEYLIB hard drive ($work) and auto-running Agi for $game.
EOF
fs-uae "${fsargs[@]}"
}
runIigs() {
# IIgs uses a CFFA2 hard disk (the 800KB floppy can't hold AGI + KQ3),
# driven entirely by run-iigs.sh's AGI mode. Delegate to it: it builds
# the two-partition CFFA image via make-agi-iigs-disk.sh, boots GS/OS,
# and launches AGI. The starting-room override is DOS-only (the IIgs
# autostart path can't pass argv), so $room is not forwarded here.
if (( room != 0 )); then
echo "note: starting-room override is DOS-only; ignored on IIgs" >&2
fi
# The IIgs build target is the binary path, not EXAMPLE=agi.
if [[ ! -f "$repo/build/iigs/bin/AGI" ]]; then
echo "$repo/build/iigs/bin/AGI not built." >&2
echo "Build it with: make -f make/iigs.mk $repo/build/iigs/bin/AGI" >&2
exit 1
fi
JOEY_AGI_GAME="$game" exec "$repo/scripts/run-iigs.sh" agi
}
case "$platform" in
dos) runDos ;;
atarist) runAtariST ;;
amiga) runAmiga ;;
iigs) runIigs ;;
*)
echo "$platform: unknown platform (expected dos, amiga, atarist, or iigs)" >&2
exit 2
;;
esac

View file

@ -1,107 +0,0 @@
#!/usr/bin/env bash
# run-amiga-headless.sh - Run one built Amiga example under FS-UAE with no
# window, wait, then hand back the example's joeylog.txt and a screenshot.
#
# This is the smoke test bench-amiga.sh cannot be: that script runs UBER
# and waits for its "press any key to exit" line, but a game never exits.
# Here the run is simply given N seconds and whatever it logged and drew
# is reported.
#
# The Amiga's joeylog.txt lands in the directory mounted as the hard
# drive, so it can be read straight off the host while the machine runs.
#
# Usage: scripts/run-amiga-headless.sh [example] [seconds] [fastKB]
# example binary name in build/amiga/bin, default Taxi
# seconds how long to let it run, default 90
# fastKB fast memory in KB, default 2048. Pass 0 for a
# genuinely bare 512 KB-chip A500 (slow memory off
# as well), which is the tight case.
# Requires: fs-uae, Xvfb, toolchains/emulators/support/kickstart.rom.
set -euo pipefail
prog=${1:-Taxi}
seconds=${2:-90}
fastKb=${3:-2048}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
binDir=$repo/build/amiga/bin
support=$repo/toolchains/emulators/support
kickstart=$support/kickstart.rom
outDir=${AMIGA_OUT_DIR:-$repo/build/amiga/headless}
# Most examples are a bare binary in bin/; Space Taxi ships as its own
# drawer holding the binary and its DATA tree, so accept either shape.
if [ -d "$binDir/$prog" ]; then
progDir=$binDir/$prog
[ -f "$progDir/$prog" ] || { echo "run-amiga-headless: $progDir/$prog not built" >&2; exit 2; }
elif [ -f "$binDir/$prog" ]; then
progDir=""
else
echo "run-amiga-headless: $binDir/$prog not built" >&2
exit 2
fi
[ -f "$kickstart" ] || { echo "run-amiga-headless: missing $kickstart" >&2; exit 2; }
work=$(mktemp -d -t joeylib-amiga-run.XXXXXX)
mkdir -p "$work/s" "$outDir"
if [ -n "$progDir" ]; then
cp -r "$progDir/." "$work/"
else
find "$binDir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
[ -d "$binDir/DATA" ] && cp -r "$binDir/DATA" "$work/"
fi
printf 'stack 32768\n:%s\n' "$prog" > "$work/s/startup-sequence"
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfbPid=$!
sleep 1
fsArgs=(
--amiga_model=A500
--hard_drive_0="$work"
--hard_drive_0_label=JOEYLIB
--kickstart_file="$kickstart"
--automatic_input_grab=0
--fullscreen=0
--fade_out_duration=0
)
# Always pass the memory explicitly. Leaving --fast_memory off does NOT
# give a bare machine: FS-UAE's A500 default still fits the 512 KB
# trapdoor expansion, which AvailMem reports as ~300 KB of FAST. A real
# unexpanded A500 is 512 KB of CHIP and nothing else, so fastKB=0 has to
# turn the slow memory off too.
fsArgs+=(--fast_memory="$fastKb")
[ "$fastKb" = "0" ] && fsArgs+=(--slow_memory=0)
DISPLAY=$display fs-uae "${fsArgs[@]}" > "$work/fsuae.log" 2>&1 &
fsPid=$!
cleanup() {
kill "$fsPid" 2>/dev/null || true
kill "$xvfbPid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT
echo "run-amiga-headless: $prog for ${seconds}s on an A500 with ${fastKb} KB fast..." >&2
elapsed=0
while [ "$elapsed" -lt "$seconds" ]; do
sleep 5
elapsed=$((elapsed + 5))
kill -0 "$fsPid" 2>/dev/null || { echo "run-amiga-headless: FS-UAE exited early" >&2; break; }
done
DISPLAY=$display import -window root "$outDir/$prog.png" 2>/dev/null || true
cp "$work/joeylog.txt" "$outDir/$prog-joeylog.txt" 2>/dev/null || true
cp "$work/fsuae.log" "$outDir/$prog-fsuae.log" 2>/dev/null || true
cleanup
trap - EXIT
rm -rf "$work"
echo "run-amiga-headless: wrote $outDir/$prog.png"
if [ -f "$outDir/$prog-joeylog.txt" ]; then
echo "--- joeylog.txt (last 25 lines)"
tail -25 "$outDir/$prog-joeylog.txt"
else
echo "run-amiga-headless: NO joeylog.txt -- the example never logged anything" >&2
fi

View file

@ -1,159 +0,0 @@
#!/usr/bin/env bash
# Launch the built Amiga example in FS-UAE. Defaults to PATTERN.
#
# Kickstart and Workbench:
# - If toolchains/emulators/support/kickstart.rom is present, it is
# used as the Kickstart ROM. Otherwise FS-UAE's built-in AROS ROM
# is used (less compatible with OCS Intuition but requires no
# licensed files).
# - If toolchains/emulators/support/workbench.adf is present, it is
# inserted into DF0: and the Amiga boots Workbench. The user
# launches the example from the JOEYLIB drawer on the desktop.
# - If Workbench is not present, AmigaDOS boots directly from DH0:,
# executes s/startup-sequence, and auto-runs the requested example.
#
# Always on A500 hardware (OCS chipset). Our HAL only uses OCS
# features so this matches real-hardware behavior.
#
# scripts/run-amiga.sh # runs Pattern
# scripts/run-amiga.sh hello # runs Hello
# scripts/run-amiga.sh draw # runs Draw
# scripts/run-amiga.sh staxi # runs Taxi (Space Taxi)
#
# Argument is any built example name (case-insensitive); the script
# normalizes it to PascalCase (first letter upper, rest lower) which
# is the JoeyLib convention for Amiga binary filenames. Special
# aliases: "staxi" / "spacetaxi" -> "Taxi" (the Amiga binary name).
set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/amiga/bin
support=$repo/toolchains/emulators/support
prog_lower=${prog,,}
# Project-name aliases (input -> Amiga binary name). On Amiga the
# Space Taxi binary is "Taxi", not "Staxi" or "Spacetaxi".
case "$prog_lower" in
staxi|spacetaxi) prog_lower=taxi ;;
esac
file=${prog_lower^}
# An example may live either directly in bin_dir (Pattern, Hello, etc.)
# or inside its own subdir alongside a per-example DATA/ tree (Taxi).
# Prefer the subdir layout if present.
if [[ -f "$bin_dir/$file/$file" ]]; then
bin_path=$bin_dir/$file/$file
bin_extra_dir=$bin_dir/$file
elif [[ -f "$bin_dir/$file" ]]; then
bin_path=$bin_dir/$file
bin_extra_dir=
else
echo "$file not built. Run 'make amiga' first." >&2
echo " looked for:" >&2
echo " $bin_dir/$file/$file" >&2
echo " $bin_dir/$file" >&2
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -executable -printf '%f\n' >&2 2>/dev/null || true
find "$bin_dir" -maxdepth 2 -type f -executable -printf '%P\n' >&2 2>/dev/null \
| grep '/' >&2 || true
exit 1
fi
kickstart=$support/kickstart.rom
workbench=$support/workbench.adf
# Stage a fresh hard-drive directory containing both example binaries
# plus an s/startup-sequence that auto-invokes the chosen one. The
# startup-sequence only runs when booting directly from the hard
# drive (i.e. no Workbench floppy); Workbench takes over boot if
# present and the user launches manually.
work=$(mktemp -d -t joeylib-amiga.XXXXXX)
# Preserve any diagnostic dumps Pattern writes to the virtual HD
# (copper.txt, etc.) before the temp dir is removed on script exit.
# Also stage UBER's joeylog.txt back to build/amiga/bin/ so the
# tools/uber-perf-table picks it up at the standard path.
dump_keep=/tmp/joeylib-amiga-dump
trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; cp "$work"/joeylog.txt "$bin_dir/joeylog.txt" 2>/dev/null; rm -rf "$work"' EXIT
mkdir -p "$work/s"
# Stage every built top-level binary (executable file at top of bin_dir,
# no extension on Amiga). DATA/ is copied separately below.
find "$bin_dir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
# Stage the top-level DATA folder (test.mod, test.sfx) the audio demo
# loads from the boot volume at runtime.
if [[ -d "$bin_dir/DATA" ]]; then
cp -r "$bin_dir/DATA" "$work/"
fi
# If the chosen example lives in its own subdir (Taxi/), stage the
# binary AND its sibling DATA tree -- the subdir's DATA replaces the
# top-level one if there's a conflict. We rsync-merge so existing
# entries (e.g. test.mod) survive alongside Taxi's font.jas, levels/,
# sprites/, tiles/.
if [[ -n "$bin_extra_dir" ]]; then
cp "$bin_path" "$work/$file"
if [[ -d "$bin_extra_dir/DATA" ]]; then
mkdir -p "$work/DATA"
cp -r "$bin_extra_dir/DATA/." "$work/DATA/"
fi
fi
# ':' prefix anchors to the root of the current volume; otherwise
# AmigaDOS looks in C: and the command is not found.
echo ":$file" > "$work/s/startup-sequence"
fsargs=(
--amiga_model=A500
--fast_memory=2048
--hard_drive_0="$work"
--hard_drive_0_label=JOEYLIB
# Mouse-handling overrides for VM hosts where FS-UAE's default
# relative-mode grab fights with the hypervisor's own pointer
# integration. automatic_input_grab=0 stops FS-UAE from grabbing
# the pointer on focus; middle_click_ungrab=1 is a fallback so a
# middle-click releases any grab that does occur; mouse_integration=1
# asks FS-UAE to track host pointer position directly instead of
# sampling relative deltas.
--automatic_input_grab=0
--middle_click_ungrab=1
--mouse_integration=1
# Speed overrides: floppy_drive_speed=800 runs the floppy at 8x so
# the Workbench boot finishes in a few seconds instead of a minute.
# accuracy=0 and fast_copper=1 are deliberately NOT set -- they
# compromise copper timing, which our per-scanline palette swaps
# depend on.
--floppy_drive_speed=800
)
if [[ -f $kickstart ]]; then
fsargs+=(--kickstart_file="$kickstart")
fi
if [[ -f $workbench ]]; then
fsargs+=(--floppy_drive_0="$workbench")
cat <<EOF
FS-UAE booting Workbench from DF0:. Once the desktop appears:
1. Double-click the JOEYLIB disk icon.
2. From the Window menu, pick "Show > All Files" (Workbench hides
files that have no .info icon by default).
3. Double-click $file to run it.
Alternatively open a Shell (Workbench > Tools > Shell), type 'cd
JOEYLIB:' then '$file'.
EOF
else
cat <<EOF
FS-UAE booting directly from the JOEYLIB hard drive. $file will
auto-run via s/startup-sequence. Drop a Workbench 3.1 ADF at
$workbench to boot the GUI instead.
EOF
fi
# Intentionally no exec: we need the bash EXIT trap to fire so the
# scratch dir gets cleaned up (and any diagnostic dumps preserved).
# exec would replace the shell and skip trap execution.
fs-uae "${fsargs[@]}"

View file

@ -1,83 +0,0 @@
#!/usr/bin/env bash
# run-atarist-headless.sh - Run one built Atari ST example under Hatari
# with no window, wait, then hand back its joeylog.txt and a screenshot.
#
# The ST twin of run-amiga-headless.sh, and for the same reason:
# bench-atarist.sh runs UBER and waits for its "press any key to exit"
# line, which a game never prints.
#
# Hatari mounts the program's directory as GEMDOS drive C, so the app's
# joeylog.txt lands there and can be read off the host while it runs.
#
# Usage: scripts/run-atarist-headless.sh [example] [seconds]
# example PRG base name in build/atarist/bin, default STAXI
# seconds how long to let it run, default 90
# Env: ST_SOUND=on keep the YM running (default off, it is faster)
# ST_MEMSIZE=n machine RAM in MB, passed to Hatari. Default is
# Hatari's own; 1 is a stock 1040 ST.
# Requires: hatari, Xvfb, toolchains/emulators/support/emutos-512k.img
set -euo pipefail
prog=${1:-STAXI}
seconds=${2:-90}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
binDir=$repo/build/atarist/bin
tos=$repo/toolchains/emulators/support/emutos-512k.img
outDir=${ST_OUT_DIR:-$repo/build/atarist/headless}
# Most examples are a bare PRG in bin/; Space Taxi ships as its own
# folder holding the PRG and its DATA tree.
if [ -d "$binDir/$prog" ]; then
driveDir=$binDir/$prog
else
driveDir=$binDir
fi
[ -f "$driveDir/$prog.PRG" ] || { echo "run-atarist-headless: $driveDir/$prog.PRG not built" >&2; exit 2; }
[ -f "$tos" ] || { echo "run-atarist-headless: missing $tos" >&2; exit 2; }
mkdir -p "$outDir"
rm -f "$driveDir/joeylog.txt"
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfbPid=$!
sleep 1
DISPLAY=$display hatari \
--tos "$tos" \
--harddrive "$driveDir" \
--gemdos-drive C \
--drive-a off --drive-b off \
--sound "${ST_SOUND:-off}" \
${ST_MEMSIZE:+--memsize "$ST_MEMSIZE"} \
--fast-forward on \
--auto "C:\\$prog.PRG" > "$outDir/$prog-hatari.log" 2>&1 &
hatPid=$!
cleanup() {
kill "$hatPid" 2>/dev/null || true
kill "$xvfbPid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT
echo "run-atarist-headless: $prog for ${seconds}s..." >&2
elapsed=0
while [ "$elapsed" -lt "$seconds" ]; do
sleep 5
elapsed=$((elapsed + 5))
kill -0 "$hatPid" 2>/dev/null || { echo "run-atarist-headless: Hatari exited early" >&2; break; }
done
DISPLAY=$display import -window root "$outDir/$prog.png" 2>/dev/null || true
cp "$driveDir/joeylog.txt" "$outDir/$prog-joeylog.txt" 2>/dev/null || true
cleanup
trap - EXIT
echo "run-atarist-headless: wrote $outDir/$prog.png"
if [ -f "$outDir/$prog-joeylog.txt" ]; then
echo "--- joeylog.txt (last 25 lines)"
tail -25 "$outDir/$prog-joeylog.txt"
else
echo "run-atarist-headless: NO joeylog.txt -- the example never logged anything" >&2
fi

View file

@ -1,69 +0,0 @@
#!/usr/bin/env bash
# Launch the built Atari ST example in Hatari. Defaults to PATTERN.
# Hatari autostarts the .PRG via the --auto flag; EmuTOS
# (toolchains/emulators/support/emutos-512k.img) provides the TOS ROM
# since the Ubuntu hatari package does not bundle one.
#
# scripts/run-atarist.sh # runs PATTERN.PRG
# scripts/run-atarist.sh hello # runs HELLO.PRG
# scripts/run-atarist.sh draw # runs DRAW.PRG
# scripts/run-atarist.sh staxi # runs STAXI/STAXI.PRG (Space Taxi)
#
# Argument is any built example name (case-insensitive); the script
# upper-cases it and appends .PRG, then checks the file exists.
# Aliases: "spacetaxi" -> "STAXI" (the canonical install dir name).
set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/atarist/bin
# Name aliases (input -> install/binary base name). On Atari ST the
# Space Taxi install dir + binary are "STAXI".
prog_upper=${prog^^}
case "$prog_upper" in
SPACETAXI) prog_upper=STAXI ;;
esac
file=$prog_upper.PRG
tos=$repo/toolchains/emulators/support/emutos-512k.img
# Examples may live directly in bin_dir (PATTERN.PRG, HELLO.PRG, etc.)
# or in their own subdir alongside a per-example DATA/ tree
# (STAXI/STAXI.PRG). Prefer the subdir layout if present so STAXI
# auto-loads from its own asset bundle.
if [[ -f "$bin_dir/$prog_upper/$file" ]]; then
auto_path="C:\\$prog_upper\\$file"
elif [[ -f "$bin_dir/$file" ]]; then
auto_path="C:\\$file"
else
echo "$file not built. Run 'make atarist' first." >&2
echo " looked for:" >&2
echo " $bin_dir/$prog_upper/$file" >&2
echo " $bin_dir/$file" >&2
if compgen -G "$bin_dir/*.PRG" > /dev/null; then
echo "available examples in $bin_dir:" >&2
ls "$bin_dir"/*.PRG | xargs -n1 basename >&2
fi
find "$bin_dir" -maxdepth 2 -name '*.PRG' -printf ' %P\n' >&2 2>/dev/null \
| grep '/' >&2 || true
exit 1
fi
if [[ ! -f $tos ]]; then
echo "TOS ROM missing: $tos" >&2
echo "run ./toolchains/install.sh (EmuTOS should have been staged)." >&2
exit 1
fi
# Hatari's --auto needs the target on a mounted GEMDOS drive.
exec hatari \
--tos "$tos" \
--harddrive "$bin_dir" \
--gemdos-drive C \
--auto "$auto_path"

View file

@ -1,75 +0,0 @@
#!/usr/bin/env bash
# Launch the built DOS example in DOSBox. Defaults to PATTERN.
#
# scripts/run-dos.sh # runs PATTERN
# scripts/run-dos.sh hello # runs HELLO
# scripts/run-dos.sh draw # runs DRAW
#
# Argument is any built example name (case-insensitive); the script
# upper-cases it and appends .EXE, then checks the file exists.
set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
file=${prog^^}.EXE
appdir=${prog^^}
# Two layouts supported:
# 1. New: bin/<APP>/<APP>.EXE (with DATA/ alongside it)
# DOSBox mounts bin/<APP> as C: so DATA/foo.bin is reachable
# via the relative path the binary opens.
# 2. Old: bin/<APP>.EXE (no DATA, or DATA at bin/DATA)
# DOSBox mounts bin/ as C: as before.
if [[ -f "$bin_dir/$appdir/$file" ]]; then
mount_dir="$bin_dir/$appdir"
elif [[ -f "$bin_dir/$file" ]]; then
mount_dir="$bin_dir"
else
echo "Neither $bin_dir/$appdir/$file nor $bin_dir/$file built." >&2
echo "Run 'make dos' first." >&2
if compgen -G "$bin_dir/*.EXE" > /dev/null; then
echo "available top-level binaries:" >&2
ls "$bin_dir"/*.EXE 2>/dev/null | xargs -n1 basename >&2
fi
if compgen -G "$bin_dir/*/*.EXE" > /dev/null; then
echo "available app-dir binaries:" >&2
ls "$bin_dir"/*/*.EXE 2>/dev/null | xargs -n1 -I{} sh -c 'dirname {} | xargs basename'
fi
exit 1
fi
# mouse_capture=seamless is required for DOSBox-Staging when running
# inside a VM whose host already manages the pointer; without it the
# default capture-on-click behavior fights the VM's grab and mouse
# input is unusable. On plain DOSBox this -set flag is unknown and is
# logged once as a warning, then ignored -- harmless either way.
#
# -conf $conf locks the CPU to a simulated 386SX-16 (the slowest
# realistic 386 desktop). DOSBox layers configs: anything not set in
# our file falls back to the user's main dosbox.conf.
dosboxArgs=(
-conf "$conf"
-set "mouse_capture=seamless"
)
# CYCLES=max (or any other DOSBox cycles value, e.g. CYCLES=50000)
# overrides the conf's period-correct 386SX-16 cap. Use this to
# A/B test whether perf ceilings are coming from the simulated CPU
# or the code itself. Same pattern as run-agi.sh.
if [[ -n "${CYCLES:-}" ]]; then
dosboxArgs+=(-set "cpu cycles=$CYCLES")
fi
dosboxArgs+=(
-c "C:"
-c "$file"
-c "pause"
--exit "$mount_dir"
)
exec dosbox "${dosboxArgs[@]}"

View file

@ -1,302 +0,0 @@
#!/usr/bin/env bash
# Launch the IIgs sprite demo (or other example) under MAME's apple2gs
# driver instead of GSplus. We get:
# - The MAME debugger window (always visible with -debug)
# - debug.log file written to the working directory
# - A Lua hook that, when the CPU halts (e.g., BRK), dumps registers
# and the surrounding code bytes to /tmp/mame-iigs/crash.txt
#
# The boot path is the same as run-iigs.sh: gsos-system.po as flop3, our
# joey.2mg as flop4. Once Finder is up, navigate to JOEYLIB and double-
# click the example.
#
# Outputs of a run land in /tmp/mame-iigs/:
# - debug.log MAME's debugger console output (-debuglog)
# - crash.txt Lua-hook crash dump (if anything halts the CPU)
set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/iigs/bin
target=${prog^^}
if [[ ! -f "$bin_dir/$target" ]]; then
echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
if compgen -G "$bin_dir/*" > /dev/null; then
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -printf '%f\n' \
| grep -vE '\.2mg$|\.txt$' >&2 || true
fi
exit 1
fi
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$bin_dir/joey.2mg
for f in "$sys_disk" "$data_disk"; do
if [[ ! -f $f ]]; then
echo "missing: $f" >&2
exit 1
fi
done
work=$(mktemp -d -t joeylib-mame.XXXXXX)
out=/tmp/mame-iigs
mkdir -p "$out"
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Lua script: drives Finder via natural keyboard + macadb key fields
# to launch the requested example, dumps register state on any halt
# (BRK, breakpoint, watchpoint, manual stop). Field names for keys
# come from MAME's apple2gs macadb input definitions:
# :macadb:KEY0 -> "d D"
# :macadb:KEY1 -> "o O"
# :macadb:KEY2 -> "j J", "p P"
# :macadb:KEY3 -> "Command / Open Apple"
# Letters not on KEY0..2 fall back to natkeyboard:post() (which
# handles modifier-less character entry only).
# Type the FULL program name to disambiguate (DRAW vs DATA which both
# start with D and live in the same JOEYLIB volume).
prog_select_str=${target}
cat > "$work/crash-hook.lua" <<LUA
-- Crash diagnostics + Finder driver for IIgs demos. Auto-resumes
-- the initial debug pause; at calibrated frame counts taps "J" to
-- select JOEYLIB, Cmd-O to open it, the program first letter to
-- select the binary, Cmd-O to launch it. On any halt (BRK trap,
-- watchpoint, breakpoint) dumps registers + bytes around PC to
-- crash.txt.
local cpu = manager.machine.devices[":maincpu"]
local prog = cpu.spaces["program"]
local outpath = "/tmp/mame-iigs/crash.txt"
local function dump(label)
local f = io.open(outpath, "a")
if f == nil then return end
f:write(string.format("=== %s @ %s ===\n", label, os.date("%H:%M:%S")))
for k, v in pairs(cpu.state) do
f:write(string.format(" %s = %X\n", k, v.value))
end
local pc = cpu.state["CURPC"].value
local lo = (pc - 16) & 0xFFFFFF
f:write(string.format(" bytes %06X..%06X:", lo, lo + 32))
for i = 0, 32 do
local b = prog:read_u8(lo + i)
f:write(string.format(" %02X", b))
end
f:write("\n")
-- Probe addresses from joeyDraw.asm
f:write(string.format(" probe 012050=%02X 011F80=%02X 023000=%02X\n",
prog:read_u8(0x012050),
prog:read_u8(0x011F80),
prog:read_u8(0x023000)))
f:close()
end
local done_marker = "/tmp/mame-iigs/.done"
local started = false
local crashed = false
local boot_frames = 0
local function signal_done(reason)
local f = io.open(done_marker, "w")
if f ~= nil then
f:write(reason)
f:close()
end
end
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 log_step(label)
local f = io.open("/tmp/mame-iigs/steps.txt", "a")
if f ~= nil then
f:write(string.format("frame=%d %s\n", boot_frames, label))
f:close()
end
end
local function snap(label)
log_step("snap " .. label)
manager.machine.video:snapshot()
end
-- Finder navigation: natkeyboard handles per-letter scancode/timing
-- correctly; the Cmd modifier is forced via the macadb field for the
-- duration of the post() call. Mirrors the proven approach in the
-- pre-existing /tmp/auto_run.lua.
local steps = {
{ 2700, function() snap("preboot") end },
{ 3000, function() snap("at_3000_finder") log_step("post J") nat:post("J") end },
{ 3120, function() snap("after_J") log_step("Cmd hold") press(key_cmd) end },
{ 3126, function() log_step("post o under Cmd") nat:post("o") end },
{ 3180, function() log_step("Cmd release") release(key_cmd) end },
{ 3300, function() snap("after_Cmd_O") end },
{ 3540, function() log_step("post name") nat:post("${prog_select_str}") end },
{ 3600, function() snap("after_first") end },
{ 3660, function() log_step("Cmd hold #2") press(key_cmd) end },
{ 3666, function() log_step("post o #2") nat:post("o") end },
{ 3720, function() log_step("Cmd release #2") release(key_cmd) end },
{ 3900, function() snap("after_launch") end },
{ 6000, function() snap("running") end },
{ 9000, function() snap("running_2") end },
{ 12000, function() snap("running_3") end },
{ 13500, function() snap("running_4") end },
{ 14500, function() snap("running_5") end },
{ 15500, function() snap("running_6") end },
{ 16500, function() snap("running_7") end },
}
local step_idx = 1
-- Probe-address poll. Watch 3 addresses and log every change, so we
-- can verify which (if any) is reachable by our long-mode STAs.
local last1, last2, last3, last4, last5, last6, last7 = -1, -1, -1, -1, -1, -1, -1
local function check_ckpt()
local v1 = prog:read_u8(0x012050)
local v2 = prog:read_u8(0x011F80)
local v3 = prog:read_u8(0x023000)
local v4 = prog:read_u8(0xE12050) -- SHR pixel byte (mirror of $012050 once blitted)
local v5 = prog:read_u8(0xE19D00) -- SCB row 0 (0 = 320 mode)
local v6 = prog:read_u8(0xE15000) -- SHR pixel byte mid-screen (row ~38, col ~64)
local v7 = prog:read_u8(0xE19E00) -- palette[0][0] low byte (color 0 lo)
if v1 ~= last1 or v2 ~= last2 or v3 ~= last3 or v4 ~= last4 or v5 ~= last5 or v6 ~= last6 or v7 ~= last7 then
local f = io.open("/tmp/mame-iigs/ckpt-trace.txt", "a")
if f ~= nil then
f:write(string.format("frame=%d 012050=%02X 011F80=%02X 023000=%02X E12050=%02X E19D00=%02X E15000=%02X E19E00=%02X\n",
boot_frames, v1, v2, v3, v4, v5, v6, v7))
f:close()
end
last1, last2, last3, last4, last5, last6, last7 = v1, v2, v3, v4, v5, v6, v7
end
end
emu.register_periodic(function()
local dbg = manager.machine.debugger
if dbg == nil then return end
if dbg.execution_state == "stop" then
if not started then
started = true
dbg.execution_state = "run"
return
end
if not crashed then
crashed = true
dump("halt")
signal_done("halt")
end
else
boot_frames = boot_frames + 1
check_ckpt()
while step_idx <= #steps and boot_frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
if boot_frames > 18000 and not crashed then
crashed = true
dump("watchdog")
signal_done("watchdog")
end
end
end)
LUA
# Wipe prior crash.txt so we don't confuse runs.
: > "$out/crash.txt"
cat <<EOF
MAME apple2gs (auto-launch ${prog^^}):
Boot disk: $work/boot.po (flop3)
Boots GS/OS, waits ~50s for Finder, then drives the keyboard via Lua
to select the JOEYLIB volume and double-launch ${prog^^}.
On crash the MAME debugger halts and Lua dumps state to:
$out/crash.txt
After exit:
$out/debug.log MAME debugger console output
EOF
cleanup() {
# Always rescue debug.log first -- it's written by MAME relative to
# cwd ($work) and lost when the dir is removed.
if [[ -f $work/debug.log ]]; then
mv -f "$work/debug.log" "$out/debug.log"
fi
# Snapshots that the Lua hook captured for visual debugging.
if [[ -d $work/snap ]]; then
rm -rf "$out/snap"
mv "$work/snap" "$out/snap"
fi
rm -rf "$work"
}
trap cleanup EXIT
# Visible by default. Set MAME_HEADLESS=1 to suppress the video window
# (CI / batch runs that only care about crash.txt / snapshots). The crash
# hook drives MAME's debugger, which needs a Qt platform; headless boxes
# have no X display, so force Qt's offscreen plugin and SDL's dummy
# video/audio drivers so MAME never tries to open a window.
video_arg="-window"
if [[ "${MAME_HEADLESS:-0}" = "1" ]]; then
# Use the software renderer (not "-video none"): MAME's video:snapshot()
# needs a real render target, so "-video none" silently drops snapshots.
# "-video soft" renders to an offscreen surface that snapshot() can read.
video_arg="-video soft"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
fi
# apple2gs ROM set. Default to the user's MAME roms; override via MAME_ROMPATH.
# (A bare `mame apple2gs` only finds ROMs if rompath is already configured;
# passing it explicitly keeps this script self-contained.)
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
# Clear the done-marker the Lua hook uses to signal shutdown.
rm -f "$out/.done"
cd "$work"
mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" \
-flop4 "$work/joey.2mg" \
$video_arg -sound none \
-nothrottle \
-debug -debuglog \
-autoboot_script "$work/crash-hook.lua" &
mame_pid=$!
# Poll for the done-marker. Kill MAME once Lua signals it. Cap total
# wall-clock at 6 min so we don't outlive the Lua-side watchdog (5 min).
deadline=$((SECONDS + 360))
while kill -0 "$mame_pid" 2>/dev/null; do
if [[ -f $out/.done ]]; then
kill "$mame_pid" 2>/dev/null
break
fi
if (( SECONDS > deadline )); then
kill "$mame_pid" 2>/dev/null
break
fi
sleep 0.5
done
wait "$mame_pid" 2>/dev/null || true

View file

@ -1,610 +0,0 @@
#!/usr/bin/env bash
# Launch the built Apple IIgs examples.
#
# scripts/run-iigs.sh GSplus + Finder: boots GS/OS with
# joey.2mg mounted; pick the demo by
# double-clicking it in Finder.
# scripts/run-iigs.sh staxi MAME auto-launch: builds a
# single-example run disk
# (joey-run.2mg -- joey.2mg is left
# alone so benches stay honest) plus a
# Finder-less boot disk whose
# /System/Start IS the launcher, so
# the example is on screen in ~50 s
# with no Finder and no keystroke
# driving. No clicking needed.
# scripts/run-iigs.sh --all MAME + every example on one 16 MB
# CFFA2 hard disk (the full set does
# not fit an 800KB floppy): boots
# GS/OS with the JOEYLIB volume so you
# pick a demo in Finder.
# scripts/run-iigs.sh --all staxi same disk, auto-launches the named
# example via the Lua Finder driver.
#
# The MAME path needs the apple2gs ROM set (default ~/.mame/roms,
# override with MAME_ROMPATH). MAME_HEADLESS=1 runs it windowless with
# periodic PNG snapshots (batch verification). --all also needs the
# CFFA2 firmware (a2cffa2/cffa20eec02.bin), like the AGI path.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/iigs/bin
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
# MAME: an installed one wins, otherwise the apple2gs build this repo
# already carries in toolchains/cache. Resolving it here means a shell
# whose PATH predates ~/.local/bin (a symlink created after login) does
# not fail with a bare "mame: command not found" three hundred lines in.
MAME="${MAME:-$(command -v mame || true)}"
if [[ -z $MAME ]]; then
MAME=$repo/toolchains/cache/mame-mame0264/apple2gs
fi
if [[ ! -x $MAME ]]; then
echo "run-iigs: no MAME found (set MAME=/path/to/mame, or apt install mame)" >&2
exit 2
fi
# --all boots the full example set off a CFFA2 hard-disk image; an
# optional example name after it auto-launches that one.
all_mode=0
if [[ "${1:-}" == "--all" ]]; then
all_mode=1
shift
fi
if [[ $# -gt 1 ]]; then
echo "usage: $0 [--all] [example-name]" >&2
exit 2
fi
# ------------------------------------------------------------ --all (CFFA2)
if [[ $all_mode -eq 1 ]]; then
all_disk=$bin_dir/joey-all.2mg
target=""
if [[ $# -eq 1 ]]; then
target=${1^^}
if [[ ! -f "$bin_dir/$target" ]]; then
echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
exit 1
fi
fi
if [[ ! -f $sys_disk ]]; then
echo "missing: $sys_disk (run ./toolchains/install.sh)" >&2
exit 1
fi
# make-iigs-disk.sh needs the toolchain env (cadius path).
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
# (Re)build the all-examples image when it is missing or older than any
# example binary. The full launchable set, sized to a 16 MB ProDOS
# volume (an 800KB floppy can't hold it).
all_examples="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI"
need_disk=0
if [[ ! -f $all_disk ]]; then
need_disk=1
else
for name in $all_examples; do
if [[ -f "$bin_dir/$name" && "$bin_dir/$name" -nt $all_disk ]]; then
need_disk=1
break
fi
done
fi
if [[ $need_disk -eq 1 ]]; then
echo "run-iigs: building all-examples disk ($all_disk)..."
BINDIR="$bin_dir" \
JOEY_DISK_SIZE="16MB" JOEY_DISK_EXAMPLES="$all_examples" \
"$repo/scripts/make-iigs-disk.sh" "$all_disk"
else
echo "run-iigs: all-examples disk is current ($all_disk)"
fi
# CFFA2 firmware (same resolution as the AGI path): prefer the repo-
# staged copy, fall back to the user's rompath.
staged_roms=$repo/toolchains/emulators/support/mame-roms
user_roms="${MAME_ROMPATH:-$HOME/.mame/roms}"
rompath="$user_roms"
if [[ -f "$staged_roms/a2cffa2/cffa20eec02.bin" ]]; then
rompath="$user_roms;$staged_roms"
elif [[ ! -f "$user_roms/a2cffa2/cffa20eec02.bin" ]]; then
cat >&2 <<EOF
missing CFFA2 firmware: a2cffa2/cffa20eec02.bin
Needed to mount the all-examples hard disk. It ships in dreher.net's
Run6_CDROM.zip as Firmware/V2.0/CFFA20EEC02.BIN. Drop it in either
$staged_roms/a2cffa2/ or $user_roms/a2cffa2/
EOF
exit 1
fi
headless=${MAME_HEADLESS:-0}
work=$(mktemp -d -t joeylib-run-all.XXXXXX)
mame_pid=""
cleanup() {
if [[ -n $mame_pid ]] && kill -0 "$mame_pid" 2>/dev/null; then
kill "$mame_pid" 2>/dev/null || true
sleep 1
kill -9 "$mame_pid" 2>/dev/null || true
fi
rm -rf "$work"
}
trap cleanup EXIT INT TERM
# hard1: writable bootable GS/OS. hard2: the all-examples volume.
cp "$sys_disk" "$work/boot.po"
cp "$all_disk" "$work/joey-all.2mg"
# Finder driver: only when a target is named (else the user picks a
# demo). Type J (select the JOEYLIB volume), Cmd-O (open), the full
# example name, Cmd-O (launch) -- the same steps the floppy path uses.
cat > "$work/launch.lua" <<LUA
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 headless = ($headless == 1)
local target = "$target"
local frames = 0
local steps = {}
if target ~= "" then
steps = {
{ 3000, function() nat:post("J") end },
{ 3120, function() press(key_cmd) end },
{ 3126, function() nat:post("o") end },
{ 3180, function() release(key_cmd) end },
{ 3540, function() nat:post(target) end },
{ 3660, function() press(key_cmd) end },
{ 3666, function() nat:post("o") end },
{ 3720, function() release(key_cmd) end },
}
end
if headless then
table.insert(steps, { 4200, function() manager.machine.video:snapshot() end })
table.insert(steps, { 6600, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9000, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9100, function() manager.machine:exit() end })
end
table.sort(steps, function(a, b) return a[1] < b[1] end)
local step_idx = 1
emu.register_periodic(function()
frames = frames + 1
while step_idx <= #steps and frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
end)
LUA
video_arg="-window"
sound_arg=""
throttle_arg=""
if [[ "$headless" = "1" ]]; then
video_arg="-video soft"
sound_arg="-sound none"
throttle_arg="-nothrottle"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
else
export SDL_MOUSE_RELATIVE_MODE_WARP=0
fi
if [[ -n $target ]]; then
echo "MAME apple2gs: all-examples CFFA2 disk, auto-launching $target."
else
echo "MAME apple2gs: all-examples CFFA2 disk. Open JOEYLIB and pick a demo."
fi
echo "GS/OS takes ~50 s to reach Finder. Quit MAME to end the session."
cd "$work"
# -ramsize 4M: AGI (on this disk) needs the larger Memory Manager pool;
# harmless for the smaller demos.
"$MAME" apple2gs \
-ramsize 4M \
-rompath "$rompath" \
-sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/joey-all.2mg" \
$video_arg $sound_arg $throttle_arg \
-skip_gameinfo \
-snapshot_directory "$work/snap" \
-autoboot_script "$work/launch.lua" &
mame_pid=$!
wait "$mame_pid" || true
mame_pid=""
if [[ -d $work/snap ]]; then
rm -rf /tmp/run-iigs-snap
mv "$work/snap" /tmp/run-iigs-snap
echo "run-iigs: snapshots -> /tmp/run-iigs-snap"
fi
exit 0
fi
# ---------------------------------------------------------------- MAME
# Example given: auto-launching MAME path.
if [[ $# -eq 1 ]]; then
target=${1^^}
if [[ ! -f "$bin_dir/$target" ]]; then
echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
if compgen -G "$bin_dir/*" > /dev/null; then
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -printf ' %f\n' \
| grep -vE '\.2mg$|\.txt$' >&2 || true
fi
exit 1
fi
if [[ ! -f $sys_disk ]]; then
echo "missing: $sys_disk (run ./toolchains/install.sh)" >&2
exit 1
fi
# ------------------------------------------------------------- AGI
# AGI needs ~880KB (240KB interpreter + 644KB KQ3 resources), which
# overflows the 800KB 3.5" floppy the generic path uses, and MAME's
# IIgs 3.5 drive is 800K-GCR-only. So AGI runs off a CFFA2 CompactFlash
# card instead: partition 1 (-hard1) is a bootable GS/OS, partition 2
# (-hard2) is a 2MB ProDOS volume with AGI + the game data. The boot
# scan hits slot 7 first, boots GS/OS from hard1, and GS/OS then mounts
# the AGI volume; the Lua driver opens it and launches AGI. No BSS/BRAM
# or startup-slot changes are needed with this two-partition topology.
if [[ $target == AGI ]]; then
game=${JOEY_AGI_GAME:-kq3}
# make-agi-iigs-disk.sh needs the toolchain env (cadius path).
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
# MAME loads the CFFA2 firmware as an a2cffa2 device ROM. Prefer the
# copy staged in the repo (toolchains/install.sh territory) so the
# run does not depend on the user's ~/.mame; fall back to the user
# rompath. Legal: the firmware is freely redistributed by dreher.net
# and the card is owned.
staged_roms=$repo/toolchains/emulators/support/mame-roms
user_roms="${MAME_ROMPATH:-$HOME/.mame/roms}"
rompath="$user_roms"
if [[ -f "$staged_roms/a2cffa2/cffa20eec02.bin" ]]; then
rompath="$user_roms;$staged_roms"
elif [[ ! -f "$user_roms/a2cffa2/cffa20eec02.bin" ]]; then
cat >&2 <<EOF
missing CFFA2 firmware: a2cffa2/cffa20eec02.bin
Needed to mount AGI's data disk (4096 bytes, SHA1
080ff88f19de22328e162954ee2b51ee65f9d5cd). It ships in dreher.net's
Run6_CDROM.zip as Firmware/V2.0/CFFA20EEC02.BIN. Drop it in either
$staged_roms/a2cffa2/ or $user_roms/a2cffa2/
EOF
exit 1
fi
headless=${MAME_HEADLESS:-0}
work=$(mktemp -d -t joeylib-run-agi.XXXXXX)
mame_pid=""
cleanup() {
if [[ -n $mame_pid ]] && kill -0 "$mame_pid" 2>/dev/null; then
kill "$mame_pid" 2>/dev/null || true
sleep 1
kill -9 "$mame_pid" 2>/dev/null || true
fi
rm -rf "$work"
}
trap cleanup EXIT INT TERM
# hard1: writable bootable GS/OS copy. hard2: freshly built AGI vol.
cp "$sys_disk" "$work/boot.po"
echo "run-iigs: building AGI CFFA data disk ($game)..."
"$repo/scripts/make-agi-iigs-disk.sh" "$work/agi.2mg" "$game"
# Finder driver: at calibrated frame counts type A (select the AGI
# volume -- boot volume is System.Disk, so 'A' is unambiguous), Cmd-O
# to open it, the full name AGI to select the app, Cmd-O to launch.
cat > "$work/launch.lua" <<LUA
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 headless = ($headless == 1)
local frames = 0
local steps = {
{ 3000, function() nat:post("A") end },
{ 3120, function() press(key_cmd) end },
{ 3126, function() nat:post("o") end },
{ 3180, function() release(key_cmd) end },
{ 3540, function() nat:post("AGI") end },
{ 3660, function() press(key_cmd) end },
{ 3666, function() nat:post("o") end },
{ 3720, function() release(key_cmd) end },
}
if headless then
table.insert(steps, { 4200, function() manager.machine.video:snapshot() end })
table.insert(steps, { 6600, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9000, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9100, function() manager.machine:exit() end })
end
table.sort(steps, function(a, b) return a[1] < b[1] end)
local step_idx = 1
emu.register_periodic(function()
frames = frames + 1
while step_idx <= #steps and frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
end)
LUA
video_arg="-window"
sound_arg=""
throttle_arg=""
if [[ "$headless" = "1" ]]; then
video_arg="-video soft"
sound_arg="-sound none"
throttle_arg="-nothrottle"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
else
export SDL_MOUSE_RELATIVE_MODE_WARP=0
fi
cat <<EOF
MAME apple2gs: AGI ($game) on a CFFA2 disk. GS/OS boots from hard1
(~50 s) and mounts the AGI volume on hard2; the Lua driver opens it and
launches AGI. Quit MAME (or close its window) to end the session.
EOF
cd "$work"
# -ramsize 4M: AGI's ~240KB app plus GS/OS/Finder leaves the stock
# 2MB Memory Manager pool too tight -- its first jlAlloc (the view
# cache) returns NULL and the app exits. 4MB is the assumed target.
"$MAME" apple2gs \
-ramsize 4M \
-rompath "$rompath" \
-sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/agi.2mg" \
$video_arg $sound_arg $throttle_arg \
-skip_gameinfo \
-snapshot_directory "$work/snap" \
-autoboot_script "$work/launch.lua" &
mame_pid=$!
wait "$mame_pid" || true
mame_pid=""
if [[ -d $work/snap ]]; then
rm -rf /tmp/run-iigs-snap
mv "$work/snap" /tmp/run-iigs-snap
echo "run-iigs: snapshots -> /tmp/run-iigs-snap"
fi
exit 0
fi
# Dedicated run disk so joey.2mg (the bench/golden disk) is never
# replaced -- a stale-swapped joey.2mg silently corrupts the next
# UBER bench run. Rebuild when missing, holding a different
# example, or older than the example binary.
run_disk=$bin_dir/joey-run.2mg
stamp=$run_disk.contents
need_disk=0
if [[ ! -f $run_disk ]]; then
need_disk=1
elif [[ "$(cat "$stamp" 2>/dev/null)" != "$target" ]]; then
need_disk=1
elif [[ "$bin_dir/$target" -nt $run_disk ]]; then
need_disk=1
fi
if [[ $need_disk -eq 1 ]]; then
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# make-iigs-disk.sh needs the toolchain env (cadius path).
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
echo "run-iigs: building single-example run disk ($target)..."
JOEY_DISK_EXAMPLES="$target" "$repo/scripts/make-iigs-disk.sh" "$run_disk"
printf '%s' "$target" > "$stamp"
else
echo "run-iigs: run disk is current ($target)"
fi
work=$(mktemp -d -t joeylib-run-iigs.XXXXXX)
mame_pid=""
cleanup() {
# Kill the emulator if we are exiting first (Ctrl-C, timeout):
# bash does not forward signals to a foreground child, and an
# orphaned MAME keeps burning CPU with its work dir deleted
# from under it.
if [[ -n $mame_pid ]] && kill -0 "$mame_pid" 2>/dev/null; then
kill "$mame_pid" 2>/dev/null || true
sleep 1
kill -9 "$mame_pid" 2>/dev/null || true
fi
# Preserve the post-run disk (high scores etc.) for inspection.
if [[ -f $work/joey-run.2mg ]]; then
cp "$work/joey-run.2mg" "$bin_dir/joey-run-after.2mg"
fi
rm -rf "$work"
}
trap cleanup EXIT INT TERM
# Finder-less boot disk: /System/Start is tools/iigsStart, which
# SetPrefixGS's to the data volume and QuitGS-launches the example.
# This replaces the old Lua keyboard driver, which typed J / Cmd-O /
# name / Cmd-O at calibrated frame counts -- workable but fragile,
# and it made every run sit through the Finder's ~60 s desktop paint
# before anything happened. Straight to the example in ~50 s now.
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
echo "run-iigs: building Finder-less boot disk for $target..."
"$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$target" >/dev/null
cp "$run_disk" "$work/joey-run.2mg"
headless=${MAME_HEADLESS:-0}
cat > "$work/launch.lua" <<LUA
-- The boot disk launches the example by itself (tools/iigsStart), so
-- nothing here drives the Finder any more. This script only exists for
-- the headless snapshot/exit schedule.
local headless = ($headless == 1)
local frames = 0
local steps = {}
if headless then
-- Batch verification: snapshot right after the launch lands, mid
-- asset load, and after the example settles on its first screen,
-- then exit so the caller regains control. Interactive runs never
-- exit on their own.
table.insert(steps, { 3900, function() manager.machine.video:snapshot() end })
table.insert(steps, { 6600, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9000, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9100, function() manager.machine:exit() end })
end
local step_idx = 1
emu.register_periodic(function()
frames = frames + 1
if headless and frames % 600 == 0 then
-- Pacing heartbeat for batch runs (rescued with the snaps).
local f = io.open("launch-progress.txt", "w")
if f ~= nil then
f:write(string.format("frames=%d\n", frames))
f:close()
end
end
while step_idx <= #steps and frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
end)
LUA
video_arg="-window"
sound_arg=""
throttle_arg=""
if [[ "$headless" = "1" ]]; then
# Verification mode: no window, no sound, and unthrottled so
# the 12k-frame scripted run finishes in about a minute.
video_arg="-video soft"
sound_arg="-sound none"
throttle_arg="-nothrottle"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
fi
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
cat <<EOF
MAME apple2gs: auto-launching $target off $run_disk.
Booting a Finder-less GS/OS whose startup application IS $target, so it
comes up in ~50 s with no Finder in the way. Quit MAME (or close its
window) to end the session.
EOF
cd "$work"
# Throttled (real IIgs speed) -- this is the "play it" path,
# unlike the verify/bench harnesses which run -nothrottle. Runs
# as a tracked child so the cleanup trap can reap it on Ctrl-C.
# -skip_gameinfo matters: without it MAME parks on the "driver is
# not working correctly" info screen waiting for a keypress and
# the machine never boots (the -debug harnesses never hit this).
# -snapshot_directory is explicit because MAME otherwise resolves
# it against the user's mame.ini (snapshots vanished into
# ~/.mame/snap during bring-up).
"$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" \
-flop4 "$work/joey-run.2mg" \
$video_arg $sound_arg $throttle_arg \
-skip_gameinfo \
-snapshot_directory "$work/snap" \
-autoboot_script "$work/launch.lua" &
mame_pid=$!
wait "$mame_pid" || true
mame_pid=""
if [[ -d $work/snap ]]; then
rm -rf /tmp/run-iigs-snap
mv "$work/snap" /tmp/run-iigs-snap
echo "run-iigs: snapshots -> /tmp/run-iigs-snap"
fi
if [[ -f $work/launch-progress.txt ]]; then
cp "$work/launch-progress.txt" /tmp/run-iigs-progress.txt
fi
exit 0
fi
# -------------------------------------------------------------- GSplus
# No argument: original GSplus + interactive Finder path.
gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom
data_disk=$bin_dir/joey.2mg
null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom
for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do
if [[ ! -f $f ]]; then
echo "missing: $f" >&2
if [[ $f == "$data_disk" ]]; then
echo "run 'make iigs-disk' to build it." >&2
else
echo "run ./toolchains/install.sh (support files should have been staged)." >&2
fi
exit 1
fi
done
# GSplus writes back to disk images during the session; stage writable
# copies so repeated runs do not mutate the originals.
work=$(mktemp -d -t joeylib-iigs.XXXXXX)
cleanup() {
# Stash the post-run disk image for manual inspection.
if [[ -f $work/joey.2mg ]]; then
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg"
echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
fi
rm -rf "$work"
}
trap cleanup EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Stage the null slot-6 PROM as c600.rom in the work dir. GSplus loads
# any file named c600.rom in its search path (cwd first) as the slot 6
# ROM card, overriding the built-in Disk II firmware. The staged image
# is 256 bytes with a leading RTS and no Pascal 1.1 firmware signature,
# so the IIgs boot ROM's slot scan skips slot 6 and the two empty 5.25
# drives never get probed. Source: toolchains/install.sh's
# install_support_iigs_null_c600.
cp "$null_c600" "$work/c600.rom"
cat <<EOF
GSplus launching GS/OS 6.0.4.
Once Finder is up:
1. Open the JOEYLIB disk on the desktop.
2. Double-click whichever demo you want to run.
(Tip: '$0 <example>' auto-launches via MAME instead.)
EOF
# GSplus auto-creates config.kegs in its cwd on first run. cd into
# the scratch dir so that file lands there and gets cleaned up with
# the rest of the temp state, rather than polluting the directory
# the user invoked the script from.
cd "$work"
# GSplus accepts any config.kegs key as a CLI override via "-<key>
# <value>". g_limit_speed=2 caps emulation at the IIgs's native
# 2.8 MHz; without this override GSplus boots at 8 MHz and our
# performance characteristics don't match real hardware. The other
# settings (0=unlimited, 1=1.024 MHz, 3=8 MHz) are reachable through
# the in-emulator speed-cycle key if needed.
#
# No exec: let the bash EXIT trap fire so the scratch dir (and the
# config.kegs GSplus auto-creates) gets cleaned up.
"$gsplus" -rom "$rom" \
-s5d1 "$work/boot.po" \
-s5d2 "$work/joey.2mg" \
-g_limit_speed 2

View file

@ -1,87 +0,0 @@
#!/usr/bin/env bash
# run-x68000.sh - launch a built X68000 example under MAME.
#
# scripts/run-x68000.sh staxi Space Taxi (binary + its DATA tree)
# scripts/run-x68000.sh uber any other built example, by name
# X68K_HEADLESS=1 scripts/run-x68000.sh staxi windowless + snapshots
#
# TWO FLOPPIES, the same split verify-x68000-golden.sh uses: Human68k plus
# AUTOEXEC on A:, and the program with its assets on B:. A 1232 KB floppy
# cannot hold Human68k and a ~285 KB .X together, and AUTOEXEC switches to
# B: first so the program's CWD is the disk its DATA/ tree is on.
#
# Needs toolchains/x68mame (HUMAN302.XDF + roms/) -- see the X68000 memory
# note for where to copy it from.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
prog=${1:-staxi}
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
bin=$repo/build/x68000/bin/$PROG.X
x68=${X68K_DIR:-$repo/toolchains/x68mame}
template=$x68/HUMAN302.XDF
# MAME: the x68k build this repo carries, unless one is installed or named.
MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}"
if [[ ! -x $MAME ]]; then
MAME="$(command -v mame || true)"
fi
[[ -n ${MAME:-} && -x $MAME ]] || { echo "run-x68000: no MAME (set MAME=...)" >&2; exit 2; }
[[ -f $bin ]] || { echo "run-x68000: $bin not built (make -f make/x68000.mk)" >&2; exit 2; }
[[ -f $template ]] || { echo "run-x68000: missing $template (copy toolchains/x68mame in)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68run.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"
# Data disk = the template emptied (keeps the BPB and boot sector intact).
# `empty` and not a list of filenames: the stock disk carries ~900 KB of
# Human68k utilities in SUBDIRECTORIES (SYS, BIN, BASIC2, ...), and deleting
# only the root files leaves every one of those clusters allocated -- which is
# what made a 1232 KB floppy look like it had 330 KB to give.
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
# Boot disk keeps only what it takes to boot and launch 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 || 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
# The example's DATA tree, if it has one, at the same paths jlDataOpen builds
# ("DATA/" + the app's relative name). Human68k names are 8.3 upper case.
data_dir=$repo/build/x68000/bin/DATA
if [[ -d $data_dir ]]; then
while IFS= read -r f; do
rel=${f#"$data_dir"/}
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \
"DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null
done < <(find "$data_dir" -type f | sort)
fi
echo "run-x68000: $(python3 "$repo/tools/xdftool.py" free "$work/data.xdf")"
if [[ ${X68K_HEADLESS:-0} == 1 ]]; then
snap=${X68K_SNAP:-/tmp/run-x68000-snap}
rm -rf "$snap"; mkdir -p "$snap"
cat > "$work/snap.lua" <<'LUA'
local n = 0
emu.register_frame_done(function()
n = n + 1
if n % 1200 == 0 then manager.machine.video:snapshot() end
if n >= 24000 then manager.machine:exit() end
end)
LUA
"$MAME" x68000 -bios ipl10 -rompath "$x68/roms" \
-flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-snapshot_directory "$snap" -autoboot_script "$work/snap.lua" </dev/null >/dev/null 2>&1 || true
echo "run-x68000: snapshots -> $snap"
else
echo "run-x68000: booting Human68k, then B:\\$PROG.X. Quit MAME to end."
"$MAME" x68000 -bios ipl10 -rompath "$x68/roms" \
-flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" -skip_gameinfo
fi

View file

@ -40,14 +40,17 @@
# example binary name in build/amiga/bin, default Audio # example binary name in build/amiga/bin, default Audio
# Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV # Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV
# Requires: fs-uae, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire), # Requires: fs-uae, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire),
# python3, toolchains/emulators/support/kickstart.rom, # python3, the Kickstart (install.sh install emulator:amiga),
# build/amiga/bin/<example> # build/amiga/bin/<example>
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed FSUAE fsuae
joeyNeed KICKSTART_ROM kickstart
prog=${1:-Audio} prog=${1:-Audio}
binDir=$repo/build/amiga/bin binDir=$repo/build/amiga/bin
kickstart=$repo/toolchains/emulators/support/kickstart.rom kickstart=$KICKSTART_ROM
grader=$repo/tools/gradeAudioPhases.py grader=$repo/tools/gradeAudioPhases.py
pulseLib=$repo/scripts/pulseRecordLib.sh pulseLib=$repo/scripts/pulseRecordLib.sh
label=verify-amiga-audio label=verify-amiga-audio
@ -57,7 +60,7 @@ label=verify-amiga-audio
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; } [ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
# shellcheck source=scripts/pulseRecordLib.sh # shellcheck source=scripts/pulseRecordLib.sh
. "$pulseLib" . "$pulseLib"
for tool in fs-uae Xvfb xdotool parec pactl python3; do for tool in Xvfb xdotool parec pactl python3; do
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; } command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
done done
pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; } pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
@ -80,7 +83,7 @@ parecPid=""
cleanup() { cleanup() {
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null [ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
[ -n "$fsPid" ] && kill "$fsPid" 2>/dev/null joeyStop "$fsPid"
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null [ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
# Always unload the sink: leaving null sinks behind on the user's # Always unload the sink: leaving null sinks behind on the user's
# audio server is the one side effect this gate could have. # audio server is the one side effect this gate could have.
@ -94,7 +97,7 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
# Stage the Amiga volume exactly as run-amiga-headless.sh does. # Stage the Amiga volume the way joey-run does.
mkdir -p "$work/hd/s" mkdir -p "$work/hd/s"
if [ -n "$progDir" ]; then if [ -n "$progDir" ]; then
cp -r "$progDir/." "$work/hd/" cp -r "$progDir/." "$work/hd/"
@ -114,7 +117,7 @@ sleep 1
# --fast_memory=0 --slow_memory=0 keeps this a bare A500, the port's floor. # --fast_memory=0 --slow_memory=0 keeps this a bare A500, the port's floor.
DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \ DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \
fs-uae \ "$FSUAE" \
--amiga_model=A500 \ --amiga_model=A500 \
--hard_drive_0="$work/hd" \ --hard_drive_0="$work/hd" \
--hard_drive_0_label=JOEYLIB \ --hard_drive_0_label=JOEYLIB \

View file

@ -10,12 +10,15 @@
# #
# Usage: scripts/verify-amiga-input.sh # Usage: scripts/verify-amiga-input.sh
# Requires: fs-uae, Xvfb, xdotool; build/amiga/bin/Keys (make amiga); # Requires: fs-uae, Xvfb, xdotool; build/amiga/bin/Keys (make amiga);
# toolchains/emulators/support/kickstart.rom. # the Kickstart (install.sh install emulator:amiga).
set -euo pipefail set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed FSUAE fsuae
joeyNeed KICKSTART_ROM kickstart
bin_dir=$repo/build/amiga/bin bin_dir=$repo/build/amiga/bin
kickstart=$repo/toolchains/emulators/support/kickstart.rom kickstart=$KICKSTART_ROM
target="192.168.1.10:6510" target="192.168.1.10:6510"
@ -25,7 +28,7 @@ target="192.168.1.10:6510"
work=$(mktemp -d -t joeylib-amiga-input.XXXXXX) work=$(mktemp -d -t joeylib-amiga-input.XXXXXX)
display_num=$((RANDOM % 500 + 300)) display_num=$((RANDOM % 500 + 300))
cleanup() { cleanup() {
[ -n "${fsuae_pid:-}" ] && kill "$fsuae_pid" 2>/dev/null joeyStop "${fsuae_pid:-}"
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null [ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work" rm -rf "$work"
} }
@ -45,7 +48,7 @@ sleep 1
# XSendEvent routes are ignored by SDL. A fullscreen SDL window takes # XSendEvent routes are ignored by SDL. A fullscreen SDL window takes
# focus itself, after which plain XTEST typing (xdotool without # focus itself, after which plain XTEST typing (xdotool without
# --window) lands in the emulated Amiga. # --window) lands in the emulated Amiga.
DISPLAY=":$display_num" fs-uae \ DISPLAY=":$display_num" "$FSUAE" \
--amiga_model=A500 \ --amiga_model=A500 \
--fast_memory=2048 \ --fast_memory=2048 \
--kickstart_file="$kickstart" \ --kickstart_file="$kickstart" \

View file

@ -46,14 +46,17 @@
# example PRG base name in build/atarist/bin, default AUDIO # example PRG base name in build/atarist/bin, default AUDIO
# Env: ST_AUDIO_TAPS how many held SPACE presses (default 6) # Env: ST_AUDIO_TAPS how many held SPACE presses (default 6)
# ST_AUDIO_KEEP set to keep the work dir for inspection # ST_AUDIO_KEEP set to keep the work dir for inspection
# Requires: hatari, toolchains/emulators/support/emutos-512k.img, # Requires: ./install.sh install emulator:atarist (Hatari, EmuTOS),
# build/atarist/bin/<example>.PRG # build/atarist/bin/<example>.PRG
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
prog=${1:-AUDIO} prog=${1:-AUDIO}
taps=${ST_AUDIO_TAPS:-6} taps=${ST_AUDIO_TAPS:-6}
tos=$repo/toolchains/emulators/support/emutos-512k.img tos=$EMUTOS_IMG
binDir=$repo/build/atarist/bin binDir=$repo/build/atarist/bin
# Space Taxi ships as its own folder; everything else is a bare PRG. # Space Taxi ships as its own folder; everything else is a bare PRG.
@ -90,7 +93,7 @@ trap cleanup EXIT INT TERM
# #
# The psg_write trace runs ~800 KB/s, so it is filtered as it is produced # The psg_write trace runs ~800 KB/s, so it is filtered as it is produced
# -- keeping the raw stream would put hundreds of MB in the work dir. # -- keeping the raw stream would put hundreds of MB in the work dir.
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy hatari \ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
--tos "$tos" \ --tos "$tos" \
--harddrive "$driveDir" \ --harddrive "$driveDir" \
--gemdos-drive C \ --gemdos-drive C \

View file

@ -19,8 +19,11 @@
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
bin_dir=$repo/build/atarist/bin bin_dir=$repo/build/atarist/bin
tos=$repo/toolchains/emulators/support/emutos-512k.img tos=$EMUTOS_IMG
target="192.168.1.10:6510" target="192.168.1.10:6510"
@ -29,13 +32,13 @@ target="192.168.1.10:6510"
work=$(mktemp -d -t joeylib-st-input.XXXXXX) work=$(mktemp -d -t joeylib-st-input.XXXXXX)
cleanup() { cleanup() {
[ -n "${hatari_pid:-}" ] && kill "$hatari_pid" 2>/dev/null joeyStop "${hatari_pid:-}"
rm -rf "$work" rm -rf "$work"
} }
trap cleanup EXIT trap cleanup EXIT
cd "$work" cd "$work"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy hatari \ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
--tos "$tos" \ --tos "$tos" \
--harddrive "$bin_dir" \ --harddrive "$bin_dir" \
--gemdos-drive C \ --gemdos-drive C \

View file

@ -2,7 +2,7 @@
# verify-dos-audio.sh - DOS audio gate, graded on recorded sound. # verify-dos-audio.sh - DOS audio gate, graded on recorded sound.
# #
# The DOS HAL drives a Sound Blaster Pro/SB16 with 8237 auto-init DMA fed # The DOS HAL drives a Sound Blaster Pro/SB16 with 8237 auto-init DMA fed
# by libxmp-lite (src/dos/audio.c). DOSBox 0.74 exposes none of that to a # by libxmp-lite (src/dos/audio.c). DOSBox exposes none of that to a
# script -- no register trace, no debugger this harness can drive -- so # script -- no register trace, no debugger this harness can drive -- so
# unlike the IIgs and ST gates (which read the DOC and the YM2149) this # unlike the IIgs and ST gates (which read the DOC and the YM2149) this
# one records what actually came out and grades it. The Amiga gate works # one records what actually came out and grades it. The Amiga gate works
@ -28,7 +28,8 @@
# unambiguous. This also covers the SMIX overlay specifically -- SFX on # unambiguous. This also covers the SMIX overlay specifically -- SFX on
# DOS mix through libxmp-lite's smix rather than a separate channel, so # DOS mix through libxmp-lite's smix rather than a separate channel, so
# "SFX audible while the module player is loaded but paused" is exactly # "SFX audible while the module player is loaded but paused" is exactly
# the path that can break. Measured: music ~3290, paused ~113, sfx ~1341. # the path that can break. Measured under DOSBox Staging 0.83 (whose mixer
# runs quieter than the old 0.74's): music ~1220, paused ~17, sfx ~659.
# #
# Keys are HELD (keydown, pause, keyup), not tapped: the demo tests # Keys are HELD (keydown, pause, keyup), not tapped: the demo tests
# jlKeyPressed, an edge predicate polled once a frame, and a make/break # jlKeyPressed, an edge predicate polled once a frame, and a make/break
@ -38,11 +39,13 @@
# example EXE base name in build/dos/bin, default AUDIO # example EXE base name in build/dos/bin, default AUDIO
# Env: DOS_AUDIO_KEEP set to keep the work dir and the WAV # Env: DOS_AUDIO_KEEP set to keep the work dir and the WAV
# CYCLES override the conf's 386SX-16 cpu cap # CYCLES override the conf's 386SX-16 cpu cap
# Requires: dosbox, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire), # Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, parec/pactl,
# python3, build/dos/bin/<example>.EXE # python3, build/dos/bin/<example>.EXE
set -uo pipefail set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed DOSBOX dosbox
prog=${1:-AUDIO} prog=${1:-AUDIO}
binDir=$repo/build/dos/bin binDir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf conf=$repo/scripts/dosbox-386sx16.conf
@ -55,7 +58,7 @@ label=verify-dos-audio
# shellcheck source=scripts/pulseRecordLib.sh # shellcheck source=scripts/pulseRecordLib.sh
. "$pulseLib" . "$pulseLib"
[ -f "$conf" ] || { echo "$label: missing $conf" >&2; exit 2; } [ -f "$conf" ] || { echo "$label: missing $conf" >&2; exit 2; }
for tool in dosbox Xvfb xdotool parec pactl python3; do for tool in Xvfb xdotool parec pactl python3; do
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; } command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
done done
pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; } pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
@ -79,7 +82,7 @@ parecPid=""
cleanup() { cleanup() {
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null [ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
[ -n "$dosPid" ] && kill "$dosPid" 2>/dev/null joeyStop "$dosPid"
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null [ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
# Always unload the sink: a leaked null sink on the user's audio # Always unload the sink: a leaked null sink on the user's audio
# server is the one side effect this gate could have. # server is the one side effect this gate could have.
@ -101,16 +104,16 @@ Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
xvfbPid=$! xvfbPid=$!
sleep 1 sleep 1
dosArgs=(-conf "$conf") dosArgs=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$conf")
[ -n "${CYCLES:-}" ] && dosArgs+=(-set "cpu cycles=$CYCLES") [ -n "${CYCLES:-}" ] && dosArgs+=(--set "cpu_cycles=$CYCLES" --set "cpu_cycles_protected=$CYCLES")
dosArgs+=( dosArgs+=(
-c "MOUNT C $mountDir" -c "MOUNT C $mountDir"
-c "C:" -c "C:"
-c "$prog.EXE" -c "$prog.EXE"
) )
DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \ XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
dosbox "${dosArgs[@]}" > "$work/dosbox.log" 2>&1 & "$DOSBOX" "${dosArgs[@]}" > "$work/dosbox.log" 2>&1 &
dosPid=$! dosPid=$!
sleep 12 sleep 12
@ -120,7 +123,7 @@ if ! kill -0 "$dosPid" 2>/dev/null; then
exit 1 exit 1
fi fi
win=$(DISPLAY="$display" xdotool search --name -- "DOSBox" | head -1) win=$(joeyFindWindow "$display" "$dosPid")
if [ -z "$win" ]; then if [ -z "$win" ]; then
echo "$label: FAIL - no DOSBox window to send keys to" >&2 echo "$label: FAIL - no DOSBox window to send keys to" >&2
exit 1 exit 1
@ -166,13 +169,13 @@ parecPid=""
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; } [ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
# Thresholds sit far from the measured values (music ~3290, paused ~113, # Thresholds sit at about half the measured levels (music ~1220, paused
# sfx ~1341) so ordinary mixing jitter cannot flip the verdict. # ~17, sfx ~659) so ordinary mixing jitter cannot flip the verdict.
python3 "$grader" "$label" "$work/out.wav" \ python3 "$grader" "$label" "$work/out.wav" \
"music:1:5:1500:" \ "music:1:5:600:" \
"paused:8:13::600" \ "paused:8:13::200" \
"sfx:16:22:700:" \ "sfx:16:22:330:" \
"resumed:27:32:1500:" "resumed:27:32:600:"
status=$? status=$?
[ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null [ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null
exit $status exit $status

Some files were not shown because too many files have changed in this diff Show more