# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # Apple IIgs build rules (llvm-mos w65816: clang + llvm-mc + link816). # # 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 # dead-stripping. # # 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` (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. # # This file holds JoeyLib's library for the IIgs, programRules, the IIgs # rules for one program, which make/programs.mk instantiates for every # program, and the disk images made of programs (a project reaches all three # through make/project.mk). JoeyLib's own examples and the IIgs gates are at # the end. PLATFORM := iigs include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk 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-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) INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen LIB := $(LIBDIR)/libjoey.a PORT_C_SRCS := $(wildcard $(SRC_DIR)/iigs/*.c) # Hand-rolled asm, migrated to llvm-mc GAS .s. PORT_ASM_SRCS := $(wildcard $(SRC_DIR)/iigs/*.s) # Sprite codegen: the 65816 emitter (spriteEmitIigs.c) now lives in src/iigs/ # and is picked up by PORT_C_SRCS; only the CPU-independent compile dispatch # remains in src/codegen. CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteCompile.c $(REPO_DIR)/src/codegen/spriteStage.c LIB_C_SRCS := $(CORE_C_SRCS_NO_MIX) $(GENERIC_C_SRCS) $(PORT_C_SRCS) $(CODEGEN_SRCS) LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS) # Library header set for archive rebuild triggering (coarse on purpose: any # header change forces a full lib recompile -- a needless rebuild is safe, a # missed one is not). IIGS_LIB_HDRS := $(wildcard $(INCLUDE_DIR)/joey/*.h) $(wildcard $(SRC_CORE)/*.h) $(wildcard $(REPO_DIR)/src/codegen/*.h) $(wildcard $(SRC_DIR)/iigs/*.h) $(wildcard $(REPO_DIR)/src/m68k/*.h) # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # binary that ships next to the program; jlpAudioInit loads "ntpplayer" from # disk at run time (src/iigs/audio_full.c NTP_PATH), so every disk a program # goes on carries it (make-iigs-disk.sh). The source is assembled # with JoeyLib's interrupt window added to NTPstreamsound (NTP_PATCH). NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py NTP_BIN := $(BUILD)/audio/ntpplayer.bin IIGS_MERLIN := $(IIGS_AS) $(NTP_BIN): $(NTP_SRC) $(NTP_PATCH) $(IIGS_MERLIN) @mkdir -p $(BUILD)/audio @python3 $(NTP_PATCH) $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s mv $(BUILD)/audio/ntpplayer $@ # 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. # The real static library: compile every library TU (through the regalloc # fallback, matching clang-build.sh) + assemble every .s, then archive them # into libjoey.a. link816 reads the archive natively and --gc-sections selects # what each example references, so examples link $(LIB) exactly like the other # ports. `iigs-lib' is kept as the canonical build/compile-check entry point. $(LIB): $(LIB_SRCS) $(IIGS_LIB_HDRS) @mkdir -p $(OBJDIR) $(LIBDIR) @set -e; objs=""; \ for c in $(LIB_C_SRCS); do \ o=$(OBJDIR)/$$(echo $$c | sed 's#[/.]#_#g').o; \ echo " CC $$c"; \ $(CCWRAP) $(CLANG) $(CFLAGS) $(INCLUDES) -c $$c -o $$o; \ objs="$$objs $$o"; \ done; \ for s in $(PORT_ASM_SRCS); do \ o=$(OBJDIR)/$$(basename $$s .s).o; \ echo " AS $$s"; \ $(LLVM_MC) -arch=w65816 -filetype=obj $$s -o $$o; \ objs="$$objs $$o"; \ done; \ rm -f $@; \ echo " AR $@"; \ $(AR) rcs $@ $$objs iigs-lib: $(LIB) # ---- programs ----------------------------------------------------------------- # A program is an S16 OMF named NAME, in bin/. It runs off a ProDOS volume: # make-iigs-disk.sh packs programs, NTP and their DATA onto a data disk, and # make-iigs-game-disk.sh one program into a bootable volume (below). DATA is # staged apart for that, in build/iigs/data/NAME/DATA, and the optional files # in build/iigs/data/NAME/OPTIONAL (make/common.mk's programOptionalDir). PROGRAM_EXT := PROGRAM_DATA_APART := yes PROGRAM_OPTIONAL_APART := yes DEP_DIR := $(PROGRAM_BUILD)/dep # Its Finder icon (ID_ICON): an icon file, staged in its icon folder, which # the disk scripts put in the volume's Icons folder. programIconFile = $(call programIconDir,$(1))/finder.icons programTarget = $(call programBin,$(1)) $(call programIconFile,$(1)) # One program, $(1) its id: clang-build.sh compiles, links and wraps it, and # writes one header-dependency file for all its sources; iconBake.py bakes # its Finder icon. ID_IIGS_OVERFLOW_CAP # opts into large overflow segments (see clang-build.sh). NTP rides on every # disk a program goes on, so it is built first. define programRules $(if $(call programVar,$(1),OVERFLOW_CAP),$(call programBin,$(1)): export JOEY_OVERFLOW_CAP := $(call programVar,$(1),OVERFLOW_CAP)) $(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(IIGS_CLANG_BUILD) | $(NTP_BIN) @mkdir -p $$(dir $$@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/$(1).d $(call programVar,$(1),CFLAGS) $(INCLUDES) -o $$@ $(call programVar,$(1),SRCS) $(LIB) $(call programIconFile,$(1)): $(call programIcon,$(1)) $(ICON_BAKE_DEPS) @mkdir -p $$(dir $$@) $(ICON_BAKE) iigs $$< $$@ $(call programName,$(1)) endef -include $(wildcard $(DEP_DIR)/*.d) # ONE self-booting volume per program (ID_IIGS_GAME_DISK; iigs-game-disk # GAME=NAME for any): a GS/OS system stripped to what JoeyLib actually uses, # a launcher in place of the Finder, and the program in its own folder so # that folder can be copied to a hard drive and run from there. Boot to the # program is 21.8 s against 43.4 s for the two-floppy boot disk. The size is # chosen per program: one that fits beside a stripped system gets a bootable # 800KB 3.5" floppy, and a bigger one a 2048KB volume, which MAME can only # boot off a CFFA2 card because its 3.5 drive is 800KB GCR: # mame apple2gs -sl7 cffa2 -hard1 build/iigs/bin/NAME.2mg # GAME_DISK_SIZE forces a size. Each builds its OWN one-program data disk # (NAME-data.2mg) first rather than using JoeyLib's joey.2mg, which carries # the small examples and whose exact contents the gates depend on. IIGS_GAME_DISKS = $(foreach p,$(PORT_PROGRAMS),$(if $(call programVar,$(p),GAME_DISK),$(call programName,$(p)))) IIGS_GAME = $(or $(GAME),$(firstword $(IIGS_GAME_DISKS))) PORT_PACKAGES = $(if $(IIGS_GAME_DISKS),iigs-game-disks) # The commands that make program $$g's (a shell variable's) game disk. IIGS_GAME_DISK_CMDS = BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$$g" \ $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$$g-data.2mg && \ JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \ $(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/$$g.2mg $$g $$g $(BINDIR)/$$g-data.2mg .PHONY: iigs-game-disk iigs-game-disks iigs-verify-game-disk iigs-game-disk: programs $(NTP_BIN) g=$(IIGS_GAME); $(IIGS_GAME_DISK_CMDS) iigs-game-disks: programs $(NTP_BIN) @set -e; for g in $(IIGS_GAME_DISKS); do $(IIGS_GAME_DISK_CMDS); done # Boot one game disk headlessly and assert it reaches the program and draws. # An 800KB image goes in the 3.5 drive, a larger one on a CFFA2 card. iigs-verify-game-disk: iigs-game-disk $(REPO_DIR)/scripts/verify-iigs-game-disk.sh $(BINDIR)/$(IIGS_GAME).2mg # ---- JoeyLib's own build: its examples and the IIgs gates ----------------------- ifndef JOEY_PROJECT include $(REPO_DIR)/make/examples.mk include $(REPO_DIR)/make/programs.mk # Default: build (compile-check) the library. The MAME smoke test is # `iigs-clang-smoke`; disks and examples are `iigs-disk`. .DEFAULT_GOAL := all .PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-verify iigs-verify-all iigs-verify-audio iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail iigs-verify-tilebank clean-iigs clean all iigs: iigs-lib # Per-example launchable binaries with their DATA -- all of them: leaving # some out once made `make iigs` silently test 8-day-old binaries against new # assets. iigs-examples: programs # 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) with their DATA. NTP_BIN rides along so jlpAudioInit's # runtime load of "ntpplayer" finds it on the volume -- without it every # example logs "jlAudioOpen failed". iigs-disk: programs $(NTP_BIN) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg # Every launchable example on one image. The full set will not fit an 800KB # 3.5" floppy, so this is a 16 MB 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: programs $(NTP_BIN) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ JOEY_DISK_SIZE="16MB" \ JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SCROLL SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \ $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg # Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW # example off joey.2mg, and assert the SHR framebuffer was actually rendered # (many distinct colors -> pixels/lines/circles/tiles/flood all drew). Pass # VERIFY_EXAMPLE=name to check a different example. iigs-verify: iigs-disk $(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw) # Audio gate: AUDIOPRB (AUDIO pressing its own keys, make/examples.mk) off a # one-program disk of its own: NTP's sound interrupt installed, the DOC's # master volume up, and the tracker's, the noise's and the stream's slots # carrying waveforms (~10 s). iigs-verify-audio: $(call programBin,AUDIOPRB) $(NTP_BIN) $(REPO_DIR)/scripts/verify-iigs-audio.sh $(call programBin,AUDIOPRB) # Cross-channel SCC gate: opening the modem port must leave the printer channel # (AppleTalk's) transmitting. Builds its own one-example disk; ~2 min. iigs-verify-serial: $(call programBin,SCCPROBE) $(REPO_DIR)/scripts/verify-iigs-serial.sh # Receive-loss gate: 4096 bytes streamed at 9600 baud while the program does # nothing but full-screen presents must all arrive. Runs the modem port (SCC) # and a Super Serial Card in slot 2 (6551) on the installed MAME, with the SCC # receive FIFO cut to the real chip's 3 bytes (JOEY_Z80SCC_RX_FIFO=3) and the # card's a2ssc firmware from iigsFirmware; see the script header. ~3 min per mode. # The card runs at SERLOSS_SSC_BAUD (make/examples.mk says why). iigs-verify-serial-loss: $(call programBin,SERLOSS) $(call programBin,SERLOSSS) $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh scc SERLOSS_BAUD=$(SERLOSS_SSC_BAUD) $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh ssc # Boot-verify every launchable example on joey.2mg (per-example thresholds). iigs-verify-all: iigs-disk $(REPO_DIR)/scripts/verify-iigs-all.sh # Gate what the present leaves on the screen: every $E1 pixel byte equal to # the stage after a run of draws, the marks' asm included (shrParity), every # dirty band CLEAN again after its present (shrBands: a mark that skips its # 8-row group flag is never cleared, and the present can skip its rows), and # the 712 display-state bytes ABOVE the pixel block: the SCB array # ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the # tree can see them -- the golden hash covers the STAGE, not $E1, and P9's # pixel compare stops at $9CFF -- so a wrong palette is invisible to every # other gate and shows only as wrong colors on a real screen. That blind spot # is why a phantom "PEI-slam corrupts palettes 1-15" note survived five weeks. # # UBER's shrTailUpload / shrTailSlam rows live behind #ifdef UBER_PROBE, which # no normal build defines, so this target benches UBERPROB (make/examples.mk: # UBER built with them, into build/iigs/probe/bin) off a single-app disk of its # own. The goldened UBER binary, joey.2mg and joeylog.txt are all left # untouched. IIGS_PROBE_DIR := $(PROGRAM_BUILD)/probe iigs-verify-shrtail: $(call programBin,UBERPROB) $(NTP_BIN) BINDIR="$(call programDir,UBERPROB)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="UBER" \ $(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \ BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh @grep -E 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ { echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; } @test "$$(grep -cE 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt)" -eq 4 || \ { echo "iigs-verify-shrtail: FAIL - not every probe row ran" >&2; exit 1; } @grep -qE 'shrParity FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ { echo "iigs-verify-shrtail: FAIL - \$$E1 pixels mismatched the stage" >&2; exit 1; } || true @grep -qE 'shrBands FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ { echo "iigs-verify-shrtail: FAIL - a band stayed dirty past its present" >&2; exit 1; } || true @grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true @echo "iigs-verify-shrtail: PASS" # Tile-bank gate: TILEBANK loads this port's own .tbk bake both ways off its # own one-example disk; both must hash like the host gate's, and every other # target's bake must be refused. NTP_BIN keeps the disk's audio init quiet. iigs-verify-tilebank: $(call programBin,TILEBANK) $(TILEBANK_DATA) $(NTP_BIN) $(REPO_DIR)/scripts/verify-iigs-tilebank.sh # Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/ # Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar. iigs-verify-save: $(call programBin,SAVE) $(call programBin,DRAW) $(REPO_DIR)/scripts/verify-iigs-save.sh # End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating # the ported asm computes correctly on emulated hardware. iigs-clang-smoke: $(REPO_DIR)/make/support/iigs/clang-smoke.sh clean-iigs: rm -rf $(BUILD) # `clean` as an alias for `clean-iigs`. Without it a bare # `make -f make/iigs.mk clean` FAILS -- and with stderr discarded that # looks like success while leaving stale objects the next build links. clean: clean-iigs endif