joeylib2/make/iigs.mk

302 lines
16 KiB
Makefile

# 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 <GS/OS> -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