I forked QuakeSpasm to see if I could use Claude Code to get a build tuned for seven old Macs spanning 1999 to 2019: PowerPC G3, three G4s, a PowerPC G5 iMac on Leopard, a 2007 Intel Core 2 Duo on Lion, and a 2019 iMac as the modern baseline. It's a personal project: I love Quake and I collect and tinker with old Macs. One fat binary, one app bundle, a per-machine autoexec on each host.
# The bench
| Machine | Year | CPU | GPU | macOS |
|---|---|---|---|---|
| yosemite | 1999 | PowerMac G3 B&W, 449 MHz | ATI Rage 128, 16 MB | 10.3.9 Panther |
| sawtooth | 1999 | PowerMac G4 AGP, 500 MHz 7400 | NVIDIA GeForce2 MX, 32 MB | 10.4.11 Tiger |
| quicksilver | 2001 | PowerMac G4, 733 MHz 7450 | Radeon 9000 Pro, 64 MB | 10.4.11 Tiger |
| mini-g4 | 2005 | Mac mini G4, 1.25 GHz 7447A | Radeon 9200, 32 MB | 10.4.11 Tiger |
| imac-g5 | 2005 | iMac G5, 2.0 GHz PPC 970 | Radeon 9600, 128 MB | 10.5.8 Leopard |
| mini-intel | 2007 | Mac mini, 2.33 GHz Core 2 Duo | Intel GMA 950 | 10.7.5 Lion |
| imac-2019 | 2019 | iMac 27", 3.7 GHz i5-9600K | Radeon Pro 580X, 8 GB | 15.7.5 Sequoia |
# The setup
Seven headless Macs driven over ssh from an Ubuntu workstation. The Lion mini is both a bench target and the cross-build host: it's the last Mac that ships PPC-targeting GCC 4.0.1 alongside the 10.3.9 and 10.4u SDKs.
The build runs four sub-builds, one each for the G3, G4, G5 and Lion Intel, so four compilers produce four CPU-specific binaries. Apple's lipo tool glues them together into one universal file. On each Mac, macOS picks the slice for its CPU at launch time. Plain rsync + ssh + scp on top of make.
Quake's built-in timedemo plays a recorded demo as fast as the engine can render it and prints the fps to the console log. Each bench run is one combination of machine, demo, and resolution. Three runs per combination, the first dropped as a warm-up, median of the other two lands in the CSV. parallel-bench.sh runs the whole grid across all seven machines in parallel.
# Before and after
Both rows are demo timings on the 1999 G3: before, and after the work with all the visual features on (see-through water, drop-shadows under monsters, glowing dynamic lights).
| Machine | Demo | Resolution | Before fps | After fps | Δ |
|---|---|---|---|---|---|
| yosemite (G3) | demo3 | 1024×768 | 5.10 | 20.95 | +311% (4.1×) |
| yosemite (G3) | demo1 | 1024×768 | 7.70 | 17.35 | +125% (2.3×) |
| yosemite (G3) | demo3 | 640×480 | 15.60 | 36.85 | +136% (2.4×) |
| yosemite (G3) | demo1 | 640×480 | 23.90 | 34.45 | +44% |
Only the G3 row is positive across every cell. Every round on G3 chased fps and nothing else. The other rows in the full earliest-to-current grid are mixed because each machine's first build was plain stock QuakeSpasm with the engine's default settings, and the project has stacked visual upgrades on top since then: 16× anisotropic filtering, smoother texture filtering, drop-shadows under monsters, see-through water and lava and slime and teleporters, glowing dynamic lights. The negative cells are deliberate visual trade-offs, not regressions. Every feature landed in a commit that measured the fps cost across the matrix and weighed it against the visual gain.
# Framerate
Round v11.1 full-grid bench at commit d64427db. Median of three runs per cell. Round v11.1 added a per-frame GL state cache in R_DrawAliasModel that lands +19% to +46% on demo3 1024 across the G4/Intel/iMac slices. The G3 ppc750 slice compiles it out via QS_DISABLE_ALIAS_STATE_CACHE because same-session A/B showed a regression on the Rage 128.
| Machine | demo1 1024×768 | demo1 640×480 | demo2 1024×768 | demo2 640×480 | demo3 1024×768 | demo3 640×480 |
|---|---|---|---|---|---|---|
| yosemite (G3) | 17.35 | 34.45 | 15.25 | 33.40 | 20.95 | 36.85 |
| sawtooth (G4) | 40.25 | 55.65 | 32.70 | 50.15 | 46.90 | 57.55 |
| quicksilver (G4) | 62.75 | 70.30 | 60.10 | 68.00 | 84.05 | 95.35 |
| mini-g4 (G4) | 48.45 | 86.30 | 37.40 | 73.85 | 65.60 | 113.20 |
| mini-intel (Lion) | 72.85 | 163.95 | 54.50 | 130.70 | 44.60 | 185.90 |
| imac-2019 | 1610.95 | 1894.45 | 1490.20 | 1876.20 | 1575.15 | 1907.25 |
The iMac G5 isn't in the grid because it runs native 1440×900 only: its Leopard driver hangs on a mode switch, so it can't drop to the 1024×768 and 640×480 columns. At 1440×900 its demo1/2/3 turn in ≈ 100.8 / 74.4 / 86.1 fps.
# Screenshots from each machine
# yosemite: PowerMac G3 B&W, ATI Rage 128, 1999
# sawtooth: PowerMac G4 AGP, GeForce2 MX, 1999
# quicksilver: PowerMac G4 733 MHz, Radeon 9000 Pro, 2001
# mini-g4: Mac mini G4 1.25 GHz, Radeon 9200, 2005
# mini-intel: Mac mini Core 2 Duo, GMA 950, Lion, 2007
# imac-2019: iMac 27" 5K, Radeon Pro 580X, Sequoia
# Claude Code setup
CLAUDE.md is the durable knowledge a session needs: the hardware inventory, naming conventions, per-Mac engine-setting rules, and the rule that every change ships behind an opt-in flag (off by default until it's proven its worth). MISTAKES.md is append-only: every approach tried and reverted, with dates and reasoning.
A ppc-ops skill tells Claude when to invoke the build, deploy and bench scripts. Two slash commands wrap them: /bench mini-g4 demo3 1024x768 for one single run, /bench (no arguments) for the parallel sweep across all seven machines, and /deploy quicksilver to build and ship one slice to a named Mac.
Static analysis runs on a Linux build target compiling the same source the PPC and Lion binaries do: cppcheck, clang-tidy, scan-build, gcc -fanalyzer, flawfinder, sparse, iwyu, shellcheck, ASan and UBSan against a headless timedemo. Apple's gcc 4.0.1 from 2007 doesn't flag what gcc 15 does, so the Linux target catches a different class of issue.
# Get the build
The current release is v1.13, which added online multiplayer and weapon damage decals on top of the visual work.
| Bundle | Runs on |
|---|---|
| QuakeSpasm-OldMac-v1.13.dmg | All seven benched Macs (Panther 10.3.9 → Sequoia 15.7) |
One universal .app inside a disk image, built on Panther so it mounts on everything from 10.3.9 through modern macOS. Drag the app and quakespasm.pak into ~/Desktop/quake/ next to your own id1/. The release notes have full install steps and Gatekeeper handling.