diff --git a/.gitignore b/.gitignore
index fdc3286..c08f495 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,11 @@ build/
*.gs
*.bin
-# Toolchain installations (everything fetched/built by install.sh)
-toolchains/*
+# Everything ./install.sh downloads, builds or generates. Nothing in it is
+# 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
*.adf
diff --git a/LICENSES.md b/LICENSES.md
index 4771dd3..a454076 100644
--- a/LICENSES.md
+++ b/LICENSES.md
@@ -22,7 +22,7 @@ JoeyLib. License compatibility for these matters most.
- **License:** MIT
- **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Linked
into AUDIO binaries on the chunky-display platforms.
-- **Vendored at:** `toolchains/audio/libxmp-lite/`
+- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
### NinjaTrackerPlus (NTP)
@@ -33,10 +33,9 @@ JoeyLib. License compatibility for these matters most.
by Jesse Blue (NinjaForce). Treated here as free for use; consult
upstream before commercial redistribution.
- **Role:** Apple IIgs Ensoniq DOC `.NTP` module replayer + SFX
- streamer. Merlin32-assembled and **linked** into the AUDIO binary as
- bytes (see `make/iigs.mk` `$(NTP_HEADER)` rule).
-- **Vendored at:** `toolchains/iigs/ntp/ninjatrackerplus.s` (source);
- build product baked into `examples/audio` AUDIO binary.
+ streamer. Merlin32-assembled into `ntpplayer`, which ships on the disk
+ next to each IIgs program and is loaded at run time.
+- **Installed at:** `toolchains/opt/ntp/` (source; installer component `ntp`).
### PTPlayer
@@ -46,7 +45,7 @@ JoeyLib. License compatibility for these matters most.
- **License:** Public Domain
- **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the
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).
- **Role:** clang C compiler targeting the 65816, plus llvm-mc for
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
variable.
-- **Vendored at:** `toolchains/iigs/llvm-mos/`
+- **Installed at:** `toolchains/opt/llvm816/` (installer component `llvm816`)
### Merlin32
@@ -75,12 +74,12 @@ produced game binaries.
- **URL:**
- **License:** Freely distributed by Brutal Deluxe Software
(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,
redistribute as part of a build chain; ask before forking
commercially."
- **Role:** 65816 cross-assembler. Builds NinjaTrackerPlus.
-- **Vendored at:** `toolchains/iigs/merlin32/`
+- **Installed at:** `toolchains/opt/merlin32/` (installer component `merlin32`)
### Bebbo m68k-amigaos GCC toolchain
@@ -92,13 +91,13 @@ produced game binaries.
domain, ira freeware, etc.).
- **Role:** AmigaOS HUNK-format gcc + binutils + libnix used to
compile the Amiga port and link against PTPlayer.
-- **Vendored at:** `toolchains/amiga/gcc/`
+- **Installed at:** `toolchains/opt/amigaGcc/` (installer component `amigaGcc`)
### vasm
- **Used on:** Amiga (PTPlayer only)
- **URL:**
-- **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
for M68k + AmigaOS targets** (see vasm distribution README). Other
commercial use requires written permission. JoeyLib's only
@@ -107,7 +106,8 @@ produced game binaries.
- **Role:** Assemble third-party `ptplayer.asm` (Frank Wille's
Devpac/vasm-syntax source). Our own `.s` files are GAS syntax and
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)
@@ -118,7 +118,7 @@ produced game binaries.
and permissive (per-file headers).
- **Role:** GEMDOS-native gcc + binutils + mintlib used to compile
the Atari ST port.
-- **Vendored at:** `toolchains/atarist/gcc/`
+- **Installed at:** `toolchains/opt/mintGcc/` (installer component `mintGcc`)
### DJGPP (build-djgpp packaging)
@@ -127,23 +127,23 @@ produced game binaries.
(DJGPP upstream: )
- **License:** Build script GNU GPL v3; bundled gcc/binutils GNU
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
COFF binaries that run on 386+ DOS via DPMI.
-- **Vendored at:** `toolchains/dos/djgpp/`
+- **Installed at:** `toolchains/opt/djgpp/` (installer component `djgpp`)
### CWSDPMI
- **Used on:** DOS
- **URL:**
- **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
- source availability — full terms in `cwsdpmi.doc`. Source is
+ source availability -- full terms in `cwsdpmi.doc`. Source is
GPL-licensed if distributed.
- **Role:** Stubbed-into-binary DPMI server for DJGPP-compiled DOS
executables (via `embed-dpmi.sh`).
-- **Vendored at:** `toolchains/dos/cwsdpmi/`
+- **Installed at:** `toolchains/opt/cwsdpmi/` (installer component `cwsdpmi`)
### NASM
@@ -152,93 +152,95 @@ produced game binaries.
- **License:** 2-clause BSD
- **Role:** Assembler for x86 inline-asm support files in the DOS
port.
-- **Vendored at:** `toolchains/dos/nasm/`
+- **Installed from:** the Linux distribution's `nasm` package
---
## Test-time emulators (not shipped, not linked)
-Used by `scripts/run-*.sh` to run the produced binaries on the
-developer's machine. Not part of any redistributed artifact.
-
-### GSplus (KEGS fork)
-
-- **Used on:** Apple IIgs
-- **URL:**
-- **License:** GNU GPL v3
-- **Vendored at:** `toolchains/emulators/gsplus/`
+Used by `joey-run` and the test gates to run the produced binaries on the
+developer's machine. Not part of any redistributed artifact. Installed by
+`./install.sh install emulators`.
### MAME
-- **Used on:** Apple IIgs (apple2gs driver, optional)
+- **Used on:** Apple IIgs (apple2gs driver) and Sharp X68000 (x68k driver)
- **URL:**
- **License:** GNU GPL v2+ (GPL-2.0-or-later) plus a 3-clause BSD
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
- **Used on:** Atari ST
- **URL:**
- **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
-- **URL:**
-- **License:** GNU GPL v2
+- **URL:**
+- **License:** GNU GPL v2+
+- **Installed at:** `toolchains/opt/dosbox/` (the project's Linux release;
+ installer component `dosbox`)
### FS-UAE
- **Used on:** Amiga
- **URL:**
- **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
- **Used on:** Atari ST (TOS replacement for Hatari)
- **URL:**
- **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)
-These are **not** part of JoeyLib distribution and **not**
-redistributed. They are downloaded to the developer's local cache by
-`toolchains/install.sh` for emulator-side testing only. License terms
-prohibit redistribution; please consult the source if you intend to
-ship anything that depends on them.
+These are **not** part of JoeyLib distribution. `./install.sh`
+places them in `toolchains/firmware/` for emulator-side testing only; see
+the source of each before shipping anything that depends on them.
-### Apple IIgs ROM 03
+### Downloaded by the installer
-- **Used on:** Apple IIgs (GSplus, MAME)
-- **URL:** Various preservation archives, e.g.
-
-- **License:** Apple Inc. proprietary firmware. Provided here for
- developer testing only; **do not redistribute** as part of any
- derivative project.
+- **Apple IIgs MAME ROM sets** (`apple2gs`, ROM 03, and `apple2gsr1`,
+ ROM 01): Apple Inc. proprietary firmware, from the archive.org MAME 0.240
+ split set. Component `iigsFirmware`.
+- **Apple IIgs System 6.0.4** (`System.Disk`): Apple Inc. proprietary OS, from
+ the Apple II archive (). Component
+ `iigsFirmware`.
+- **CFFA2 card firmware** (`cffa20eec02.bin`): freely redistributed by
+ 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
+ (). 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)
-- **URL:**
-- **License:** Apple Inc. proprietary OS. Same caveat as the ROM.
+Installed with `./install.sh import PATH`, or from the bundle a
+project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in
+`installer/files.sha1`.
-### Apple IIgs apple2gs MAME ROM set
-
-- **Used on:** Apple IIgs (MAME testing path)
-- **URL:** Same preservation archives as above.
-- **License:** Apple Inc. proprietary firmware. Required for MAME's
- apple2gs driver but not redistributed.
-
-### Amiga Kickstart ROM (optional)
-
-- **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.
+- **Amiga Kickstart 3.1 ROM**: Cloanto / Amiga Corp. proprietary, sold today
+ (Amiga Forever). Required by the Amiga test gates; `joey-run` falls back to
+ FS-UAE's AROS without it. Component `kickstart`.
+- **X68000 CG (font) ROM**: not part of Sharp's free release. MAME's x68000
+ driver needs it. Component `x68kCgrom`.
+- **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original
+ game's data, from which the example's levels are extracted. Optional.
+ Component `staxiData`.
---
@@ -249,7 +251,7 @@ When adding a new third-party dependency:
1. Append a row in the appropriate section above.
2. Record platform(s), upstream URL, license name, and a one-line
role description.
-3. If the dependency is vendored, list the path under
- `toolchains/`.
+3. List where the installer puts it (`toolchains/opt//` or
+ `toolchains/firmware/`).
4. If license terms restrict redistribution, call that out
explicitly (as we do for Apple ROMs).
diff --git a/Makefile b/Makefile
index 0bc8290..32b1146 100644
--- a/Makefile
+++ b/Makefile
@@ -1,27 +1,30 @@
# JoeyLib top-level Makefile.
#
# Usage:
-# source toolchains/env.sh (first, every shell session)
-# make builds every target whose toolchain is present
+# make builds every target whose toolchain is installed
# make iigs builds Apple IIgs only
# make amiga builds Commodore Amiga only
# make atarist builds Atari ST only
# make dos builds MS-DOS only
+# make x68000 builds Sharp X68000 only
# make docs renders the manual to build/docs (HTML + PDF)
# make verify-audio runs every port's audio gate (needs emulators)
# make clean removes all build outputs
#
-# The build references cross tools exclusively from toolchains/, set up
-# by sourcing toolchains/env.sh. Do not invoke make without sourcing
-# env.sh first or builds will fail with missing tool errors.
+# Cross tools come from ./install.sh, which records their paths in
+# toolchains/config.mk; every makefile reads that file itself, so no
+# environment setup is needed first.
REPO_DIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST))))
-# Detect which toolchains are actually available.
-HAVE_IIGS := $(shell [ -x "$(IIGS_CC)" ] && echo 1)
-HAVE_AMIGA := $(shell [ -x "$(AMIGA_CC)" ] && echo 1)
-HAVE_ATARIST := $(shell [ -x "$(ST_CC)" ] && echo 1)
-HAVE_DOS := $(shell [ -x "$(DOS_CC)" ] && echo 1)
+-include $(REPO_DIR)/toolchains/config.mk
+
+# A port is available when its real compiler binary is installed.
+HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),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 :=
ifeq ($(HAVE_IIGS),1)
@@ -36,12 +39,15 @@ endif
ifeq ($(HAVE_DOS),1)
ALL_TARGETS += dos
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
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
endif
@@ -66,6 +72,9 @@ atarist:
dos:
@$(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
# cc. No toolchain needed; keeps the template from bit-rotting as the lib grows.
blank-check:
@@ -94,11 +103,12 @@ status:
@echo " amiga: $(if $(HAVE_AMIGA),yes,NO)"
@echo " atarist: $(if $(HAVE_ATARIST),yes,NO)"
@echo " dos: $(if $(HAVE_DOS),yes,NO)"
+ @echo " x68000: $(if $(HAVE_X68000),yes,NO)"
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 " 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 " 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"
diff --git a/README.md b/README.md
index 1624ee8..44e7d89 100644
--- a/README.md
+++ b/README.md
@@ -17,42 +17,68 @@ See `claudeDocs/DESIGN.md` for the full 1.0 design.
## 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 joeylib
+git clone https://forge.duensing.digital/AI_Slop/joeylib2.git joeylib
cd joeylib
-./toolchains/install.sh
-# follow any non-free-tool placement instructions the script prints
-source toolchains/env.sh
+./install.sh deps toolchains --install # distribution packages (sudo)
+./install.sh install toolchains # every cross toolchain
make
```
-This builds `libjoey.a` for every target whose toolchain is installed,
-plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
+The first `install` takes about an hour, almost all of it compiling the IIgs
+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`,
`adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception:
-`make iigs` compile-checks the library and runs the clang smoke test, and
-`make -f make/iigs.mk iigs-examples` builds its example binaries. The
-Sharp X68000 is not covered at all -- see below.
+`make iigs` builds the library, and `make iigs-disk` builds its examples and
+packages them onto a disk image.
+
+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
```
-source toolchains/env.sh
make iigs
make amiga
make atarist
make dos
-```
-
-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
+make x68000
```
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
```
-Both X68000 gates want `X68K_SCRATCH` pointing at a work dir containing
-`x68mame/`, and a MAME built with the patches in `patches/`.
+Both X68000 gates run under the patched MAME and the X68000 firmware that
+`./install.sh install emulator:x68000` sets up.
## 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/diff-uber-hashes UBER golden-hash comparator
examples/ example programs
-scripts/ run / bench / verification scripts
-patches/ MAME patches the X68000 gates need
+joey-run run a built program in its emulator
+scripts/ bench / verification scripts (scripts/lib: shared helpers)
tests/goldens/uber/ frozen UBER conformance hashes
-toolchains/ self-contained cross-build tools
-make/ per-target Makefile fragments
+install.sh installs toolchains, emulators and firmware
+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// per-target build outputs
```
diff --git a/docs/Appendix.adoc b/docs/Appendix.adoc
index ecccbc5..8b2c535 100644
--- a/docs/Appendix.adoc
+++ b/docs/Appendix.adoc
@@ -133,8 +133,10 @@
|===
|Symptom |Likely cause
-|Build produces stale or missing binaries, `Error 127 (ignored)`
-|`toolchains/env.sh` was not sourced in this shell.
+|`make` stops with "toolchain:PORT is not installed"
+|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
|The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check
diff --git a/docs/Assets.adoc b/docs/Assets.adoc
index ce4d0d8..febeba3 100644
--- a/docs/Assets.adoc
+++ b/docs/Assets.adoc
@@ -212,8 +212,10 @@ The output extension selects the conversion:
Amiga replayer, PTPlayer, has no native song-end signal, so without this
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore
`E8FF`, so an `.amod` is harmless if it reaches the wrong port.
-* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the staged
- converter under `toolchains/iigs/ntp/`.
+* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
+ 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
the sample data has to fit in memory alongside your game.
diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc
index 4eebe58..0248c81 100644
--- a/docs/BuildIntegration.adoc
+++ b/docs/BuildIntegration.adoc
@@ -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
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
-Build success is weak evidence on retro targets. Run it:
+Build success is weak evidence on retro targets. Run it with `joey-run` (see
+<>), on every machine:
[source,console]
----
-$ scripts/run-iigs.sh MYGAME
-$ scripts/run-amiga.sh MyGame
-$ scripts/run-atarist.sh MYGAME
-$ scripts/run-dos.sh MYGAME
-$ scripts/run-x68000.sh MYGAME
+$ ./joey-run --port iigs mygame
+$ ./joey-run --port amiga mygame
+$ ./joey-run --port atarist mygame
+$ ./joey-run --port dos mygame
+$ ./joey-run --port x68000 mygame
----
-There are also headless runners that capture a screenshot and the program's log
-without opening a window, which is what you want when iterating:
+Without a window, which is what you want while iterating, run it headless and
+look at the snapshot; the program's log lands next to it as usual:
[source,console]
----
-$ scripts/run-atarist-headless.sh MYGAME 90
-$ scripts/run-amiga-headless.sh MyGame 90
+$ ./joey-run --headless --seconds 90 --snapshot mygame.png --port atarist mygame
----
=== The Gates
diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc
index 0e54fee..1b906da 100644
--- a/docs/GettingStarted.adoc
+++ b/docs/GettingStarted.adoc
@@ -2,29 +2,47 @@
=== Setting Up the Toolchains
-JoeyLib cross-compiles for five machines, and every cross toolchain lives under
-`toolchains/` in the repository. They are fetched and built once:
+JoeyLib cross-compiles for five machines. It builds on Linux x86-64, and on
+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]
----
-$ ./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 toolchains/env.sh
+$ ./install.sh install toolchain:dos toolchain:atarist
----
-[WARNING]
-====
-Forgetting `source toolchains/env.sh` is the single most common build problem.
-The makefiles reference the cross compilers only through the variables that
-script sets, so without it a build can appear to succeed while producing stale
-or missing binaries, and errors are reported as `Error 127 (ignored)`. If a
-build behaves strangely, source the script and try again.
-====
+Running `install` again is cheap. Components that are already current are
+skipped, and a component is rebuilt only when its pinned version changes.
+
+No environment setup is needed afterwards. `make` reads the tool paths the
+installer records in `toolchains/config.mk`, and the scripts in
+`scripts/` load the same settings themselves. To use a cross compiler by hand
+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:
@@ -36,27 +54,143 @@ Toolchain availability:
amiga: yes
atarist: 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 <>) 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
[source,console]
----
-$ make # every target whose toolchain is present
+$ make # every target whose toolchain is installed
$ make iigs # Apple IIgs only
$ make amiga # Commodore Amiga only
$ make atarist # Atari ST only
$ make dos # MS-DOS only
+$ make x68000 # Sharp X68000 only
$ 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//bin/`. The IIgs additionally
produces a bootable disk image:
@@ -65,6 +199,15 @@ produces a bootable disk image:
$ 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]
====
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
-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]
----
-$ scripts/run-iigs.sh DRAW
-$ scripts/run-amiga.sh Draw
-$ scripts/run-atarist.sh DRAW
-$ scripts/run-dos.sh DRAW
-$ scripts/run-x68000.sh DRAW
+$ ./install.sh install emulators # every machine
+$ ./install.sh install emulator:dos # or just one
----
-Note the capitalisation differences: the IIgs, ST and DOS use uppercase names
-(ProDOS, GEMDOS and DOS 8.3 conventions), while the Amiga keeps mixed case.
+MAME (for the IIgs and X68000) is always built from source, because the test
+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
diff --git a/examples/spacetaxi/PLAN.md b/examples/spacetaxi/PLAN.md
index 0bbd953..250346f 100644
--- a/examples/spacetaxi/PLAN.md
+++ b/examples/spacetaxi/PLAN.md
@@ -25,7 +25,7 @@ Same loop every time, so it stays cheap to iterate:
`stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
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.
## Phase 0 -- foundation (DONE)
diff --git a/examples/spacetaxi/VERIFIED.md b/examples/spacetaxi/VERIFIED.md
index aaa7ed4..274f1a1 100644
--- a/examples/spacetaxi/VERIFIED.md
+++ b/examples/spacetaxi/VERIFIED.md
@@ -82,9 +82,9 @@ plus the inline scripts run during verification sessions.
## 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/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.
-- `stuff/spacetaxi/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator.
+- `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).
+- `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/dumpAllLevels.py`: pre-existing scene-decompress drivers.
+- `examples/spacetaxi/assets/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator.
## METHODOLOGY (for future verification sessions)
diff --git a/examples/spacetaxi/assets/cpu6502.py b/examples/spacetaxi/assets/cpu6502.py
new file mode 100644
index 0000000..2ab6af5
--- /dev/null
+++ b/examples/spacetaxi/assets/cpu6502.py
@@ -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,
+}
diff --git a/examples/spacetaxi/assets/depackSpeechRam.py b/examples/spacetaxi/assets/depackSpeechRam.py
index 765fac1..22752b0 100755
--- a/examples/spacetaxi/assets/depackSpeechRam.py
+++ b/examples/spacetaxi/assets/depackSpeechRam.py
@@ -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
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,
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.
@@ -41,11 +41,8 @@ def main():
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")
- sys.path.insert(0, STUFF)
- try:
- from cpu6502 import Cpu6502
- except ImportError:
- sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF)
+ sys.path.insert(0, HERE)
+ from cpu6502 import Cpu6502
prg = open(prgPath, "rb").read()
load = prg[0] | (prg[1] << 8)
diff --git a/examples/spacetaxi/assets/dumpScenes.py b/examples/spacetaxi/assets/dumpScenes.py
new file mode 100644
index 0000000..77a32be
--- /dev/null
+++ b/examples/spacetaxi/assets/dumpScenes.py
@@ -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 [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()
diff --git a/examples/spacetaxi/assets/levels/format.md b/examples/spacetaxi/assets/levels/format.md
index a883d27..ea60e44 100644
--- a/examples/spacetaxi/assets/levels/format.md
+++ b/examples/spacetaxi/assets/levels/format.md
@@ -82,7 +82,7 @@ without changes.
3. Author each level layout in a text editor or grid tool, save as a
.dat per this spec. The helper `examples/spacetaxi/mkstlevel`
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.
## Naming
diff --git a/examples/spacetaxi/assets/romToLevel.py b/examples/spacetaxi/assets/romToLevel.py
new file mode 100644
index 0000000..7c101c9
--- /dev/null
+++ b/examples/spacetaxi/assets/romToLevel.py
@@ -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 # all 24 levels
+# romToLevel.py # one specific
+#
+# Writes:
+# /level01.dat ... level24.dat (scene 0 = "A" = level01)
+# /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.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 [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()
diff --git a/examples/spacetaxi/assets/sidCapture.py b/examples/spacetaxi/assets/sidCapture.py
index 8c028e4..76befb8 100644
--- a/examples/spacetaxi/assets/sidCapture.py
+++ b/examples/spacetaxi/assets/sidCapture.py
@@ -13,8 +13,8 @@ extracted the speech:
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).
"""
-import json, sys
-sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi")
+import json, os, sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cpu6502 import Cpu6502
RAM = bytearray(open(sys.argv[1], "rb").read()[:65536])
diff --git a/examples/spacetaxi/stLevelFile.c b/examples/spacetaxi/stLevelFile.c
index dc59814..5ca896e 100644
--- a/examples/spacetaxi/stLevelFile.c
+++ b/examples/spacetaxi/stLevelFile.c
@@ -1,7 +1,7 @@
// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared
// 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"
// name[22] $7D78 level name in screen codes
// header[9] $7D00 VIC colour block
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..06a1f75
--- /dev/null
+++ b/install.sh
@@ -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] [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
diff --git a/installer/components/amigaGcc.sh b/installer/components/amigaGcc.sh
new file mode 100644
index 0000000..f27a6ea
--- /dev/null
+++ b/installer/components/amigaGcc.sh
@@ -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"
+}
diff --git a/installer/components/cadius.sh b/installer/components/cadius.sh
new file mode 100644
index 0000000..72cb26d
--- /dev/null
+++ b/installer/components/cadius.sh
@@ -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"
+}
diff --git a/installer/components/cwsdpmi.sh b/installer/components/cwsdpmi.sh
new file mode 100644
index 0000000..10fde39
--- /dev/null
+++ b/installer/components/cwsdpmi.sh
@@ -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"
+}
diff --git a/installer/components/djgpp.sh b/installer/components/djgpp.sh
new file mode 100644
index 0000000..8bb323e
--- /dev/null
+++ b/installer/components/djgpp.sh
@@ -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"
+}
diff --git a/installer/components/dosbox.sh b/installer/components/dosbox.sh
new file mode 100644
index 0000000..10f6681
--- /dev/null
+++ b/installer/components/dosbox.sh
@@ -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"
+}
diff --git a/installer/components/elf2x68k.sh b/installer/components/elf2x68k.sh
new file mode 100644
index 0000000..2878070
--- /dev/null
+++ b/installer/components/elf2x68k.sh
@@ -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"
+}
diff --git a/installer/components/emutos.sh b/installer/components/emutos.sh
new file mode 100644
index 0000000..1318d80
--- /dev/null
+++ b/installer/components/emutos.sh
@@ -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"
+}
diff --git a/installer/components/fsuae.sh b/installer/components/fsuae.sh
new file mode 100644
index 0000000..41a0a07
--- /dev/null
+++ b/installer/components/fsuae.sh
@@ -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"
+}
diff --git a/installer/components/hatari.sh b/installer/components/hatari.sh
new file mode 100644
index 0000000..3273233
--- /dev/null
+++ b/installer/components/hatari.sh
@@ -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"
+}
diff --git a/installer/components/headless.sh b/installer/components/headless.sh
new file mode 100644
index 0000000..b2b2bda
--- /dev/null
+++ b/installer/components/headless.sh
@@ -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() {
+ :
+}
diff --git a/installer/components/host.sh b/installer/components/host.sh
new file mode 100644
index 0000000..e56795d
--- /dev/null
+++ b/installer/components/host.sh
@@ -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() {
+ :
+}
diff --git a/installer/components/iigsFirmware.sh b/installer/components/iigsFirmware.sh
new file mode 100644
index 0000000..bb0a9e8
--- /dev/null
+++ b/installer/components/iigsFirmware.sh
@@ -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"
+}
diff --git a/installer/components/kickstart.sh b/installer/components/kickstart.sh
new file mode 100644
index 0000000..2956fca
--- /dev/null
+++ b/installer/components/kickstart.sh
@@ -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"
+}
diff --git a/installer/components/libxmp.sh b/installer/components/libxmp.sh
new file mode 100644
index 0000000..cf9ad0f
--- /dev/null
+++ b/installer/components/libxmp.sh
@@ -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"
+}
diff --git a/installer/components/llvm816.sh b/installer/components/llvm816.sh
new file mode 100644
index 0000000..d3ff5b5
--- /dev/null
+++ b/installer/components/llvm816.sh
@@ -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"
+}
diff --git a/installer/components/mame.sh b/installer/components/mame.sh
new file mode 100644
index 0000000..cbe277f
--- /dev/null
+++ b/installer/components/mame.sh
@@ -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"
+}
diff --git a/installer/components/merlin32.sh b/installer/components/merlin32.sh
new file mode 100644
index 0000000..44ce4cf
--- /dev/null
+++ b/installer/components/merlin32.sh
@@ -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"
+}
diff --git a/installer/components/mintGcc.sh b/installer/components/mintGcc.sh
new file mode 100644
index 0000000..939eec9
--- /dev/null
+++ b/installer/components/mintGcc.sh
@@ -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"
+}
diff --git a/installer/components/nasm.sh b/installer/components/nasm.sh
new file mode 100644
index 0000000..a14d369
--- /dev/null
+++ b/installer/components/nasm.sh
@@ -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)"
+}
diff --git a/installer/components/ntp.sh b/installer/components/ntp.sh
new file mode 100644
index 0000000..42f3172
--- /dev/null
+++ b/installer/components/ntp.sh
@@ -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"
+}
diff --git a/installer/components/ptplayer.sh b/installer/components/ptplayer.sh
new file mode 100644
index 0000000..bbf3003
--- /dev/null
+++ b/installer/components/ptplayer.sh
@@ -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"
+}
diff --git a/installer/components/staxiData.sh b/installer/components/staxiData.sh
new file mode 100644
index 0000000..a80ee0c
--- /dev/null
+++ b/installer/components/staxiData.sh
@@ -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() {
+ :
+}
diff --git a/installer/components/x68kCgrom.sh b/installer/components/x68kCgrom.sh
new file mode 100644
index 0000000..1606cdc
--- /dev/null
+++ b/installer/components/x68kCgrom.sh
@@ -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() {
+ :
+}
diff --git a/installer/components/x68kFirmware.sh b/installer/components/x68kFirmware.sh
new file mode 100644
index 0000000..5496f9e
--- /dev/null
+++ b/installer/components/x68kFirmware.sh
@@ -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"
+}
diff --git a/installer/distroPackages.txt b/installer/distroPackages.txt
new file mode 100644
index 0000000..7e8d224
--- /dev/null
+++ b/installer/distroPackages.txt
@@ -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
diff --git a/installer/files.sha1 b/installer/files.sha1
new file mode 100644
index 0000000..65a311e
--- /dev/null
+++ b/installer/files.sha1
@@ -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)
diff --git a/installer/lib/bundle.sh b/installer/lib/bundle.sh
new file mode 100644
index 0000000..422caea
--- /dev/null
+++ b/installer/lib/bundle.sh
@@ -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"
+}
diff --git a/installer/lib/common.sh b/installer/lib/common.sh
new file mode 100644
index 0000000..5e0f350
--- /dev/null
+++ b/installer/lib/common.sh
@@ -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/ 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// scratch build trees, deleted after install
+# toolchains/opt// 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"
+}
diff --git a/installer/lib/config.sh b/installer/lib/config.sh
new file mode 100644
index 0000000..321c673
--- /dev/null
+++ b/installer/lib/config.sh
@@ -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"
+}
diff --git a/installer/lib/distro.sh b/installer/lib/distro.sh
new file mode 100644
index 0000000..ec73e51
--- /dev/null
+++ b/installer/lib/distro.sh
@@ -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
+}
diff --git a/patches/mame-0.264-apple2gs-scc-clock.patch b/installer/patches/mame/0001-apple2gs-scc-clock.patch
similarity index 85%
rename from patches/mame-0.264-apple2gs-scc-clock.patch
rename to installer/patches/mame/0001-apple2gs-scc-clock.patch
index bc6ef88..1dd6d3f 100644
--- a/patches/mame-0.264-apple2gs-scc-clock.patch
+++ b/installer/patches/mame/0001-apple2gs-scc-clock.patch
@@ -1,5 +1,5 @@
---- /tmp/apple2gs.cpp.orig 2026-08-13 15:22:16.445448309 -0500
-+++ toolchains/cache/mame-mame0264/src/mame/apple/apple2gs.cpp 2026-08-13 15:22:16.497448131 -0500
+--- a/src/mame/apple/apple2gs.cpp
++++ b/src/mame/apple/apple2gs.cpp
@@ -3839,8 +3839,13 @@
ADDRESS_MAP_BANK(config, A2GS_C300_TAG).set_map(&apple2gs_state::c300bank_map).set_options(ENDIANNESS_LITTLE, 8, 32, 0x100);
diff --git a/installer/patches/mame/0002-z80scc-rx-fifo.patch b/installer/patches/mame/0002-z80scc-rx-fifo.patch
new file mode 100644
index 0000000..abc2cda
--- /dev/null
+++ b/installer/patches/mame/0002-z80scc-rx-fifo.patch
@@ -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
diff --git a/patches/mame-0.264-x68k-crtc-timing.patch b/installer/patches/mame/0003-x68k-crtc-timing.patch
similarity index 97%
rename from patches/mame-0.264-x68k-crtc-timing.patch
rename to installer/patches/mame/0003-x68k-crtc-timing.patch
index 6ed8789..e110e83 100644
--- a/patches/mame-0.264-x68k-crtc-timing.patch
+++ b/installer/patches/mame/0003-x68k-crtc-timing.patch
@@ -1,5 +1,5 @@
---- a/src/mame/sharp/x68k_crtc.h 2026-08-04 16:07:13.182675756 -0500
-+++ b/src/mame/sharp/x68k_crtc.h 2026-08-04 16:32:58.889623301 -0500
+--- a/src/mame/sharp/x68k_crtc.h
++++ b/src/mame/sharp/x68k_crtc.h
@@ -61,7 +61,7 @@
private:
// internal helpers
@@ -30,8 +30,8 @@
};
class vinas_device : public x68k_crtc_device
---- a/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:07:13.181675761 -0500
-+++ b/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:32:58.889623301 -0500
+--- a/src/mame/sharp/x68k_crtc.cpp
++++ b/src/mame/sharp/x68k_crtc.cpp
@@ -25,6 +25,8 @@
, m_clock_69m(0)
, m_clock_50m(0)
diff --git a/patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch b/installer/patches/mame/0004-x68k-waitstates-rs232-mouse.patch
similarity index 66%
rename from patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch
rename to installer/patches/mame/0004-x68k-waitstates-rs232-mouse.patch
index 43f019e..15995e9 100644
--- a/patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch
+++ b/installer/patches/mame/0004-x68k-waitstates-rs232-mouse.patch
@@ -1,19 +1,108 @@
-From ab6b6fc9e7971b367ec54d565cf10f04e71109ec Mon Sep 17 00:00:00 2001
-From: Patrick Mackinlay
-Date: Tue, 14 Jan 2025 13:53:33 +0700
-Subject: [PATCH] x68k: add serial mouse
-
----
- src/mame/sharp/x68k.cpp | 169 ++++------------------------------
- src/mame/sharp/x68k.h | 24 +----
- src/mame/sharp/x68k_mouse.cpp | 103 +++++++++++++++++++++
- 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
-index 8e5def0a501a6..c6787eeef393d 100644
+--- a/src/mame/sharp/x68k.h
++++ b/src/mame/sharp/x68k.h
+@@ -20,7 +20,7 @@
+ #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 m_pcgpalette;
+ required_device m_mfpdev;
+ required_device m_rtc;
+- required_device m_scc;
++ required_device m_scc;
+ required_device m_ym2151;
+ required_device m_ppi;
+ required_device m_screen;
+@@ -111,9 +108,6 @@
+ required_device_array m_adpcm_out;
+
+ required_ioport m_options;
+- required_ioport m_mouse1;
+- required_ioport m_mouse2;
+- required_ioport m_mouse3;
+
+ output_finder<4> m_eject_drv_out;
+ 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 void irq4_line(int state);
+ template 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
+++ b/src/mame/sharp/x68k.cpp
@@ -119,6 +119,7 @@
@@ -24,7 +113,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
#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()
{
uint32_t rate = adpcm_div[m_adpcm.rate];
-@@ -833,7 +713,7 @@ void x68k_state::update_ipl()
+@@ -837,7 +717,7 @@
new_ipl = 7;
else if (m_mfp_int)
new_ipl = 6;
@@ -161,7 +250,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
new_ipl = 5;
else if (m_exp_irq4[0] || m_exp_irq4[1])
new_ipl = 4;
-@@ -946,18 +826,6 @@ uint8_t x68k_state::iack4()
+@@ -950,18 +830,6 @@
return 0x18; // spurious interrupt
}
@@ -180,7 +269,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
void x68k_state::cpu_space_map(address_map &map)
{
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(0xfffff7, 0xfffff7).r(m_hd63450, FUNC(hd63450_device::iack));
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(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(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));
@@ -198,7 +370,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
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(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_CONFSETTING( 0x00, DEF_STR( Off ))
PORT_CONFSETTING( 0x02, DEF_STR( On ))
@@ -215,18 +387,34 @@ index 8e5def0a501a6..c6787eeef393d 100644
INPUT_PORTS_END
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());
space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer());
- // start mouse timer
- m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now
- 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
m_led_timer->adjust(attotime::zero, 0, attotime::from_msec(400));
-
-@@ -1173,11 +1027,11 @@ void x68k_state::machine_start()
+@@ -1177,11 +1117,11 @@
m_dmac_int = false;
m_mfp_int = false;
@@ -239,7 +427,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
m_current_ipl = 0;
m_adpcm.rate = 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
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_fdc_tc = timer_alloc(FUNC(x68ksupr_state::floppy_tc_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);
}
@@ -259,7 +447,7 @@ index 8e5def0a501a6..c6787eeef393d 100644
void x68k_state::x68000_base(machine_config &config)
{
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_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"));
+ 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));
-diff --git a/src/mame/sharp/x68k.h b/src/mame/sharp/x68k.h
-index 2c1dc8552af67..b5c40b213b6bb 100644
---- a/src/mame/sharp/x68k.h
-+++ b/src/mame/sharp/x68k.h
-@@ -20,7 +20,7 @@
- #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 @@ class x68k_state : public driver_device
- , 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 @@ class x68k_state : public driver_device
- required_device m_pcgpalette;
- required_device m_mfpdev;
- required_device m_rtc;
-- required_device m_scc;
-+ required_device m_scc;
- required_device m_ym2151;
- required_device m_ppi;
- required_device m_screen;
-@@ -111,9 +108,6 @@ class x68k_state : public driver_device
- required_device_array m_adpcm_out;
-
- required_ioport m_options;
-- required_ioport m_mouse1;
-- required_ioport m_mouse2;
-- required_ioport m_mouse3;
-
- 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 void irq4_line(int state);
- template 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
++++ 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
--- /dev/null
+++ b/src/mame/sharp/x68k_mouse.cpp
@@ -0,0 +1,103 @@
@@ -467,45 +620,3 @@ index 0000000000000..f30cfc7d25d54
+{
+ 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
diff --git a/installer/patches/mame/README.md b/installer/patches/mame/README.md
new file mode 100644
index 0000000..ca7733a
--- /dev/null
+++ b/installer/patches/mame/README.md
@@ -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/.lua`.
diff --git a/patches/x68k-boot-check.lua b/installer/patches/mame/lua/x68k-boot-check.lua
similarity index 100%
rename from patches/x68k-boot-check.lua
rename to installer/patches/mame/lua/x68k-boot-check.lua
diff --git a/patches/x68k-crtc-probe.lua b/installer/patches/mame/lua/x68k-crtc-probe.lua
similarity index 100%
rename from patches/x68k-crtc-probe.lua
rename to installer/patches/mame/lua/x68k-crtc-probe.lua
diff --git a/patches/x68k-wait-probe.lua b/installer/patches/mame/lua/x68k-wait-probe.lua
similarity index 100%
rename from patches/x68k-wait-probe.lua
rename to installer/patches/mame/lua/x68k-wait-probe.lua
diff --git a/joey-run b/joey-run
new file mode 100755
index 0000000..8f2400b
--- /dev/null
+++ b/joey-run
@@ -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" <= $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
diff --git a/make/amiga.mk b/make/amiga.mk
index aa3b68d..80b1b6c 100644
--- a/make/amiga.mk
+++ b/make/amiga.mk
@@ -2,17 +2,21 @@
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
BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib
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
-# 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
# 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)
# OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses
# 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
# 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
-# title from scene 24, via stuff/spacetaxi/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
+# title from scene 24, via examples/spacetaxi/assets/romToLevel.py.
STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat
STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN)
@@ -165,13 +166,13 @@ ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py
ASSETBAKE_TARGET := amiga
# 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.
DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.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
@mkdir -p $(dir $@)
diff --git a/make/atarist.mk b/make/atarist.mk
index 4622ce3..b886099 100644
--- a/make/atarist.mk
+++ b/make/atarist.mk
@@ -2,20 +2,24 @@
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
+ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim
BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin
# 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).
-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 :=
# libxmp-lite shared with the DOS port. Built as a static archive that
# provides the Protracker pattern decoder for the ST audio HAL; the
# 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_OBJDIR := $(BUILD)/obj/libxmp-lite
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
# 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
-# title from scene 24, via stuff/spacetaxi/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
+# title from scene 24, via examples/spacetaxi/assets/romToLevel.py.
STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \
$(STAXI_GEN_DIR)/levels/title.dat
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
.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
@mkdir -p $(dir $@)
@@ -194,7 +195,7 @@ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c
@mkdir -p $(dir $@)
$(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 $@)
$(ST_CC) $(CFLAGS) -c $< -o $@
diff --git a/make/common.mk b/make/common.mk
index fe45e5a..9ff1baf 100644
--- a/make/common.mk
+++ b/make/common.mk
@@ -3,6 +3,30 @@
# Source this only via the per-target make file (make/iigs.mk etc).
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
# Machine-specific code lives in src// (iigs, amiga, atarist, dos);
# code shared across machines of one CPU lives in a processor folder (src/m68k).
diff --git a/make/dos.mk b/make/dos.mk
index abd9322..96d9cb0 100644
--- a/make/dos.mk
+++ b/make/dos.mk
@@ -2,6 +2,11 @@
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
BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib
@@ -12,15 +17,14 @@ BINDIR := $(BUILD)/bin
# DOSBox's per-instruction-charged core (perf-hunt-2 rank 2).
# 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).
-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
LDFLAGS :=
# libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends
-# replayer + SMIX SFX overlay. Staged at toolchains/audio/libxmp-lite/
-# (currently checked in; install.sh wiring is a follow-up). Built as
-# a static archive that the DOS audio HAL links against.
-LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite
+# replayer + SMIX SFX overlay. Installed by ./install.sh
+# (LIBXMP_DIR, from config.mk). Built as a static archive that the DOS
+# audio HAL links against.
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
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
# 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
# decompressor through a Python 6502 emulator and writes STL2 .dat
# 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
# custom charset (extracted into tbank0.png), so a hand-authored tile
# 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_GEN_DIR)/levels/title.dat
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
.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
@mkdir -p $(dir $@)
diff --git a/make/iigs.mk b/make/iigs.mk
index 1f09d90..c0ef10c 100644
--- a/make/iigs.mk
+++ b/make/iigs.mk
@@ -1,6 +1,6 @@
# 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.
# Hand-rolled asm lives in src/iigs/*.s (llvm-mc GAS syntax);
# bank placement is handled by -ffunction-sections + link816
@@ -9,8 +9,8 @@
# STATUS: the launchable-binary pipeline (multi-segment OMF emission +
# disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
# compile-checks every library + example translation unit, and
-# `iigs-clang-smoke` validates the full clang -> llvm-mc -> link816 ->
-# MAME path end to end. Per-example binary targets build via
+# `iigs-clang-smoke` (not part of the default goal: it needs MAME)
+# 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
# in progress.
@@ -22,8 +22,11 @@ BINDIR := $(BUILD)/bin
OBJDIR := $(BUILD)/obj
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
-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
RT_INC := $(LLVM816_ROOT)/runtime/include
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
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
-# binary, then bake it into a dead-strippable GAS data object (gNtpPlayerBytes
-# / _len, which audio_full.c externs). bin-to-s.sh is the clang-era replacement
-# for the deleted bin-to-asm.sh. Only the AUDIO binary pulls it in; link816
-# dead-strips it everywhere else.
-NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
+# binary that ships next to the program; jlpAudioInit loads "ntpplayer" from
+# disk at run time (see the $(BINDIR)/AUDIO rule).
+NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
-NTP_ASM := $(BUILD)/audio/ntpdata.s
-IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
+IIGS_MERLIN := $(IIGS_AS)
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean
-# Default: compile-check the library + run the end-to-end smoke test.
-all iigs: iigs-lib iigs-clang-smoke
+# Default: build (compile-check) the library. The MAME smoke test is
+# `iigs-clang-smoke`; disks and examples are `iigs-disk`.
+all iigs: iigs-lib
# Per-example launchable binaries. STAXI and AUDIO are INCLUDED: the
# 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)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO
-# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that run-iigs-mame.sh /
-# run-iigs.sh mount on flop4. Packs the launchable examples as ProDOS S16 apps
+# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that the gates, and
+# `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).
# 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
@@ -120,10 +121,18 @@ STAXI_IIGS_SPC := $(STAXICELS_SPR:.spr=.spc)
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)
# 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/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py
+ @mkdir -p $(dir $@)
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
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)
$(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
# 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
# joey.2mg's exact contents, so this must not rewrite it.
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)" \
$(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$(GAME)-data.2mg
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
# `-sl7 cffa2 -hard1 -hard2 build/iigs/bin/joey-all.2mg` and launch
# 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)" \
JOEY_DISK_SIZE="16MB" \
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
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,
# and assemble every .s with llvm-mc. This is the canonical "does the
# 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
# the ported asm computes correctly on emulated hardware.
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
# 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
# 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
-# rodata segment anyway). The binary no longer links $(NTP_ASM); $(NTP_BIN) is
-# still built so it can be shipped on the application disk next to the binary.
+# rodata segment anyway). $(NTP_BIN) is shipped on the application disk next
+# to the binary.
$(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AUDIO.d $(INCLUDES) -o $@ $(AUDIO_SRC) $(LIB)
diff --git a/make/support/atarist/include-shim/math.h b/make/support/atarist/include-shim/math.h
new file mode 100644
index 0000000..c56128d
--- /dev/null
+++ b/make/support/atarist/include-shim/math.h
@@ -0,0 +1,25 @@
+// Minimal math.h shim for the m68k-atari-mint toolchain (mintlib
+// doesn't ship 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
diff --git a/make/support/atarist/include-shim/math_shim.c b/make/support/atarist/include-shim/math_shim.c
new file mode 100644
index 0000000..cc49138
--- /dev/null
+++ b/make/support/atarist/include-shim/math_shim.c
@@ -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; }
diff --git a/make/support/dos/embed-dpmi.sh b/make/support/dos/embed-dpmi.sh
new file mode 100755
index 0000000..732725d
--- /dev/null
+++ b/make/support/dos/embed-dpmi.sh
@@ -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"
diff --git a/make/support/iigs/clang-build.sh b/make/support/iigs/clang-build.sh
new file mode 100755
index 0000000..f8b14b3
--- /dev/null
+++ b/make/support/iigs/clang-build.sh
@@ -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
diff --git a/make/support/iigs/clang-smoke.sh b/make/support/iigs/clang-smoke.sh
new file mode 100755
index 0000000..6c072bb
--- /dev/null
+++ b/make/support/iigs/clang-smoke.sh
@@ -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
+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"
diff --git a/make/x68000.mk b/make/x68000.mk
index 947cfd4..bbadb85 100644
--- a/make/x68000.mk
+++ b/make/x68000.mk
@@ -4,8 +4,8 @@
# generic backend -- no native primitives, no audio, no sprite codegen yet.
# 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
-# toolchains/x68000 by the tarball. m68k-xelf-gcc emits ELF; the driver's
+# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), installed
+# by ./install.sh (X68K_ROOT). m68k-xelf-gcc emits ELF; the driver's
# -Wl,-elf2x68k pass converts it to a Human68k .X.
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).
.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
BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin
-X68K_ROOT := $(REPO_DIR)/toolchains/x68000/m68k-xelf
X68K_CC := $(X68K_ROOT)/bin/m68k-xelf-gcc
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 \
-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)
LDFLAGS := -lm
@@ -73,7 +75,7 @@ LIB_OBJS := \
# 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,
# 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_OBJDIR := $(BUILD)/obj/libxmp-lite
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.
STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000
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_GEN_DIR)/levels/title.dat
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
# Human68k floppy: the unstripped binary is most of the volume on its
# own. The DATA tree next to it (levels + the precompiled cel bank) is
-# what scripts/run-x68000.sh copies onto the data floppy.
-$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(STAXI_ASSET_DSTS)
+# what joey-run copies onto the data floppy.
+$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(call staxiData,$(STAXI_ASSET_DSTS))
@mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
diff --git a/patches/mame-0.264-x68k-rs232.patch b/patches/mame-0.264-x68k-rs232.patch
deleted file mode 100644
index f98dc69..0000000
--- a/patches/mame-0.264-x68k-rs232.patch
+++ /dev/null
@@ -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);
diff --git a/patches/mame-0.264-x68k-waitstates.patch b/patches/mame-0.264-x68k-waitstates.patch
deleted file mode 100644
index 47d8de5..0000000
--- a/patches/mame-0.264-x68k-waitstates.patch
+++ /dev/null
@@ -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;
diff --git a/scripts/bench-amiga.sh b/scripts/bench-amiga.sh
index 56f6dc8..9b19787 100755
--- a/scripts/bench-amiga.sh
+++ b/scripts/bench-amiga.sh
@@ -11,25 +11,23 @@
# scripts/bench-amiga.sh # default 150s timeout
# 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.
set -euo pipefail
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
-support=$repo/toolchains/emulators/support
-kickstart=$support/kickstart.rom
+kickstart=$KICKSTART_ROM
timeout_s=${TIMEOUT:-150}
if [[ ! -f $bin_dir/Uber ]]; then
echo "Uber not built. Run 'make amiga' first." >&2
exit 1
fi
-if [[ ! -f $kickstart ]]; then
- echo "Kickstart ROM missing: $kickstart" >&2
- exit 1
-fi
work=$(mktemp -d -t joeylib-amiga-bench.XXXXXX)
mkdir -p "$work/s"
@@ -50,7 +48,7 @@ Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
- kill "$fs_pid" 2>/dev/null || true
+ joeyStop "$fs_pid"
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
rm -rf "$work"
@@ -58,7 +56,7 @@ cleanup() {
trap cleanup EXIT
echo "bench-amiga: running UBER headless (timeout ${timeout_s}s)..." >&2
-DISPLAY=$display fs-uae \
+DISPLAY=$display "$FSUAE" \
--amiga_model=A500 \
--fast_memory=2048 \
--hard_drive_0="$work" \
diff --git a/scripts/bench-atarist.sh b/scripts/bench-atarist.sh
index 98969dc..7ffdd2f 100755
--- a/scripts/bench-atarist.sh
+++ b/scripts/bench-atarist.sh
@@ -20,8 +20,11 @@
set -euo pipefail
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
-tos=$repo/toolchains/emulators/support/emutos-512k.img
+tos=$EMUTOS_IMG
log=$bin_dir/joeylog.txt
timeout_s=${TIMEOUT:-90}
@@ -41,14 +44,14 @@ Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
- kill "$hat_pid" 2>/dev/null || true
+ joeyStop "$hat_pid"
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT
echo "bench-atarist: running UBER headless (timeout ${timeout_s}s)..." >&2
-DISPLAY=$display hatari \
+DISPLAY=$display "$HATARI" \
--tos "$tos" \
--harddrive "$bin_dir" \
--gemdos-drive C \
diff --git a/scripts/bench-dos.sh b/scripts/bench-dos.sh
index 290f495..d43f6fd 100755
--- a/scripts/bench-dos.sh
+++ b/scripts/bench-dos.sh
@@ -6,7 +6,7 @@
# 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.
#
-# 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.
#
# scripts/bench-dos.sh # default 90s timeout
@@ -15,6 +15,8 @@
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed DOSBOX dosbox
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
log=$bin_dir/JOEYLOG.TXT
@@ -26,15 +28,17 @@ if [[ ! -f $bin_dir/UBER.EXE ]]; then
fi
rm -f "$log" "$bin_dir/joeylog.txt"
+work=$(mktemp -d -t joeylib-bench-dos.XXXXXX)
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
- kill "$dosbox_pid" 2>/dev/null || true
+ joeyStop "$dosbox_pid"
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
+ rm -rf "$work"
}
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
# not exist!" / "Illegal command") and the run sat at Z:\> until the
# timeout with no JOEYLOG.TXT.
-DISPLAY=$display dosbox \
- -conf "$conf" \
+XDG_CONFIG_HOME="$work/xdg" DISPLAY=$display "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
+ --conf "$conf" \
-c "MOUNT C \"$bin_dir\"" \
-c "C:" \
-c "UBER.EXE" \
- >/tmp/bench-dos-dosbox.log 2>&1 &
+ >"$work/dosbox.log" 2>&1 &
dosbox_pid=$!
waited=0
diff --git a/scripts/bench-iigs.sh b/scripts/bench-iigs.sh
index cb4d157..541eb39 100755
--- a/scripts/bench-iigs.sh
+++ b/scripts/bench-iigs.sh
@@ -15,23 +15,27 @@
# MAME_ROMPATH override ROM dir (default ~/.mame/roms)
# 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).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?bench-iigs.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+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
# `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
# default to the standard locations, so normal bench runs are unchanged.
data_disk="${BENCH_DATA_DISK:-$repo/build/iigs/bin/joey.2mg}"
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}"
[ -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
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 1200 mame apple2gs \
+ timeout 1200 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/bench-staxi-title.sh b/scripts/bench-staxi-title.sh
index 0bdc138..d1ba47f 100755
--- a/scripts/bench-staxi-title.sh
+++ b/scripts/bench-staxi-title.sh
@@ -25,12 +25,16 @@
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?bench-staxi-title.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+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
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+rompath="$IIGS_ROMPATH"
maxFrames="${STAXI_MAX_FRAMES:-15000}"
maxChanges="${STAXI_MAX_CHANGES:-80}"
label="${1:-run}"
@@ -148,7 +152,7 @@ LUA
echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2
cd "$work"
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" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/bench-x68000.sh b/scripts/bench-x68000.sh
index 0bcfc6d..a9a75b0 100755
--- a/scripts/bench-x68000.sh
+++ b/scripts/bench-x68000.sh
@@ -22,13 +22,15 @@
# larger than the gate's because a real measurement window runs each operation
# for hundreds of frames rather than one.
#
-# X68K_SCRATCH= bash scripts/bench-x68000.sh
+# bash scripts/bench-x68000.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
-MAME="$repo/toolchains/cache/mame-mame0264/x68k"
-TEMPLATE="$SP/x68mame/HUMAN302.XDF"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed X68K_DIR x68kFirmware
+joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+TEMPLATE="$X68K_DIR/HUMAN302.XDF"
FRAMES="${X68K_BENCH_FRAMES:-120000}"
WALL="${X68K_BENCH_WALL:-7200}"
# 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.
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)
trap 'rm -rf "$work"' EXIT
@@ -48,7 +52,7 @@ trap 'rm -rf "$work"' EXIT
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_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/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \
-o "$work/UBER.X" || exit 1
@@ -168,7 +172,7 @@ end)
LUA
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 \
-autoboot_script "$work/bench.lua" &1 | grep '^BENCH')
echo "$runLog"
diff --git a/scripts/captureDos.sh b/scripts/captureDos.sh
deleted file mode 100755
index 9c98a24..0000000
--- a/scripts/captureDos.sh
+++ /dev/null
@@ -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 [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.
diff --git a/scripts/check-blank.sh b/scripts/check-blank.sh
index 77c6a5c..f99cca9 100755
--- a/scripts/check-blank.sh
+++ b/scripts/check-blank.sh
@@ -2,13 +2,17 @@
# 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
-# 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
# compile (catches a new platform-#if dispatch point that forgot a BLANK
# 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
# chunky port really does get everything except the platform services for
# 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
# (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)
cd "$repo"
+srcs=(src/core/*.c src/generic/*.c src/blank/blank.c)
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"
if ! "$cc" -DJOEYLIB_PLATFORM_BLANK "${inc[@]}" -Wall -c "$f" -o "$o" 2>"$work/err"; then
echo "check-blank: COMPILE FAIL: $f" >&2
@@ -54,4 +59,26 @@ if [ -n "$missing" ]; then
exit 1
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)"
diff --git a/scripts/diag-staxi-screen.sh b/scripts/diag-staxi-screen.sh
index 0e63232..2d2f16b 100755
--- a/scripts/diag-staxi-screen.sh
+++ b/scripts/diag-staxi-screen.sh
@@ -7,12 +7,16 @@
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?diag-staxi-screen.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+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
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+rompath="$IIGS_ROMPATH"
dumpFrame="${STAXI_DUMP_FRAME:-9000}"
work=$(mktemp -d -t joeylib-staxidiag.XXXXXX)
@@ -120,7 +124,7 @@ LUA
echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 900 mame apple2gs \
+ timeout 900 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/dosbox-386sx16.conf b/scripts/dosbox-386sx16.conf
index 95bbf27..e2d79de 100644
--- a/scripts/dosbox-386sx16.conf
+++ b/scripts/dosbox-386sx16.conf
@@ -1,5 +1,5 @@
-# DOSBox config: simulate an Intel 386SX-16 (1988), the slowest 386
-# desktop CPU JoeyLib could realistically be run on. Use this floor
+# DOSBox Staging config: simulate an Intel 386SX-16 (1988), the slowest
+# 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
# the 386 stack rather than coasting on host CPU.
#
@@ -14,15 +14,19 @@
# defeat slow-CPU simulation).
# cputype = 386 386 instruction set (no 486 BSWAP /
# CMPXCHG, no Pentium MMX).
-# cycles = fixed 2200 community-standard approximation for
-# 386SX-16 throughput in DOSBox.
-# DOSBox-Staging deprecates this in favor
-# of cpu_cycles, but still accepts it.
-# Vanilla DOSBox and DOSBox-X only know
-# the old key, so 'cycles' stays for
-# cross-fork portability.
+# cpu_cycles, cpu_cycles_protected = 2200
+# community-standard approximation for
+# 386SX-16 throughput. Staging splits real
+# and protected mode; JoeyLib's DJGPP
+# programs run in protected mode, so both
+# are pinned. The window title shows the
+# rate in force (" - 2200 cycles/ms").
+#
+# Launch with --noprimaryconf --nolocalconf so nothing from the user's
+# own DOSBox configuration leaks into a run.
[cpu]
-core = normal
-cputype = 386
-cycles = fixed 2200
+core = normal
+cputype = 386
+cpu_cycles = 2200
+cpu_cycles_protected = 2200
diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh
new file mode 100644
index 0000000..7f702f4
--- /dev/null
+++ b/scripts/lib/joeyEnv.sh
@@ -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
+}
diff --git a/scripts/make-agi-iigs-disk.sh b/scripts/make-agi-iigs-disk.sh
index 85eafed..a24b672 100755
--- a/scripts/make-agi-iigs-disk.sh
+++ b/scripts/make-agi-iigs-disk.sh
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# 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
-# it as the CFFA2 card's second partition (-hard2); GS/OS boots from the
-# first partition and auto-mounts this one.
+# IIgs AGI interpreter plus one AGI v2 game's resources. The
+# iigs-game-disk-agi make target merges it into the bootable AGI.2mg that
+# joey-run starts (--ram 8M).
#
# 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
@@ -11,12 +11,13 @@
#
# Usage: scripts/make-agi-iigs-disk.sh [game-name]
# 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
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?make-agi-iigs-disk.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed LLVM816_ROOT toolchain:iigs
+joeyNeed CADIUS cadius
BINDIR="${BINDIR:-$repo/build/iigs/bin}"
out=${1:?usage: make-agi-iigs-disk.sh [game]}
diff --git a/scripts/make-iigs-boot-disk.sh b/scripts/make-iigs-boot-disk.sh
index e80a7b7..3d2edeb 100755
--- a/scripts/make-iigs-boot-disk.sh
+++ b/scripts/make-iigs-boot-disk.sh
@@ -19,7 +19,7 @@
# Usage: scripts/make-iigs-boot-disk.sh [volume]
# EXAMPLE uppercase binary name as it appears on the data disk
# 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
out=${1:?usage: make-iigs-boot-disk.sh [volume]}
@@ -27,9 +27,12 @@ prog=${2:?usage: make-iigs-boot-disk.sh [volume]}
vol=${3:-JOEYLIB}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?make-iigs-boot-disk.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
-sysDisk="$repo/toolchains/emulators/support/gsos-system.po"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed LLVM816_ROOT toolchain:iigs
+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; }
[ -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.
"$CADIUS" DELETEFILE "$out" "/$sysVol/System/Start" >/dev/null
-"$repo/toolchains/iigs/clang-build.sh" \
+"$IIGS_CLANG_BUILD" \
-DJOEY_START_PATH="\"/$vol/$prog\"" \
-o "$work/Start#B30000" \
"$repo/tools/iigsStart/iigsStart.c" \
diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh
index 5b14bcd..ca86f57 100755
--- a/scripts/make-iigs-disk.sh
+++ b/scripts/make-iigs-disk.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# 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
# 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 ...".
#
# 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
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed LLVM816_ROOT toolchain:iigs
+joeyNeed CADIUS cadius
BINDIR="${BINDIR:-$repo/build/iigs/bin}"
# The NTP replayer. DEFAULTED, not optional: without it on the volume every
# IIgs example is silent except the ROM boot beep, because jlpAudioInit
diff --git a/scripts/make-iigs-game-disk.sh b/scripts/make-iigs-game-disk.sh
index 9907b5e..94d24b9 100755
--- a/scripts/make-iigs-game-disk.sh
+++ b/scripts/make-iigs-game-disk.sh
@@ -40,7 +40,7 @@
# drive is 800KB GCR and REJECTS a 1440KB image, so anything
# larger has to boot from a CFFA2 card:
# -sl7 cffa2 -hard1 .
-# Requires: toolchains/env.sh sourced.
+# Requires: ./install.sh install iigs (the toolchain and the GS/OS disk).
set -euo pipefail
out=${1:?usage: make-iigs-game-disk.sh [volume] [data.2mg]}
@@ -48,10 +48,13 @@ prog=${2:?usage: make-iigs-game-disk.sh [volume] [data.2mg]}
vol=${3:-$prog}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-: "${LLVM816_ROOT:?make-iigs-game-disk.sh: source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+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}
-sysDisk="$repo/toolchains/emulators/support/gsos-system.po"
+sysDisk=$GSOS_SYSTEM_DISK
size=${JOEY_GAME_DISK_SIZE:-auto}
[ -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
# ----- the launcher -----
-"$repo/toolchains/iigs/clang-build.sh" \
+"$IIGS_CLANG_BUILD" \
-DJOEY_START_PATH="\"/$vol/$prog/$prog\"" \
-o "$work/Start#B30000" \
"$repo/tools/iigsStart/iigsStart.c" \
diff --git a/scripts/profile-x68000.sh b/scripts/profile-x68000.sh
index 354ea89..7310b7f 100755
--- a/scripts/profile-x68000.sh
+++ b/scripts/profile-x68000.sh
@@ -23,7 +23,7 @@
# example .X base name in build/x68000/bin, default staxi
# seconds emulated seconds to run, default 240
# 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/.X.elf).
set -uo pipefail
@@ -33,15 +33,17 @@ skip=${3:-130}
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
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
elf=$bin.elf
-x68=${X68K_DIR:-$repo/toolchains/x68mame}
+x68=$X68K_DIR
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 $elf ]] || { echo "profile-x68000: $elf missing (need symbols)" >&2; exit 2; }
diff --git a/scripts/run-agi.sh b/scripts/run-agi.sh
deleted file mode 100755
index 6ac3b06..0000000
--- a/scripts/run-agi.sh
+++ /dev/null
@@ -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 [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 [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 <&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 <&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 <&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
diff --git a/scripts/run-amiga-headless.sh b/scripts/run-amiga-headless.sh
deleted file mode 100755
index e164fea..0000000
--- a/scripts/run-amiga-headless.sh
+++ /dev/null
@@ -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
diff --git a/scripts/run-amiga.sh b/scripts/run-amiga.sh
deleted file mode 100755
index bf5ae2a..0000000
--- a/scripts/run-amiga.sh
+++ /dev/null
@@ -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 < 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 <&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
diff --git a/scripts/run-atarist.sh b/scripts/run-atarist.sh
deleted file mode 100755
index ef48f8c..0000000
--- a/scripts/run-atarist.sh
+++ /dev/null
@@ -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"
diff --git a/scripts/run-dos.sh b/scripts/run-dos.sh
deleted file mode 100755
index 9c3241f..0000000
--- a/scripts/run-dos.sh
+++ /dev/null
@@ -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//.EXE (with DATA/ alongside it)
-# DOSBox mounts bin/ as C: so DATA/foo.bin is reachable
-# via the relative path the binary opens.
-# 2. Old: bin/.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[@]}"
diff --git a/scripts/run-iigs-mame.sh b/scripts/run-iigs-mame.sh
deleted file mode 100755
index 78e414b..0000000
--- a/scripts/run-iigs-mame.sh
+++ /dev/null
@@ -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" <= 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 </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
diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh
deleted file mode 100755
index 16e428a..0000000
--- a/scripts/run-iigs.sh
+++ /dev/null
@@ -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 </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" <= 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 </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" <= 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 < /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" <= 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 < /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 <' 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 "-
-# ". 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
diff --git a/scripts/run-x68000.sh b/scripts/run-x68000.sh
deleted file mode 100755
index 920a59e..0000000
--- a/scripts/run-x68000.sh
+++ /dev/null
@@ -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 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
diff --git a/scripts/verify-amiga-audio.sh b/scripts/verify-amiga-audio.sh
index 8bd0074..83e3873 100755
--- a/scripts/verify-amiga-audio.sh
+++ b/scripts/verify-amiga-audio.sh
@@ -40,14 +40,17 @@
# example binary name in build/amiga/bin, default Audio
# Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV
# 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/
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed FSUAE fsuae
+joeyNeed KICKSTART_ROM kickstart
prog=${1:-Audio}
binDir=$repo/build/amiga/bin
-kickstart=$repo/toolchains/emulators/support/kickstart.rom
+kickstart=$KICKSTART_ROM
grader=$repo/tools/gradeAudioPhases.py
pulseLib=$repo/scripts/pulseRecordLib.sh
label=verify-amiga-audio
@@ -57,7 +60,7 @@ label=verify-amiga-audio
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
# shellcheck source=scripts/pulseRecordLib.sh
. "$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; }
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; }
@@ -80,7 +83,7 @@ parecPid=""
cleanup() {
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
- [ -n "$fsPid" ] && kill "$fsPid" 2>/dev/null
+ joeyStop "$fsPid"
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
# Always unload the sink: leaving null sinks behind on the user's
# audio server is the one side effect this gate could have.
@@ -94,7 +97,7 @@ cleanup() {
}
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"
if [ -n "$progDir" ]; then
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.
DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \
- fs-uae \
+ "$FSUAE" \
--amiga_model=A500 \
--hard_drive_0="$work/hd" \
--hard_drive_0_label=JOEYLIB \
diff --git a/scripts/verify-amiga-input.sh b/scripts/verify-amiga-input.sh
index 3ddb7f9..254010c 100755
--- a/scripts/verify-amiga-input.sh
+++ b/scripts/verify-amiga-input.sh
@@ -10,12 +10,15 @@
#
# Usage: scripts/verify-amiga-input.sh
# 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
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
-kickstart=$repo/toolchains/emulators/support/kickstart.rom
+kickstart=$KICKSTART_ROM
target="192.168.1.10:6510"
@@ -25,7 +28,7 @@ target="192.168.1.10:6510"
work=$(mktemp -d -t joeylib-amiga-input.XXXXXX)
display_num=$((RANDOM % 500 + 300))
cleanup() {
- [ -n "${fsuae_pid:-}" ] && kill "$fsuae_pid" 2>/dev/null
+ joeyStop "${fsuae_pid:-}"
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work"
}
@@ -45,7 +48,7 @@ sleep 1
# XSendEvent routes are ignored by SDL. A fullscreen SDL window takes
# focus itself, after which plain XTEST typing (xdotool without
# --window) lands in the emulated Amiga.
-DISPLAY=":$display_num" fs-uae \
+DISPLAY=":$display_num" "$FSUAE" \
--amiga_model=A500 \
--fast_memory=2048 \
--kickstart_file="$kickstart" \
diff --git a/scripts/verify-atarist-audio.sh b/scripts/verify-atarist-audio.sh
index 16a0ac7..64dcc85 100755
--- a/scripts/verify-atarist-audio.sh
+++ b/scripts/verify-atarist-audio.sh
@@ -46,14 +46,17 @@
# example PRG base name in build/atarist/bin, default AUDIO
# Env: ST_AUDIO_TAPS how many held SPACE presses (default 6)
# 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/.PRG
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed HATARI hatari
+joeyNeed EMUTOS_IMG emutos
prog=${1:-AUDIO}
taps=${ST_AUDIO_TAPS:-6}
-tos=$repo/toolchains/emulators/support/emutos-512k.img
+tos=$EMUTOS_IMG
binDir=$repo/build/atarist/bin
# 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
# -- 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" \
--harddrive "$driveDir" \
--gemdos-drive C \
diff --git a/scripts/verify-atarist-input.sh b/scripts/verify-atarist-input.sh
index 828265e..4317566 100755
--- a/scripts/verify-atarist-input.sh
+++ b/scripts/verify-atarist-input.sh
@@ -19,8 +19,11 @@
set -uo pipefail
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
-tos=$repo/toolchains/emulators/support/emutos-512k.img
+tos=$EMUTOS_IMG
target="192.168.1.10:6510"
@@ -29,13 +32,13 @@ target="192.168.1.10:6510"
work=$(mktemp -d -t joeylib-st-input.XXXXXX)
cleanup() {
- [ -n "${hatari_pid:-}" ] && kill "$hatari_pid" 2>/dev/null
+ joeyStop "${hatari_pid:-}"
rm -rf "$work"
}
trap cleanup EXIT
cd "$work"
-SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy hatari \
+SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
--tos "$tos" \
--harddrive "$bin_dir" \
--gemdos-drive C \
diff --git a/scripts/verify-dos-audio.sh b/scripts/verify-dos-audio.sh
index e779d56..5e815fe 100755
--- a/scripts/verify-dos-audio.sh
+++ b/scripts/verify-dos-audio.sh
@@ -2,7 +2,7 @@
# 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
-# 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
# 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
@@ -28,7 +28,8 @@
# unambiguous. This also covers the SMIX overlay specifically -- SFX on
# 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
-# 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
# 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
# Env: DOS_AUDIO_KEEP set to keep the work dir and the WAV
# 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/.EXE
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed DOSBOX dosbox
prog=${1:-AUDIO}
binDir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
@@ -55,7 +58,7 @@ label=verify-dos-audio
# shellcheck source=scripts/pulseRecordLib.sh
. "$pulseLib"
[ -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; }
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; }
@@ -79,7 +82,7 @@ parecPid=""
cleanup() {
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
- [ -n "$dosPid" ] && kill "$dosPid" 2>/dev/null
+ joeyStop "$dosPid"
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
# Always unload the sink: a leaked null sink on the user's audio
# 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=$!
sleep 1
-dosArgs=(-conf "$conf")
-[ -n "${CYCLES:-}" ] && dosArgs+=(-set "cpu cycles=$CYCLES")
+dosArgs=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$conf")
+[ -n "${CYCLES:-}" ] && dosArgs+=(--set "cpu_cycles=$CYCLES" --set "cpu_cycles_protected=$CYCLES")
dosArgs+=(
-c "MOUNT C $mountDir"
-c "C:"
-c "$prog.EXE"
)
-DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
- dosbox "${dosArgs[@]}" > "$work/dosbox.log" 2>&1 &
+XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
+ "$DOSBOX" "${dosArgs[@]}" > "$work/dosbox.log" 2>&1 &
dosPid=$!
sleep 12
@@ -120,7 +123,7 @@ if ! kill -0 "$dosPid" 2>/dev/null; then
exit 1
fi
-win=$(DISPLAY="$display" xdotool search --name -- "DOSBox" | head -1)
+win=$(joeyFindWindow "$display" "$dosPid")
if [ -z "$win" ]; then
echo "$label: FAIL - no DOSBox window to send keys to" >&2
exit 1
@@ -166,13 +169,13 @@ parecPid=""
[ -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,
-# sfx ~1341) so ordinary mixing jitter cannot flip the verdict.
+# Thresholds sit at about half the measured levels (music ~1220, paused
+# ~17, sfx ~659) so ordinary mixing jitter cannot flip the verdict.
python3 "$grader" "$label" "$work/out.wav" \
- "music:1:5:1500:" \
- "paused:8:13::600" \
- "sfx:16:22:700:" \
- "resumed:27:32:1500:"
+ "music:1:5:600:" \
+ "paused:8:13::200" \
+ "sfx:16:22:330:" \
+ "resumed:27:32:600:"
status=$?
[ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null
exit $status
diff --git a/scripts/verify-dos-input.sh b/scripts/verify-dos-input.sh
index c1cc8fa..7797c23 100755
--- a/scripts/verify-dos-input.sh
+++ b/scripts/verify-dos-input.sh
@@ -8,11 +8,13 @@
# palette entries distinct colors precisely so this decode is exact.
#
# Usage: scripts/verify-dos-input.sh
-# Requires: dosbox (staging), Xvfb, xdotool, python3 + PIL;
+# Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, python3 + PIL;
# build/dos/bin/KEYS.EXE (make dos).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed DOSBOX dosbox
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
@@ -23,7 +25,7 @@ target="192.168.1.10:6510"
work=$(mktemp -d -t joeylib-dos-input.XXXXXX)
display_num=$((RANDOM % 500 + 300))
cleanup() {
- [ -n "${dosbox_pid:-}" ] && kill "$dosbox_pid" 2>/dev/null
+ joeyStop "${dosbox_pid:-}"
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work"
}
@@ -33,12 +35,12 @@ Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 &
xvfb_pid=$!
sleep 1
-DISPLAY=":$display_num" dosbox \
- -conf "$conf" \
- -set "capture capture_dir=$work" \
- -set "capture default_image_capture_formats=raw" \
- -c "C:" -c "KEYS.EXE" \
- "$bin_dir" >/dev/null 2>&1 &
+XDG_CONFIG_HOME="$work/xdg" DISPLAY=":$display_num" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
+ --conf "$conf" \
+ --set "capture_dir=$work" \
+ --set "default_image_capture_formats=raw" \
+ -c "MOUNT C $bin_dir" -c "C:" -c "KEYS.EXE" \
+ >"$work/dosbox.log" 2>&1 &
dosbox_pid=$!
# Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16.
@@ -46,7 +48,7 @@ sleep 12
# Staging titles its window " - cycles/ms"; it is the only
# client on this private display, so match any named window.
-win=$(DISPLAY=":$display_num" xdotool search --name -- "." | head -1)
+win=$(joeyFindWindow ":$display_num" "$dosbox_pid")
if [ -z "$win" ]; then
echo "verify-dos-input: FAIL - DOSBox window not found" >&2
exit 1
diff --git a/scripts/verify-iigs-all.sh b/scripts/verify-iigs-all.sh
index 6d27d73..3510fda 100755
--- a/scripts/verify-iigs-all.sh
+++ b/scripts/verify-iigs-all.sh
@@ -4,14 +4,14 @@
# with its per-example distinct-color threshold. Reports one line per example
# and exits nonzero if any example fails to render.
#
-# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built
+# Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg built
# (`make iigs-disk`); apple2gs ROM + gsos-system.po (see verify-iigs.sh).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-source "$repo/toolchains/env.sh" 2>/dev/null || true
-: "${LLVM816_ROOT:?source toolchains/env.sh first}"
-CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed LLVM816_ROOT toolchain:iigs
+joeyNeed CADIUS cadius
disk="$repo/build/iigs/bin/joey.2mg"
[ -f "$disk" ] || { echo "verify-iigs-all: $disk missing (run 'make iigs-disk')" >&2; exit 2; }
diff --git a/scripts/verify-iigs-audio.sh b/scripts/verify-iigs-audio.sh
index 851489c..05aee3e 100755
--- a/scripts/verify-iigs-audio.sh
+++ b/scripts/verify-iigs-audio.sh
@@ -41,15 +41,19 @@
# Usage: scripts/verify-iigs-audio.sh [example]
# example lowercase name, default "audio". A single-example
# disk is built for it (joey.2mg cannot hold them all).
-# Requires: toolchains/env.sh sourced; the example built under build/iigs/bin.
+# Requires: ./install.sh install iigs; the example built under build/iigs/bin.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
example=${1:-audio}
NAME=${example^^}
-sys_disk=$repo/toolchains/emulators/support/gsos-system.po
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+sys_disk=$GSOS_SYSTEM_DISK
+rompath="$IIGS_ROMPATH"
probeA="${MAME_PROBE_A:-5400}"
probeB="${MAME_PROBE_B:-9000}"
@@ -156,7 +160,7 @@ LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 400 mame apple2gs \
+ timeout 400 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/verify-iigs-game-disk.sh b/scripts/verify-iigs-game-disk.sh
index ef2f533..499d092 100755
--- a/scripts/verify-iigs-game-disk.sh
+++ b/scripts/verify-iigs-game-disk.sh
@@ -20,7 +20,7 @@
# GAME_VERIFY_MACHINE MAME machine, default apple2gs (ROM 3).
# Use apple2gsr1 to check a ROM 01 machine; these disks keep
# TS2 so they must boot on both.
-# Requires: toolchains/env.sh sourced.
+# Requires: ./install.sh install iigs.
set -euo pipefail
img=${1:?usage: verify-iigs-game-disk.sh [minDistinct] [ramsize]}
@@ -30,8 +30,10 @@ readFrame=${GAME_VERIFY_FRAMES:-5400}
machine=${GAME_VERIFY_MACHINE:-apple2gs}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-MAME="${MAME:-mame}"
-rompath="$HOME/.mame/roms;$repo/toolchains/emulators/support/mame-roms"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+rompath="$IIGS_ROMPATH"
name=$(basename "$img")
[ -f "$img" ] || { echo "verify-iigs-game-disk: no such image $img" >&2; exit 2; }
diff --git a/scripts/verify-iigs-input.sh b/scripts/verify-iigs-input.sh
index b87e300..4c5496b 100755
--- a/scripts/verify-iigs-input.sh
+++ b/scripts/verify-iigs-input.sh
@@ -9,17 +9,21 @@
# '.', and ':' all arrive through the typed-character queue.
#
# Usage: scripts/verify-iigs-input.sh
-# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built
+# Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg built
# with the current KEYS (make iigs-disk).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
target="192.168.1.10:6510"
-sys_disk=$repo/toolchains/emulators/support/gsos-system.po
+sys_disk=$GSOS_SYSTEM_DISK
data_disk=$repo/build/iigs/bin/joey.2mg
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+rompath="$IIGS_ROMPATH"
typeFrame="${MAME_TYPE_FRAME:-6600}"
readFrame="${MAME_READ_FRAME:-12000}"
@@ -89,7 +93,7 @@ LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 300 mame apple2gs \
+ timeout 300 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/verify-iigs-save.sh b/scripts/verify-iigs-save.sh
index 9df46bc..ac62f01 100755
--- a/scripts/verify-iigs-save.sh
+++ b/scripts/verify-iigs-save.sh
@@ -14,16 +14,19 @@
# isolates GS/OS Create: a green lamp then proves Create made the directory
# from scratch (not that a prestaged one was merely present).
#
-# Requires: toolchains/env.sh sourced; build/iigs/bin/SAVE + DRAW built; the
+# Requires: ./install.sh install iigs; build/iigs/bin/SAVE + DRAW built; the
# apple2gs ROM and gsos-system.po present (same as verify-iigs.sh).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-source "$repo/toolchains/env.sh" 2>/dev/null || true
-: "${LLVM816_ROOT:?source toolchains/env.sh first}"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed LLVM816_ROOT toolchain:iigs
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
-sys_disk=$repo/toolchains/emulators/support/gsos-system.po
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+sys_disk=$GSOS_SYSTEM_DISK
+rompath="$IIGS_ROMPATH"
readFrame="${MAME_READ_FRAME:-9000}"
[ -f "$repo/build/iigs/bin/SAVE" ] || { echo "verify-iigs-save: build/iigs/bin/SAVE missing (make iigs first)" >&2; exit 2; }
@@ -96,7 +99,7 @@ LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 300 mame apple2gs \
+ timeout 300 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/verify-iigs-serial.sh b/scripts/verify-iigs-serial.sh
index 2e00b29..945e412 100755
--- a/scripts/verify-iigs-serial.sh
+++ b/scripts/verify-iigs-serial.sh
@@ -17,7 +17,7 @@
# A1 missing -> the probe never ran / printer path is broken
#
# MAME CONNECTS OUT to the bitbanger socket, so the peer here listens first.
-# Needs the PATCHED apple2gs (patches/mame-0.264-apple2gs-scc-clock.patch):
+# Needs the PATCHED apple2gs (installer/patches/mame/0001-apple2gs-scc-clock.patch):
# stock 0.264 feeds the SCC BRG the wrong clock and pins RX at 230400, so no
# baud ever matches the null_modem.
#
@@ -25,14 +25,16 @@
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-MAME="$repo/toolchains/cache/mame-mame0264/apple2gs"
-sys_disk=$repo/toolchains/emulators/support/gsos-system.po
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
+sys_disk=$GSOS_SYSTEM_DISK
+rompath="$IIGS_ROMPATH"
PORT="${IIGS_SCC_PORT:-45231}"
readFrame="${MAME_READ_FRAME:-9000}"
WALL="${IIGS_SCC_WALL:-420}"
-[ -x "$MAME" ] || { echo "verify-iigs-serial: missing patched MAME at $MAME" >&2; exit 2; }
[ -f "$sys_disk" ] || { echo "verify-iigs-serial: missing $sys_disk" >&2; exit 2; }
[ -f "$repo/build/iigs/bin/SCCPROBE" ] || { echo "verify-iigs-serial: build SCCPROBE first" >&2; exit 2; }
diff --git a/scripts/verify-iigs.sh b/scripts/verify-iigs.sh
index 7538511..5a77c93 100755
--- a/scripts/verify-iigs.sh
+++ b/scripts/verify-iigs.sh
@@ -21,10 +21,14 @@
# legitimately render only ~3 distinct colors. An explicit
# second arg always overrides.
#
-# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built.
+# Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg built.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed IIGS_ROMPATH iigsFirmware
+joeyNeed GSOS_SYSTEM_DISK iigsFirmware
example=${1:-draw}
NAME=${example^^}
if [ -n "${2:-}" ]; then
@@ -36,9 +40,9 @@ else
esac
fi
-sys_disk=$repo/toolchains/emulators/support/gsos-system.po
+sys_disk=$GSOS_SYSTEM_DISK
data_disk=$repo/build/iigs/bin/joey.2mg
-rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
+rompath="$IIGS_ROMPATH"
readFrame="${MAME_READ_FRAME:-9000}"
for f in "$sys_disk" "$data_disk"; do
@@ -131,7 +135,7 @@ LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
- timeout 300 mame apple2gs \
+ timeout 300 "$MAME" apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
diff --git a/scripts/verify-x68000-audio.sh b/scripts/verify-x68000-audio.sh
index 6f4ce17..b50ac0b 100755
--- a/scripts/verify-x68000-audio.sh
+++ b/scripts/verify-x68000-audio.sh
@@ -28,7 +28,7 @@
# noise curve (all seventeen distinct table values) and speaks.
#
# Usage: scripts/verify-x68000-audio.sh [example] [seconds]
-# 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 (make -f make/x68000.mk), MAME with x68000.
set -uo pipefail
@@ -37,16 +37,15 @@ seconds=${2:-160}
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
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
-x68=${X68K_DIR:-$repo/toolchains/x68mame}
+x68=$X68K_DIR
template=$x68/HUMAN302.XDF
source=$repo/src/x68000/audio.c
-MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}"
-if [[ ! -x $MAME ]]; then
- MAME="$(command -v mame || true)"
-fi
-[[ -n ${MAME:-} && -x $MAME ]] || { echo "verify-x68000-audio: no MAME (set MAME=...)" >&2; exit 2; }
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; }
[[ -f $template ]] || { echo "verify-x68000-audio: missing $template" >&2; exit 2; }
diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh
index cb7dd27..841f7a5 100755
--- a/scripts/verify-x68000-golden.sh
+++ b/scripts/verify-x68000-golden.sh
@@ -15,13 +15,15 @@
# X68K_GOLDEN_FRAMES is now only a backstop: reaching it means the done-screen
# was never detected, which is a FAILURE, not a normal exit.
#
-# X68K_SCRATCH= bash scripts/verify-x68000-golden.sh
+# bash scripts/verify-x68000-golden.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
-MAME="$repo/toolchains/cache/mame-mame0264/x68k"
-TEMPLATE="$SP/x68mame/HUMAN302.XDF"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed X68K_DIR x68kFirmware
+joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+TEMPLATE="$X68K_DIR/HUMAN302.XDF"
GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
WALL="${X68K_GOLDEN_WALL:-7200}"
@@ -30,7 +32,9 @@ WALL="${X68K_GOLDEN_WALL:-7200}"
# the hashes are of the surface in RAM and are the same either way.
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-x68gold.XXXXXX)
trap 'rm -rf "$work"' EXIT
@@ -42,7 +46,7 @@ trap 'rm -rf "$work"' EXIT
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 -DUBER_FRAMES=1u \
-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/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \
-o "$work/UBER.X" || exit 1
@@ -162,7 +166,7 @@ end)
LUA
runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
- -rompath "$SP/x68mame/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \
+ -rompath "$X68K_DIR/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/gold.lua" &1 | grep '^GOLD')
echo "$runLog"
diff --git a/scripts/verify-x68000-input.sh b/scripts/verify-x68000-input.sh
index a8c8166..5be8ebf 100755
--- a/scripts/verify-x68000-input.sh
+++ b/scripts/verify-x68000-input.sh
@@ -11,18 +11,19 @@
# per pixel at $C00000, 512 words per row; the 320x200 stage is centred
# at origin (96, 156). Verification row = stage y 198 -> GVRAM y 354.
#
-# X68K_SCRATCH= bash scripts/verify-x68000-input.sh
+# bash scripts/verify-x68000-input.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
-MAME="$repo/toolchains/cache/mame-mame0264/x68k"
-TEMPLATE="$SP/x68mame/HUMAN302.XDF"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed X68K_DIR x68kFirmware
+joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+TEMPLATE="$X68K_DIR/HUMAN302.XDF"
WALL="${X68K_INPUT_WALL:-900}"
target="192.168.1.10:6510"
-[ -x "$MAME" ] || { echo "verify-x68000-input: missing patched MAME at $MAME" >&2; exit 2; }
[ -f "$TEMPLATE" ] || { echo "verify-x68000-input: missing $TEMPLATE" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-input: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
@@ -45,11 +46,24 @@ local frame = 0
local typedAt = 0
local done = false
--- Stage pixel (px, 198) -> GVRAM word at base + 2*px, value low byte.
-local rowBase = 0xC00000 + ((156 + 198) * 512 + 96) * 2
+-- Stage pixel (px, y) -> its 4-bit colour index. The port presents to the
+-- TEXT plane (src/x68000/hal.c X68K_PRESENT_TEXT): four bit planes 128 KB
+-- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage
+-- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at
+-- 0xC00000, which this gate used to read.)
+local function textPix(px, y)
+ local sx = 96 + px
+ local addr = 0xE00000 + (156 + y) * 128 + (sx >> 3)
+ local bit = 7 - (sx & 7)
+ local v = 0
+ for p = 0, 3 do
+ v = v | (((mem:read_u8(addr + p * 0x20000) >> bit) & 1) << p)
+ end
+ return v
+end
local function pix(px)
- return mem:read_u8(rowBase + 2 * px + 1)
+ return textPix(px, 198)
end
local function report()
@@ -92,7 +106,7 @@ LUA
# instead of the caller's cwd.
cd "$work"
out=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
- -rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" \
+ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/input.lua" &1) || true
diff --git a/scripts/verify-x68000-mouse.sh b/scripts/verify-x68000-mouse.sh
index c1a42d1..a2846da 100755
--- a/scripts/verify-x68000-mouse.sh
+++ b/scripts/verify-x68000-mouse.sh
@@ -27,16 +27,17 @@
# at origin (96, 156). Mouse verification row = stage y 199 -> GVRAM
# y 355.
#
-# X68K_SCRATCH= bash scripts/verify-x68000-mouse.sh
+# bash scripts/verify-x68000-mouse.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
-MAME="$repo/toolchains/cache/mame-mame0264/x68k"
-TEMPLATE="$SP/x68mame/HUMAN302.XDF"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed X68K_DIR x68kFirmware
+joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+TEMPLATE="$X68K_DIR/HUMAN302.XDF"
WALL="${X68K_MOUSE_WALL:-900}"
-[ -x "$MAME" ] || { echo "verify-x68000-mouse: missing patched MAME at $MAME" >&2; exit 2; }
[ -f "$TEMPLATE" ] || { echo "verify-x68000-mouse: missing $TEMPLATE" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-mouse: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
@@ -61,10 +62,24 @@ local done = false
local checks = 0
local fails = {}
--- Stage pixel (px, y) -> GVRAM word low byte.
+-- Stage pixel (px, y) -> its 4-bit colour index. The port presents to the
+-- TEXT plane (src/x68000/hal.c X68K_PRESENT_TEXT): four bit planes 128 KB
+-- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage
+-- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at
+-- 0xC00000, which this gate used to read.)
+local function textPix(px, y)
+ local sx = 96 + px
+ local addr = 0xE00000 + (156 + y) * 128 + (sx >> 3)
+ local bit = 7 - (sx & 7)
+ local v = 0
+ for p = 0, 3 do
+ v = v | (((mem:read_u8(addr + p * 0x20000) >> bit) & 1) << p)
+ end
+ return v
+end
+
local function pix(px, y)
- local rowBase = 0xC00000 + ((156 + y) * 512 + 96) * 2
- return mem:read_u8(rowBase + 2 * px + 1)
+ return textPix(px, y)
end
-- Decode the mouse verification row (stage y 199): 5A5A sentinel,
@@ -208,7 +223,7 @@ LUA
# instead of the caller's cwd.
cd "$work"
out=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
- -rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" \
+ -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/mouse.lua" &1) || true
diff --git a/scripts/verify-x68000-serial.sh b/scripts/verify-x68000-serial.sh
index 9ba9bc4..6510433 100755
--- a/scripts/verify-x68000-serial.sh
+++ b/scripts/verify-x68000-serial.sh
@@ -6,16 +6,18 @@
# back. That exercises the full chain: elf2x68k -> .X -> xdftool image ->
# Human68k -> JoeyLib serial HAL -> IOCS -> SCC -> host.
#
-# Requires: the patched MAME built at toolchains/cache/mame-mame0264/x68k
-# (patches/mame-0.264-x68k-rs232.patch), and a Human68k template
-# image to copy.
+# Requires: ./install.sh install x68000 -- the patched MAME
+# (installer/patches/mame/0004-x68k-waitstates-rs232-mouse.patch)
+# and the Human68k template image it copies.
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
-MAME="$repo/toolchains/cache/mame-mame0264/x68k"
-TEMPLATE="$SP/x68mame/HUMAN302.XDF"
-ROMS="$SP/x68mame/roms"
+source "$repo/scripts/lib/joeyEnv.sh"
+joeyNeed MAME mame
+joeyNeed X68K_DIR x68kFirmware
+joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
+TEMPLATE="$X68K_DIR/HUMAN302.XDF"
+ROMS="$X68K_DIR/roms"
PORT="${X68K_SERIAL_PORT:-6800}"
PROBE="${X68K_SERIAL_PROBE:-JOEYLIB-X68K}"
MAXWAIT="${X68K_SERIAL_TIMEOUT:-420}"
diff --git a/toolchains/env.sh b/toolchains/env.sh
index b3a512d..7e04ad3 100644
--- a/toolchains/env.sh
+++ b/toolchains/env.sh
@@ -1,80 +1,46 @@
-# JoeyLib toolchain environment.
-#
-# Source this file before invoking make:
+# Generated by install.sh -- do not edit. For a shell:
# source toolchains/env.sh
-#
-# All build commands resolve cross-compilers and assemblers exclusively
-# from toolchains/ regardless of what is installed on the host. The
-# Makefiles reference these variables by name; never hardcode paths.
-
-JOEY_TOOLCHAINS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-export JOEY_TOOLCHAINS
-
-# ------------------------------------------------------------------------
-# IIgs (llvm-mos w65816: clang + llvm-mc + link816)
-#
-# LLVM816_ROOT is the toolchain root (clang / llvm-mc / link816 plus the
-# prebuilt runtime + MAME + ROMs). clang-build.sh drives compile/assemble/
-# link. It is vendored under toolchains/iigs/llvm-mos by install.sh -- a
-# git clone of the 65816-llvm-mos repo with tools/ + runtime built via the
-# repo's own setup.sh. Override LLVM816_ROOT to point at a different copy.
-# ------------------------------------------------------------------------
-
-: "${LLVM816_ROOT:=${JOEY_TOOLCHAINS}/iigs/llvm-mos}"
-export LLVM816_ROOT
-export IIGS_CLANG_BUILD="${JOEY_TOOLCHAINS}/iigs/clang-build.sh"
-# merlin32 assembles the NinjaTrackerPlus audio replayer (third-party asm).
-export IIGS_AS="${JOEY_TOOLCHAINS}/iigs/merlin32/bin/merlin32"
-# Alias used by the top-level Makefile's HAVE_IIGS probe (`[ -x $IIGS_CC ]`).
-export IIGS_CC="${IIGS_CLANG_BUILD}"
-
-# ------------------------------------------------------------------------
-# Amiga
-# ------------------------------------------------------------------------
-
-export AMIGA_PREFIX="${JOEY_TOOLCHAINS}/amiga/gcc"
-export AMIGA_CC="${AMIGA_PREFIX}/bin/m68k-amigaos-gcc"
-export AMIGA_AR="${AMIGA_PREFIX}/bin/m68k-amigaos-ar"
-export AMIGA_LD="${AMIGA_PREFIX}/bin/m68k-amigaos-ld"
-# vasm is invoked only for ptplayer.asm (third-party Devpac/vasm
-# syntax). JoeyLib's own .s files are GAS syntax and go through
-# AMIGA_CC -> m68k-amigaos-as (binutils, GPL) instead.
-export AMIGA_AS="${JOEY_TOOLCHAINS}/amiga/vasm/bin/vasmm68k_mot"
-export AMIGA_NDK="${AMIGA_PREFIX}/m68k-amigaos/sys-include"
-export AMIGA_PTPLAYER="${JOEY_TOOLCHAINS}/amiga/ptplayer"
-
-# ------------------------------------------------------------------------
-# Atari ST
-# ------------------------------------------------------------------------
-
-export ST_PREFIX="${JOEY_TOOLCHAINS}/atarist/gcc"
-export ST_CC="${ST_PREFIX}/bin/m68k-atari-mint-gcc"
-export ST_AR="${ST_PREFIX}/bin/m68k-atari-mint-ar"
-export ST_LD="${ST_PREFIX}/bin/m68k-atari-mint-ld"
-# Atari ST .s files are GAS syntax and assembled via ST_CC ->
-# m68k-atari-mint-as (binutils, GPL). No vasm dependency.
-
-# ------------------------------------------------------------------------
-# DOS
-# ------------------------------------------------------------------------
-
-export DOS_PREFIX="${JOEY_TOOLCHAINS}/dos/djgpp"
-export DOS_CC="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-gcc"
-export DOS_AR="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-ar"
-export DOS_LD="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-ld"
-# DJGPP's DOS-target utilities live under the i586-...-msdosdjgpp
-# sysroot bin, not the host bin that holds the cross-gcc.
-export DOS_STUBEDIT="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/stubedit"
-export DOS_STUBIFY="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/stubify"
-export DOS_EXE2COFF="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/exe2coff"
-export DOS_AS="${JOEY_TOOLCHAINS}/dos/nasm/bin/nasm"
-export DOS_CWSDPMI="${JOEY_TOOLCHAINS}/dos/cwsdpmi/bin/bin"
-export DOS_CWSDSTUB="${DOS_CWSDPMI}/CWSDSTUB.EXE"
-export DOS_EMBED_DPMI="${JOEY_TOOLCHAINS}/dos/embed-dpmi.sh"
-
-# ------------------------------------------------------------------------
-# PATH augmentation (in case sub-tools shell out unprefixed names)
-# ------------------------------------------------------------------------
-
-PATH="${AMIGA_PREFIX}/bin:${ST_PREFIX}/bin:${DOS_PREFIX}/bin:${JOEY_TOOLCHAINS}/iigs/merlin32/bin:${JOEY_TOOLCHAINS}/amiga/vasm/bin:${JOEY_TOOLCHAINS}/dos/nasm/bin:${PATH}"
+export JOEY_TOOLCHAINS='/home/scott/claude/joeylib/toolchains'
+export AMIGA_PREFIX='/home/scott/claude/joeylib/toolchains/opt/amigaGcc'
+export AMIGA_CC='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-gcc'
+export AMIGA_AR='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-ar'
+export AMIGA_LD='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-ld'
+export AMIGA_AS='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/vasmm68k_mot'
+export AMIGA_NDK='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/m68k-amigaos/sys-include'
+export CADIUS='/home/scott/claude/joeylib/toolchains/opt/cadius/bin/cadius'
+export DOS_CWSDPMI='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin'
+export DOS_CWSDSTUB='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin/CWSDSTUB.EXE'
+export DOS_PREFIX='/home/scott/claude/joeylib/toolchains/opt/djgpp'
+export DOS_CC='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-gcc'
+export DOS_AR='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-ar'
+export DOS_LD='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-ld'
+export DOS_STUBEDIT='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/stubedit'
+export DOS_STUBIFY='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/stubify'
+export DOS_EXE2COFF='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/exe2coff'
+export DOS_EMBED_DPMI='/home/scott/claude/joeylib/make/support/dos/embed-dpmi.sh'
+export DOSBOX='/home/scott/claude/joeylib/toolchains/opt/dosbox/dosbox'
+export X68K_ROOT='/home/scott/claude/joeylib/toolchains/opt/elf2x68k'
+export EMUTOS_IMG='/home/scott/claude/joeylib/toolchains/firmware/atarist/emutos-512k.img'
+export FSUAE='/usr/bin/fs-uae'
+export HATARI='/usr/bin/hatari'
+export IIGS_ROMPATH='/home/scott/claude/joeylib/toolchains/firmware/iigs/roms'
+export GSOS_SYSTEM_DISK='/home/scott/claude/joeylib/toolchains/firmware/iigs/gsos-system.po'
+export KICKSTART_ROM='/home/scott/claude/joeylib/toolchains/firmware/amiga/kickstart.rom'
+export LIBXMP_DIR='/home/scott/claude/joeylib/toolchains/opt/libxmp'
+export LLVM816_ROOT='/home/scott/claude/joeylib/toolchains/opt/llvm816'
+export IIGS_CLANG_BUILD='/home/scott/claude/joeylib/make/support/iigs/clang-build.sh'
+export MAME='/home/scott/claude/joeylib/toolchains/opt/mame/bin/mame'
+export IIGS_AS='/home/scott/claude/joeylib/toolchains/opt/merlin32/bin/merlin32'
+export ST_PREFIX='/home/scott/claude/joeylib/toolchains/opt/mintGcc'
+export ST_CC='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-gcc'
+export ST_AR='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-ar'
+export ST_LD='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-ld'
+export DOS_AS='/usr/bin/nasm'
+export IIGS_NTP_DIR='/home/scott/claude/joeylib/toolchains/opt/ntp'
+export AMIGA_PTPLAYER='/home/scott/claude/joeylib/toolchains/opt/ptplayer'
+export X68K_DIR='/home/scott/claude/joeylib/toolchains/firmware/x68000'
+case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin':"$PATH" ;; esac
+case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/djgpp/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin':"$PATH" ;; esac
+case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/elf2x68k/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/elf2x68k/bin':"$PATH" ;; esac
+case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin':"$PATH" ;; esac
export PATH
diff --git a/toolchains/install.sh b/toolchains/install.sh
deleted file mode 100755
index 7e2e142..0000000
--- a/toolchains/install.sh
+++ /dev/null
@@ -1,1308 +0,0 @@
-#!/usr/bin/env bash
-#
-# JoeyLib toolchain installer.
-#
-# Installs all cross-compilers, assemblers, and libraries needed to build
-# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
-# toolchains/ folder. Nothing is installed system-wide. Free tools are
-# fetched and built; tools that must be placed manually print clear
-# placement instructions and the script verifies they were placed
-# correctly on a subsequent run.
-#
-# Usage:
-# ./toolchains/install.sh install everything we can get
-# ./toolchains/install.sh --only iigs install only one platform
-# ./toolchains/install.sh --force wipe and reinstall
-# ./toolchains/install.sh --skip-emulators skip emulators (cross tools only)
-# ./toolchains/install.sh --help show this message
-#
-# Emulators: dosbox, fs-uae (Amiga), hatari (Atari ST), GSplus (Apple IIgs).
-# On Linux with apt, the first three are installed via 'sudo apt install'.
-# On macOS with Homebrew, they are installed via 'brew install'. GSplus is
-# always built from source into toolchains/emulators/gsplus/.
-#
-
-set -euo pipefail
-
-# Never prompt for git credentials. Missing / private GitHub repos
-# otherwise ask for a username/password on HTTPS clones; we want those
-# to fail fast so the installer reports clear instructions instead.
-export GIT_TERMINAL_PROMPT=0
-export GIT_ASKPASS=/bin/true
-
-# ------------------------------------------------------------------------
-# Paths and globals
-# ------------------------------------------------------------------------
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
-CACHE_DIR="${SCRIPT_DIR}/cache"
-STATE_DIR="${SCRIPT_DIR}/.install_state"
-
-ONLY_PLATFORM=""
-FORCE_INSTALL=0
-SKIP_EMULATORS=0
-
-# Status tracking. Keys are component identifiers; values are "ok",
-# "missing", or "failed".
-declare -A STATUS
-declare -A INSTRUCTIONS
-
-# ------------------------------------------------------------------------
-# Output helpers (ANSI color when stdout is a TTY)
-# ------------------------------------------------------------------------
-
-if [ -t 1 ]; then
- C_RESET=$'\033[0m'
- C_BOLD=$'\033[1m'
- C_RED=$'\033[31m'
- C_GREEN=$'\033[32m'
- C_YELLOW=$'\033[33m'
- C_CYAN=$'\033[36m'
-else
- C_RESET=""
- C_BOLD=""
- C_RED=""
- C_GREEN=""
- C_YELLOW=""
- C_CYAN=""
-fi
-
-info() { printf '%s[*]%s %s\n' "${C_CYAN}" "${C_RESET}" "$*"; }
-ok() { printf '%s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$*"; }
-warn() { printf '%s[!!]%s %s\n' "${C_YELLOW}" "${C_RESET}" "$*"; }
-err() { printf '%s[xx]%s %s\n' "${C_RED}" "${C_RESET}" "$*" 1>&2; }
-header() { printf '\n%s== %s ==%s\n' "${C_BOLD}" "$*" "${C_RESET}"; }
-
-# ------------------------------------------------------------------------
-# Utility functions
-# ------------------------------------------------------------------------
-
-usage() {
- sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
- exit 0
-}
-
-require_tool() {
- local tool="$1"
- if ! command -v "${tool}" >/dev/null 2>&1; then
- err "Required host tool not found: ${tool}"
- err "Install it via your system package manager and re-run."
- exit 1
- fi
-}
-
-detect_host_os() {
- case "$(uname -s)" in
- Linux*) HOST_OS="linux" ;;
- Darwin*) HOST_OS="macos" ;;
- MINGW*|MSYS*|CYGWIN*)
- HOST_OS="windows" ;;
- *)
- err "Unsupported host OS: $(uname -s)"
- exit 1
- ;;
- esac
- info "Host OS: ${HOST_OS}"
-}
-
-ensure_dirs() {
- mkdir -p "${CACHE_DIR}" "${STATE_DIR}"
-}
-
-mark_done() {
- touch "${STATE_DIR}/$1.done"
-}
-
-is_done() {
- [ -f "${STATE_DIR}/$1.done" ]
-}
-
-clear_done() {
- rm -f "${STATE_DIR}/$1.done"
-}
-
-download() {
- local url="$1"
- local dest="$2"
- if [ -f "${dest}" ]; then
- info "Cached: $(basename "${dest}")"
- return 0
- fi
- info "Downloading $(basename "${dest}")"
- if command -v curl >/dev/null 2>&1; then
- curl -fL --retry 3 -o "${dest}" "${url}"
- elif command -v wget >/dev/null 2>&1; then
- wget -O "${dest}" "${url}"
- else
- err "Neither curl nor wget is installed"
- return 1
- fi
-}
-
-# ------------------------------------------------------------------------
-# Argument parsing
-# ------------------------------------------------------------------------
-
-while [ $# -gt 0 ]; do
- case "$1" in
- --only)
- ONLY_PLATFORM="$2"
- shift 2
- ;;
- --force)
- FORCE_INSTALL=1
- shift
- ;;
- --skip-emulators)
- SKIP_EMULATORS=1
- shift
- ;;
- --help|-h)
- usage
- ;;
- *)
- err "Unknown argument: $1"
- usage
- ;;
- esac
-done
-
-should_install() {
- local platform="$1"
- if [ -z "${ONLY_PLATFORM}" ]; then
- return 0
- fi
- [ "${ONLY_PLATFORM}" = "${platform}" ]
-}
-
-# ------------------------------------------------------------------------
-# IIgs: llvm-mos w65816 toolchain (clang + runtime + MAME -- the primary
-# build path, vendored under iigs/llvm-mos) + Merlin32 + NinjaTrackerPlus.
-# ------------------------------------------------------------------------
-
-install_audio_libxmp() {
- header "libxmp-lite (DOS / ST audio decoder)"
- local base="${SCRIPT_DIR}/audio"
- local target="${base}/libxmp-lite"
- local key="audio_libxmp"
- local version="4.7.0"
-
- if [ "${FORCE_INSTALL}" -eq 1 ]; then
- rm -rf "${target}"
- clear_done "${key}"
- fi
- if is_done "${key}" && [ -f "${target}/include/libxmp-lite/xmp.h" ]; then
- ok "libxmp-lite ${version} already installed"
- STATUS[${key}]="ok"
- return
- fi
-
- info "Downloading libxmp-lite ${version} from libxmp.org"
- local tarball="${CACHE_DIR}/libxmp-lite-${version}.tar.gz"
- local url="https://github.com/libxmp/libxmp/releases/download/libxmp-${version}/libxmp-lite-${version}.tar.gz"
- if ! download "${url}" "${tarball}"; then
- STATUS[${key}]="failed"
- INSTRUCTIONS[${key}]="Could not download ${url}"
- return
- fi
-
- mkdir -p "${base}"
- rm -rf "${target}"
- if (cd "${base}" && tar xzf "${tarball}") && [ -d "${base}/libxmp-lite-${version}" ]; then
- mv "${base}/libxmp-lite-${version}" "${target}"
- mark_done "${key}"
- ok "libxmp-lite installed at ${target}"
- STATUS[${key}]="ok"
- else
- STATUS[${key}]="failed"
- INSTRUCTIONS[${key}]="Extract of ${tarball} did not produce libxmp-lite-${version}/"
- fi
-}
-
-
-install_iigs() {
- header "IIgs toolchain"
- local base="${SCRIPT_DIR}/iigs"
- mkdir -p "${base}"
-
- # llvm-mos w65816 toolchain (clang + llvm-mc + link816 + the prebuilt
- # runtime, plus MAME + apple2gs ROMs). The primary IIgs build path:
- # clang-build.sh / make/iigs.mk resolve it via $LLVM816_ROOT, which
- # toolchains/env.sh defaults to ${base}/llvm-mos. It is a git clone of
- # the project repo, built by the repo's own setup.sh (clang/llc/llvm-mc
- # build + link816/omfEmit + runtime + MAME). Calypsi and GNO/ME are
- # optional reference targets we skip.
- local llvm_dir="${base}/llvm-mos"
- local llvm_repo="https://forge.duensing.digital/AI_Slop/65816-llvm-mos.git"
- local llvm_clang="${llvm_dir}/tools/llvm-mos-build/bin/clang"
- if is_done "iigs_llvm" && [ -x "${llvm_clang}" ]; then
- ok "llvm-mos w65816 toolchain already built"
- STATUS[iigs_llvm]="ok"
- else
- if [ ! -d "${llvm_dir}/.git" ]; then
- info "Cloning 65816-llvm-mos into ${llvm_dir}"
- git clone "${llvm_repo}" "${llvm_dir}" || \
- INSTRUCTIONS[iigs_llvm]="git clone ${llvm_repo} ${llvm_dir} failed"
- fi
- if [ -f "${llvm_dir}/setup.sh" ] && [ ! -x "${llvm_clang}" ]; then
- info "Building llvm-mos toolchain via setup.sh (~30-60 min: clang + runtime + MAME)"
- ( cd "${llvm_dir}" && bash setup.sh --skip-calypsi --skip-gno ) || \
- warn "llvm-mos setup.sh reported issues; see above"
- fi
- if [ -x "${llvm_clang}" ]; then
- mark_done "iigs_llvm"
- ok "llvm-mos w65816 toolchain built at ${llvm_dir}"
- STATUS[iigs_llvm]="ok"
- else
- STATUS[iigs_llvm]="missing"
- INSTRUCTIONS[iigs_llvm]=$(cat </dev/null 2>&1; then
- ok "php CLI already on PATH"
- STATUS[iigs_php]="ok"
- else
- info "Installing php CLI for joeymod's MOD->NTP converter"
- local php_pkgs=""
- case "${HOST_OS}" in
- linux) php_pkgs="php-cli" ;;
- macos) php_pkgs="php" ;;
- esac
- if [ -n "${php_pkgs}" ] && pkg_install ${php_pkgs}; then
- ok "php installed"
- STATUS[iigs_php]="ok"
- else
- STATUS[iigs_php]="missing"
- INSTRUCTIONS[iigs_php]=$(cat < .NTP conversion path.
- Debian/Ubuntu: sudo apt install php-cli
- macOS Homebrew: brew install php
-EOF
-)
- fi
- fi
-
- # Merlin32 -- 65816 cross-assembler from Brutal Deluxe.
- # The canonical distribution is a zip on brutaldeluxe.fr that bundles
- # prebuilt binaries for Linux / macOS / Windows plus the C source.
- # We prefer the host's prebuilt binary; if not present, we build
- # from source with plain gcc (the source has no Makefile).
- if [ "${FORCE_INSTALL}" -eq 1 ]; then
- rm -rf "${base}/merlin32"
- clear_done "iigs_merlin32"
- fi
-
- if is_done "iigs_merlin32" && [ -x "${base}/merlin32/bin/merlin32" ]; then
- ok "Merlin32 already installed"
- STATUS[iigs_merlin32]="ok"
- else
- local merlin_installed=0
- local merlin_urls=(
- "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32_v1.1.zip"
- "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32.zip"
- )
- local merlin_zip="${CACHE_DIR}/merlin32.zip"
- local merlin_extract="${CACHE_DIR}/merlin32-extract"
- local prebuilt_subdir=""
-
- case "${HOST_OS}" in
- linux) prebuilt_subdir="Linux" ;;
- macos) prebuilt_subdir="MacOs" ;;
- *) prebuilt_subdir="" ;;
- esac
-
- for url in "${merlin_urls[@]}"; do
- if download "${url}" "${merlin_zip}" 2>/dev/null; then
- info "Extracting Merlin32"
- rm -rf "${merlin_extract}"
- mkdir -p "${merlin_extract}"
- if ! unzip -o "${merlin_zip}" -d "${merlin_extract}" >/dev/null 2>&1; then
- continue
- fi
- mkdir -p "${base}/merlin32/bin"
-
- # 1. Try prebuilt binary for the host OS. Name may be
- # capitalized ("Merlin32") or lowercase. No executable
- # bit in the zip, so we chmod after copying.
- local mbin=""
- if [ -n "${prebuilt_subdir}" ]; then
- mbin=$(find "${merlin_extract}" -type d -name "${prebuilt_subdir}" -print 2>/dev/null | head -n1)
- if [ -n "${mbin}" ]; then
- mbin=$(find "${mbin}" -maxdepth 1 -type f -iname "merlin32" 2>/dev/null | head -n1)
- fi
- fi
-
- if [ -n "${mbin}" ] && [ -f "${mbin}" ]; then
- cp "${mbin}" "${base}/merlin32/bin/merlin32"
- chmod +x "${base}/merlin32/bin/merlin32"
- mark_done "iigs_merlin32"
- ok "Merlin32 installed (prebuilt ${prebuilt_subdir} binary)"
- STATUS[iigs_merlin32]="ok"
- merlin_installed=1
- break
- fi
-
- # 2. No prebuilt for this host; build from Source/. The
- # source has no Makefile; 'gcc -O2 *.c' produces the
- # binary directly.
- local msrc
- msrc=$(find "${merlin_extract}" -type d -name "Source" 2>/dev/null | head -n1)
- if [ -n "${msrc}" ] && [ -f "${msrc}/Main.c" ]; then
- info "Building Merlin32 from source"
- (
- cd "${msrc}"
- gcc -O2 -o merlin32 *.c
- ) && {
- if [ -x "${msrc}/merlin32" ]; then
- cp "${msrc}/merlin32" "${base}/merlin32/bin/merlin32"
- mark_done "iigs_merlin32"
- ok "Merlin32 built from source and installed"
- STATUS[iigs_merlin32]="ok"
- merlin_installed=1
- break
- fi
- }
- fi
- fi
- done
-
- if [ "${merlin_installed}" -eq 0 ]; then
- STATUS[iigs_merlin32]="missing"
- INSTRUCTIONS[iigs_merlin32]=$(cat </dev/null || true
- ) && {
- mark_done "vasm"
- ok "vasm installed"
- STATUS[vasm]="ok"
- } || {
- STATUS[vasm]="failed"
- INSTRUCTIONS[vasm]="vasm build failed; see ${vasm_src}"
- }
- fi
-
- # NDK -- AmigaOS includes (Bebbo's repo bundles these via 'make all')
- if [ -d "${base}/gcc/m68k-amigaos/sys-include" ] || [ -d "${base}/gcc/m68k-amigaos/include" ]; then
- ok "Amiga NDK headers present (bundled with amiga-gcc)"
- STATUS[amiga_ndk]="ok"
- else
- STATUS[amiga_ndk]="missing"
- INSTRUCTIONS[amiga_ndk]="Amiga NDK headers should be installed by Bebbo's amiga-gcc 'make all'. Re-run installer after fixing amiga-gcc."
- fi
-
- # PTPlayer -- MOD replayer by Frank Wille, distributed as an LHA
- # archive on Aminet. Extract with lhasa (installed as part of the
- # Amiga GCC build deps above).
- if [ "${FORCE_INSTALL}" -eq 1 ]; then
- rm -rf "${base}/ptplayer"
- clear_done "amiga_ptplayer"
- fi
-
- if is_done "amiga_ptplayer" && [ -f "${base}/ptplayer/ptplayer.asm" ]; then
- ok "PTPlayer already installed"
- STATUS[amiga_ptplayer]="ok"
- else
- info "Downloading PTPlayer from Aminet"
- local ptp_lha="${CACHE_DIR}/ptplayer.lha"
- if ! download "http://aminet.net/mus/play/ptplayer.lha" "${ptp_lha}"; then
- STATUS[amiga_ptplayer]="failed"
- INSTRUCTIONS[amiga_ptplayer]="Could not download http://aminet.net/mus/play/ptplayer.lha"
- else
- local extractor=""
- if command -v lhasa >/dev/null 2>&1; then
- extractor="lhasa"
- elif command -v lha >/dev/null 2>&1; then
- extractor="lha"
- fi
-
- if [ -z "${extractor}" ]; then
- STATUS[amiga_ptplayer]="missing"
- INSTRUCTIONS[amiga_ptplayer]=$(cat < amiga/vasm"
- STATUS[atarist_vasm]="ok"
- else
- STATUS[atarist_vasm]="missing"
- INSTRUCTIONS[atarist_vasm]="vasm not yet installed for Amiga; install Amiga toolchain first."
- fi
- fi
-}
-
-# ------------------------------------------------------------------------
-# DOS: DJGPP, NASM, CWSDPMI
-# ------------------------------------------------------------------------
-
-install_dos() {
- header "DOS toolchain"
- local base="${SCRIPT_DIR}/dos"
- mkdir -p "${base}"
-
- # DJGPP -- the canonical zip set
- if [ "${FORCE_INSTALL}" -eq 1 ]; then
- rm -rf "${base}/djgpp"
- clear_done "dos_djgpp"
- fi
-
- if is_done "dos_djgpp" && [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then
- ok "DJGPP already installed"
- STATUS[dos_djgpp]="ok"
- else
- # Use prebuilt DJGPP binaries from andrewwutw/build-djgpp
- # releases. Much faster than source build and avoids a long list
- # of build-dep prerequisites.
- info "Downloading DJGPP prebuilt binaries"
- local djgpp_arch=""
- local djgpp_url=""
- case "${HOST_OS}" in
- linux)
- if [ "$(uname -m)" = "x86_64" ]; then
- djgpp_arch="linux64"
- else
- djgpp_arch="linux32"
- fi
- ;;
- macos)
- djgpp_arch="osx"
- ;;
- esac
-
- if [ -n "${djgpp_arch}" ]; then
- djgpp_url="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-${djgpp_arch}-gcc1220.tar.bz2"
- fi
-
- local djgpp_tar="${CACHE_DIR}/djgpp.tar.bz2"
- if [ -n "${djgpp_url}" ] && download "${djgpp_url}" "${djgpp_tar}" 2>/dev/null; then
- mkdir -p "${base}/djgpp"
- # The tarball extracts to a 'djgpp' directory at the top level.
- # Strip one component so contents land directly in ${base}/djgpp.
- tar -xjf "${djgpp_tar}" -C "${base}/djgpp" --strip-components=1 && {
- if [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then
- mark_done "dos_djgpp"
- ok "DJGPP installed (prebuilt ${djgpp_arch})"
- STATUS[dos_djgpp]="ok"
- else
- STATUS[dos_djgpp]="failed"
- INSTRUCTIONS[dos_djgpp]="DJGPP tarball extracted but i586-pc-msdosdjgpp-gcc not found under ${base}/djgpp/bin/"
- fi
- } || {
- STATUS[dos_djgpp]="failed"
- INSTRUCTIONS[dos_djgpp]="Failed to extract DJGPP tarball ${djgpp_tar}"
- }
- else
- STATUS[dos_djgpp]="failed"
- INSTRUCTIONS[dos_djgpp]=$(cat </dev/null) && {
- ok "CWSDPMI installed"
- STATUS[dos_cwsdpmi]="ok"
- } || {
- STATUS[dos_cwsdpmi]="failed"
- }
- fi
-}
-
-# ------------------------------------------------------------------------
-# Emulators: DOSBox, FS-UAE, Hatari, GSplus
-#
-# Package-manager-installable emulators (DOSBox, FS-UAE, Hatari) are
-# installed via apt on Linux / brew on macOS and exist on the host PATH
-# after install. GSplus has no package and is always built from source
-# into toolchains/emulators/gsplus/.
-# ------------------------------------------------------------------------
-
-pkg_install() {
- # Install a list of package names on the current host. Returns 0 on
- # success, non-zero on any failure. Uses apt on Linux, brew on macOS.
- case "${HOST_OS}" in
- linux)
- if command -v apt-get >/dev/null 2>&1; then
- sudo apt-get install -y "$@"
- return $?
- fi
- ;;
- macos)
- if command -v brew >/dev/null 2>&1; then
- brew install "$@"
- return $?
- fi
- ;;
- esac
- return 1
-}
-
-# Install build dependencies. Args: label, apt-list, brew-list.
-# The label identifies what we're about to build; the lists are the
-# space-separated package names for each package manager. On failure,
-# prints instructions but does not abort the installer (the build step
-# will then fail with a clearer message).
-ensure_build_deps() {
- local label="$1"
- local apt_pkgs="$2"
- local brew_pkgs="$3"
- local pkgs=""
-
- case "${HOST_OS}" in
- linux) pkgs="${apt_pkgs}" ;;
- macos) pkgs="${brew_pkgs}" ;;
- esac
-
- if [ -z "${pkgs}" ]; then
- return 0
- fi
-
- info "Installing ${label} build dependencies"
- if ! pkg_install ${pkgs}; then
- warn "Failed to install ${label} dependencies via package manager."
- warn "You may need to install manually: ${pkgs}"
- return 1
- fi
- return 0
-}
-
-install_apt_emulator() {
- local key="$1"
- local cmd="$2"
- local aptpkg="$3"
- local brewpkg="$4"
-
- if command -v "${cmd}" >/dev/null 2>&1; then
- ok "${cmd} already on PATH"
- STATUS[${key}]="ok"
- return
- fi
-
- info "Installing ${cmd} via host package manager"
- local pkgs
- case "${HOST_OS}" in
- linux) pkgs="${aptpkg}" ;;
- macos) pkgs="${brewpkg}" ;;
- *) pkgs="" ;;
- esac
-
- if [ -n "${pkgs}" ] && pkg_install ${pkgs}; then
- ok "${cmd} installed"
- STATUS[${key}]="ok"
- else
- STATUS[${key}]="missing"
- INSTRUCTIONS[${key}]=$(cat </dev/null 2>&1; then
- ok "gsplus already on PATH"
- STATUS[emu_gsplus]="ok"
- return
- fi
-
- if is_done "emu_gsplus" && [ -x "${bin}" ]; then
- ok "GSplus already built at ${bin}"
- STATUS[emu_gsplus]="ok"
- return
- fi
-
- ensure_build_deps "GSplus" \
- "build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev" \
- "libpcap freetype pulseaudio" || true
-
- info "Building GSplus from source (Apple IIgs emulator)"
- local src="${CACHE_DIR}/gsplus-src"
- if [ ! -d "${src}" ]; then
- if ! git clone --depth 1 https://github.com/digarok/gsplus.git "${src}"; then
- STATUS[emu_gsplus]="failed"
- INSTRUCTIONS[emu_gsplus]="git clone https://github.com/digarok/gsplus.git failed"
- return
- fi
- fi
-
- # GSplus build layout: source lives at gsplus/src/ and expects one
- # of the vars_* config files to be copied into 'vars' before make.
- # On Linux we use the X11/PulseAudio build; on macOS we use the
- # Mac X11 build for command-line use (the Swift/clang .app build
- # needs Xcode and is skipped here).
- local gsplus_src_dir="${src}/gsplus/src"
- local gsplus_target=""
- local vars_file=""
- case "${HOST_OS}" in
- linux)
- vars_file="vars_x86linux"
- gsplus_target="gsplus-x"
- ;;
- macos)
- vars_file="vars_mac_x"
- gsplus_target="gsplus-x"
- ;;
- esac
-
- if [ -z "${vars_file}" ] || [ ! -f "${gsplus_src_dir}/${vars_file}" ]; then
- STATUS[emu_gsplus]="failed"
- INSTRUCTIONS[emu_gsplus]=$(cat </dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ')
-Copy the appropriate vars_* file to 'vars' and run make manually.
-EOF
-)
- return
- fi
-
- (
- cd "${gsplus_src_dir}"
- cp "${vars_file}" vars
- make clean >/dev/null 2>&1 || true
- make "${gsplus_target}"
- ) && {
- # Built binary lands at gsplus/src/../gsplus per the Makefile
- # (it's moved up one level after linking).
- local gsbin="${src}/gsplus/gsplus"
- if [ ! -f "${gsbin}" ]; then
- # Fallback: find anywhere under the tree
- gsbin=$(find "${src}" -maxdepth 4 -name gsplus -type f 2>/dev/null | head -n1)
- fi
- if [ -n "${gsbin}" ] && [ -f "${gsbin}" ]; then
- mkdir -p "${base}/bin"
- cp "${gsbin}" "${base}/bin/gsplus"
- chmod +x "${base}/bin/gsplus"
- # Also copy the support files GSplus reads at runtime
- if [ -d "${src}/gsplus/lib" ]; then
- mkdir -p "${base}/lib"
- cp -r "${src}/gsplus/lib/." "${base}/lib/"
- fi
- mark_done "emu_gsplus"
- ok "GSplus built and installed"
- STATUS[emu_gsplus]="ok"
- else
- STATUS[emu_gsplus]="failed"
- INSTRUCTIONS[emu_gsplus]="GSplus build succeeded but binary not found; check ${src}"
- fi
- } || {
- STATUS[emu_gsplus]="failed"
- INSTRUCTIONS[emu_gsplus]=$(cat < "${dest}"
- mark_done "${key}"
- ok "IIgs null slot-6 PROM staged at ${dest}"
- STATUS[${key}]="ok"
-}
-
-
-install_emulator_support() {
- header "Emulator support files"
- local support_dir="${SCRIPT_DIR}/emulators/support"
- mkdir -p "${support_dir}"
-
- install_support_emutos "${support_dir}"
- install_support_iigs_rom "${support_dir}"
- install_support_iigs_null_c600 "${support_dir}"
- install_support_gsos "${support_dir}"
-}
-
-
-install_emulators() {
- header "Emulators"
-
- install_apt_emulator emu_dosbox dosbox dosbox dosbox-x
- install_apt_emulator emu_fsuae fs-uae fs-uae fs-uae
- install_apt_emulator emu_hatari hatari hatari hatari
- install_gsplus
-
- install_emulator_support
-}
-
-# ------------------------------------------------------------------------
-# Final status report
-# ------------------------------------------------------------------------
-
-print_status_report() {
- header "Installation status"
- local any_missing=0
- local any_failed=0
- local key
- local s
-
- # First pass: compute overall status. Do this in the current shell
- # (not inside a pipeline) so variable assignments stick.
- for key in "${!STATUS[@]}"; do
- s="${STATUS[$key]}"
- case "$s" in
- missing) any_missing=1 ;;
- failed) any_failed=1 ;;
- esac
- done
-
- # Second pass: render sorted output.
- {
- for key in "${!STATUS[@]}"; do
- s="${STATUS[$key]}"
- case "$s" in
- ok)
- printf ' %s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$key"
- ;;
- missing)
- printf ' %s[--]%s %s (manual install required)\n' "${C_YELLOW}" "${C_RESET}" "$key"
- ;;
- failed)
- printf ' %s[xx]%s %s (install failed)\n' "${C_RED}" "${C_RESET}" "$key"
- ;;
- esac
- done
- } | sort
-
- if [ "${any_missing}" -eq 1 ] || [ "${any_failed}" -eq 1 ]; then
- header "Action required"
- for key in "${!INSTRUCTIONS[@]}"; do
- if [ "${STATUS[$key]}" != "ok" ]; then
- printf '\n%s---- %s ----%s\n%s\n' "${C_BOLD}" "$key" "${C_RESET}" "${INSTRUCTIONS[$key]}"
- fi
- done
- echo ""
- warn "Some components are not yet installed. Re-run after addressing the items above."
- exit 1
- else
- echo ""
- ok "All requested toolchains are ready."
- info "Now: source ${SCRIPT_DIR}/env.sh"
- fi
-}
-
-# ------------------------------------------------------------------------
-# Main
-# ------------------------------------------------------------------------
-
-main() {
- detect_host_os
- require_tool make
- require_tool tar
- require_tool unzip
- ensure_dirs
-
- if should_install iigs; then install_iigs; fi
- if should_install amiga; then install_amiga; fi
- if should_install atarist; then install_atarist; fi
- if should_install dos; then install_dos; fi
-
- # Audio third-party fetches that span multiple targets. libxmp-lite
- # is used by the DOS HAL today and will back the ST mixer's
- # Protracker decoder later, so it lives in toolchains/audio/ rather
- # than under any one platform's tree.
- if should_install dos || should_install atarist; then
- install_audio_libxmp
- fi
-
- if [ "${SKIP_EMULATORS}" -eq 0 ] && [ -z "${ONLY_PLATFORM}" ]; then
- install_emulators
- fi
-
- print_status_report
-}
-
-main "$@"
diff --git a/tools/joeymod/joeymod b/tools/joeymod/joeymod
index b9f26a5..dbcce22 100755
--- a/tools/joeymod/joeymod
+++ b/tools/joeymod/joeymod
@@ -16,8 +16,8 @@
# E8FF (or treat it as a filter
# toggle), so .amod is harmless if
# fed to the wrong port.
-# joeymod input.mod output.ntp -- runs the PHP ntpconverter staged
-# at toolchains/iigs/ntp/ntpconverter.php
+# joeymod input.mod output.ntp -- runs the PHP ntpconverter that
+# ./install.sh installs (ntp)
# (NinjaTrackerPlus' .NTP runtime
# format on the IIgs)
#
@@ -69,9 +69,11 @@ case "$ext" in
;;
ntp)
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
- ntp_dir="$repo/toolchains/iigs/ntp"
+ source "$repo/scripts/lib/joeyEnv.sh"
+ joeyNeed IIGS_NTP_DIR ntp
+ ntp_dir="$IIGS_NTP_DIR"
if [[ ! -f $ntp_dir/ntpconverter.php ]]; then
- echo "joeymod: $ntp_dir/ntpconverter.php missing; run toolchains/install.sh" >&2
+ echo "joeymod: $ntp_dir/ntpconverter.php missing; run: ./install.sh install ntp" >&2
exit 2
fi
if ! command -v php >/dev/null 2>&1; then