From 8b9590ddb2dd29c7d88ccb0267ace89fff677e87 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 28 Sep 2026 15:27:29 -0500 Subject: [PATCH] Major changes to setup. Now installable by mere mortals on Debian, Fedora, and WSL. --- .gitignore | 7 +- LICENSES.md | 134 +- Makefile | 40 +- README.md | 81 +- docs/Appendix.adoc | 6 +- docs/Assets.adoc | 6 +- docs/BuildIntegration.adoc | 22 +- docs/GettingStarted.adoc | 289 +++- examples/spacetaxi/PLAN.md | 2 +- examples/spacetaxi/VERIFIED.md | 6 +- examples/spacetaxi/assets/cpu6502.py | 560 +++++++ examples/spacetaxi/assets/depackSpeechRam.py | 9 +- examples/spacetaxi/assets/dumpScenes.py | 115 ++ examples/spacetaxi/assets/levels/format.md | 2 +- examples/spacetaxi/assets/romToLevel.py | 205 +++ examples/spacetaxi/assets/sidCapture.py | 4 +- examples/spacetaxi/stLevelFile.c | 2 +- install.sh | 447 ++++++ installer/components/amigaGcc.sh | 96 ++ installer/components/cadius.sh | 44 + installer/components/cwsdpmi.sh | 36 + installer/components/djgpp.sh | 44 + installer/components/dosbox.sh | 41 + installer/components/elf2x68k.sh | 36 + installer/components/emutos.sh | 36 + installer/components/fsuae.sh | 58 + installer/components/hatari.sh | 58 + installer/components/headless.sh | 29 + installer/components/host.sh | 67 + installer/components/iigsFirmware.sh | 61 + installer/components/kickstart.sh | 31 + installer/components/libxmp.sh | 33 + installer/components/llvm816.sh | 68 + installer/components/mame.sh | 49 + installer/components/merlin32.sh | 41 + installer/components/mintGcc.sh | 52 + installer/components/nasm.sh | 36 + installer/components/ntp.sh | 41 + installer/components/ptplayer.sh | 34 + installer/components/staxiData.sh | 30 + installer/components/x68kCgrom.sh | 31 + installer/components/x68kFirmware.sh | 42 + installer/distroPackages.txt | 89 ++ installer/files.sha1 | 9 + installer/lib/bundle.sh | 140 ++ installer/lib/common.sh | 339 +++++ installer/lib/config.sh | 61 + installer/lib/distro.sh | 103 ++ .../mame/0001-apple2gs-scc-clock.patch | 4 +- .../patches/mame/0002-z80scc-rx-fifo.patch | 26 + .../patches/mame/0003-x68k-crtc-timing.patch | 8 +- .../0004-x68k-waitstates-rs232-mouse.patch | 429 ++++-- installer/patches/mame/README.md | 27 + .../patches/mame/lua}/x68k-boot-check.lua | 0 .../patches/mame/lua}/x68k-crtc-probe.lua | 0 .../patches/mame/lua}/x68k-wait-probe.lua | 0 joey-run | 547 +++++++ make/amiga.mk | 19 +- make/atarist.mk | 17 +- make/common.mk | 24 + make/dos.mk | 21 +- make/iigs.mk | 56 +- make/support/atarist/include-shim/math.h | 25 + make/support/atarist/include-shim/math_shim.c | 100 ++ make/support/dos/embed-dpmi.sh | 69 + make/support/iigs/clang-build.sh | 342 +++++ make/support/iigs/clang-smoke.sh | 65 + make/x68000.mk | 19 +- patches/mame-0.264-x68k-rs232.patch | 53 - patches/mame-0.264-x68k-waitstates.patch | 141 -- scripts/bench-amiga.sh | 16 +- scripts/bench-atarist.sh | 9 +- scripts/bench-dos.sh | 14 +- scripts/bench-iigs.sh | 16 +- scripts/bench-staxi-title.sh | 14 +- scripts/bench-x68000.sh | 18 +- scripts/captureDos.sh | 65 - scripts/check-blank.sh | 33 +- scripts/diag-staxi-screen.sh | 14 +- scripts/dosbox-386sx16.conf | 28 +- scripts/lib/joeyEnv.sh | 83 ++ scripts/make-agi-iigs-disk.sh | 13 +- scripts/make-iigs-boot-disk.sh | 13 +- scripts/make-iigs-disk.sh | 9 +- scripts/make-iigs-game-disk.sh | 13 +- scripts/profile-x68000.sh | 14 +- scripts/run-agi.sh | 277 ---- scripts/run-amiga-headless.sh | 107 -- scripts/run-amiga.sh | 159 -- scripts/run-atarist-headless.sh | 83 -- scripts/run-atarist.sh | 69 - scripts/run-dos.sh | 75 - scripts/run-iigs-mame.sh | 302 ---- scripts/run-iigs.sh | 610 -------- scripts/run-x68000.sh | 87 -- scripts/verify-amiga-audio.sh | 15 +- scripts/verify-amiga-input.sh | 11 +- scripts/verify-atarist-audio.sh | 9 +- scripts/verify-atarist-input.sh | 9 +- scripts/verify-dos-audio.sh | 35 +- scripts/verify-dos-input.sh | 20 +- scripts/verify-iigs-all.sh | 8 +- scripts/verify-iigs-audio.sh | 12 +- scripts/verify-iigs-game-disk.sh | 8 +- scripts/verify-iigs-input.sh | 12 +- scripts/verify-iigs-save.sh | 15 +- scripts/verify-iigs-serial.sh | 12 +- scripts/verify-iigs.sh | 12 +- scripts/verify-x68000-audio.sh | 13 +- scripts/verify-x68000-golden.sh | 18 +- scripts/verify-x68000-input.sh | 32 +- scripts/verify-x68000-mouse.sh | 33 +- scripts/verify-x68000-serial.sh | 16 +- toolchains/env.sh | 122 +- toolchains/install.sh | 1308 ----------------- tools/joeymod/joeymod | 10 +- 116 files changed, 5632 insertions(+), 4003 deletions(-) create mode 100644 examples/spacetaxi/assets/cpu6502.py create mode 100644 examples/spacetaxi/assets/dumpScenes.py create mode 100644 examples/spacetaxi/assets/romToLevel.py create mode 100755 install.sh create mode 100644 installer/components/amigaGcc.sh create mode 100644 installer/components/cadius.sh create mode 100644 installer/components/cwsdpmi.sh create mode 100644 installer/components/djgpp.sh create mode 100644 installer/components/dosbox.sh create mode 100644 installer/components/elf2x68k.sh create mode 100644 installer/components/emutos.sh create mode 100644 installer/components/fsuae.sh create mode 100644 installer/components/hatari.sh create mode 100644 installer/components/headless.sh create mode 100644 installer/components/host.sh create mode 100644 installer/components/iigsFirmware.sh create mode 100644 installer/components/kickstart.sh create mode 100644 installer/components/libxmp.sh create mode 100644 installer/components/llvm816.sh create mode 100644 installer/components/mame.sh create mode 100644 installer/components/merlin32.sh create mode 100644 installer/components/mintGcc.sh create mode 100644 installer/components/nasm.sh create mode 100644 installer/components/ntp.sh create mode 100644 installer/components/ptplayer.sh create mode 100644 installer/components/staxiData.sh create mode 100644 installer/components/x68kCgrom.sh create mode 100644 installer/components/x68kFirmware.sh create mode 100644 installer/distroPackages.txt create mode 100644 installer/files.sha1 create mode 100644 installer/lib/bundle.sh create mode 100644 installer/lib/common.sh create mode 100644 installer/lib/config.sh create mode 100644 installer/lib/distro.sh rename patches/mame-0.264-apple2gs-scc-clock.patch => installer/patches/mame/0001-apple2gs-scc-clock.patch (85%) create mode 100644 installer/patches/mame/0002-z80scc-rx-fifo.patch rename patches/mame-0.264-x68k-crtc-timing.patch => installer/patches/mame/0003-x68k-crtc-timing.patch (97%) rename patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch => installer/patches/mame/0004-x68k-waitstates-rs232-mouse.patch (66%) create mode 100644 installer/patches/mame/README.md rename {patches => installer/patches/mame/lua}/x68k-boot-check.lua (100%) rename {patches => installer/patches/mame/lua}/x68k-crtc-probe.lua (100%) rename {patches => installer/patches/mame/lua}/x68k-wait-probe.lua (100%) create mode 100755 joey-run create mode 100644 make/support/atarist/include-shim/math.h create mode 100644 make/support/atarist/include-shim/math_shim.c create mode 100755 make/support/dos/embed-dpmi.sh create mode 100755 make/support/iigs/clang-build.sh create mode 100755 make/support/iigs/clang-smoke.sh delete mode 100644 patches/mame-0.264-x68k-rs232.patch delete mode 100644 patches/mame-0.264-x68k-waitstates.patch delete mode 100755 scripts/captureDos.sh create mode 100644 scripts/lib/joeyEnv.sh delete mode 100755 scripts/run-agi.sh delete mode 100755 scripts/run-amiga-headless.sh delete mode 100755 scripts/run-amiga.sh delete mode 100755 scripts/run-atarist-headless.sh delete mode 100755 scripts/run-atarist.sh delete mode 100755 scripts/run-dos.sh delete mode 100755 scripts/run-iigs-mame.sh delete mode 100755 scripts/run-iigs.sh delete mode 100755 scripts/run-x68000.sh delete mode 100755 toolchains/install.sh 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