Optimising Quake on six old Macs

I forked QuakeSpasm, the long-running fork of FitzQuake maintained by Spike, Eric Wasylishen, Ozkan Sezer and others, to see what Claude Code could do under my direction across seven rounds on six old Macs from 1999 to 2019. I drove the methodology and the decisions. Claude wrote every line of C. The 1999 G3 went from unplayable to playable: demo3 at 1024×768 nearly quadrupled (5.10 → 19.80 fps, +288% over the early-port build at commit 4c165e6f), with translucent water, alias-model shadows and emissive-fullbright dynamic lights on. Every fps in this post is the engine's timedemo median of three runs.

The fork is at matthewdeaves/old-mac-quakespasm. Lava, slime and teleporters get the same translucent treatment as water; emissive dynamic lights make buttons, computer screens and tech panels cast coloured light into the room; the on-pickup HUD blink, broken upstream for years, is restored. A round v6 engine fix closes a long-standing X-ray glitch where translucent water used to reveal geometry through pool floors on un-watervis'd id1 maps. The 1999 G3 runs a tighter dynamic-light gate to buy back the muzzle-flash cost on its fragment-shader-less Rage 128; the two G4 Radeons add 16x anisotropic filtering and the GLSL water shader. Jump to the screenshots if you'd rather see it in motion.

QuakeSpasm on yosemite (1999 PowerMac G3, ATI Rage 128) — grenade in mid-air with explosion sparks
yosemite — 1999 PowerMac G3 / Rage 128 / Panther
QuakeSpasm on quicksilver (2001 PowerMac G4 733 MHz, Radeon 9000) — gibbed grunts on the floor with another in the doorway
quicksilver — 2001 PowerMac G4 / Radeon 9000
QuakeSpasm on mini-g4 (2005 Mac mini G4, Radeon 9200) — overhead view of an Ogre with chainsaw and visible alias-model shadow
mini-g4 — 2005 Mac mini G4 / Radeon 9200
QuakeSpasm on imac-2019 (2019 iMac 5K, Radeon Pro 580X) — slipgate hub with Q logo on floor
imac-2019 — 2019 iMac 5K / Radeon Pro 580X
Same source tree, same fat binary, four very different Macs. Click any to expand. Full per-machine galleries lower down.

#The bench of old Macs

The bench is six machines, named by their Apple codenames in the order they were built:

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
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 four PowerPC machines and the Lion-era Intel mini are the interesting targets. The 2019 iMac is along for the ride as a sanity-check baseline — same source tree, same fat binary, but with a current GPU that turns Quake into something it doesn't have to work for. It runs Quake at about 2000 fps; the bottleneck is the CPU side of the engine, not the graphics card.

The mini-intel pulls double duty: it's both a benchmark target and the cross-build host for every PPC slice, because it's the only modern-ish Mac that still ships Apple's PowerPC-targeting GCC 4.0.1 alongside the 10.3.9 and 10.4u SDKs. The whole bench sits next to the Ubuntu workstation, driven over ssh — I'm not clicking around any of the Mac UIs by hand, but I'm sitting next to them while they run.

The rule for the project is best looking, comfortably playable; never sacrifice visuals for fps. The playability floors are 60 fps on the G4s and Lion, 20 fps on the G3, and on the iMac there isn't one.

#The build and devops setup

Six old Macs benched against every code change sounds like it ought to involve sitting in front of one at a time with a keyboard and a stack of patience. It doesn't. The Macs are fully automated bench targets — they run the timedemos, write qconsole.log, and reboot themselves when needed — driven over ssh from the Ubuntu workstation. Displays stay on so I can watch the demos play, but I never click around any of the Mac UIs by hand. The Linux box is the orchestrator: git repo, Claude Code session, every build/deploy/bench script.

Architecture diagram showing Ubuntu workstation orchestrating six Mac bench targets via the Lion mini cross-build host
The orchestrator is Linux. The Macs are headless ssh targets. The Lion mini is both a bench target and the cross-build host.

You can't compile a binary that runs on a 1999 PowerPC G3 from a modern Linux box — you need a real Apple toolchain pointed at a real Apple SDK. So when Claude lands a code change on Ubuntu, the build flow is: rsync sources up to the Lion mini, ssh in to run make against the right SDK, scp the resulting binary back. Three slices come out:

  • g3 — PPC 750, 10.3.9 SDK, no AltiVec.
  • g4 — PPC 7400, 10.4u SDK, AltiVec on. One binary serves all three G4s.
  • lion — x86_64 against Lion's clang. Runs on both Intel Macs.

build-fat.sh runs the three sub-builds serially and lipo -creates them into one fat Mach-O. That binary plus one Quakespasm.app bundle ships to all six Macs unchanged, and dyld picks the right slice on each host. Within a slice (the Lion x86_64 build runs on both the mini-intel and the iMac, the G4 build runs on three different G4s), the engine queries sysctlbyname("hw.model", …) at boot and Cbuf_AddTexts the matching autoexec-<machine>.cfg from inside the bundle — that's how one fat binary truly serves all six machines, each with its own visual stack.

Build pipeline diagram showing three toolchains (g3 SDK, g4 SDK, lion clang) converging through lipo into one fat binary
Three toolchains, one binary. The trick is that lipo -create stitches the three Mach-O slices together so each Mac picks the one for its CPU at launch.

No CI, no containers, no Docker — plain rsync + ssh + scp on top of make. The one gotcha is that build-fat.sh has to hold a flock: running the three sub-builds in parallel races on the shared source tree on Lion, so they're serialised.

#The bench loop

Quake ships with three demo files (demo1, demo2, demo3) and a built-in timedemo command that plays back a demo as fast as the engine can render it, then prints the average frames per second to the console log. That gives a deterministic, repeatable number for any given (machine, demo, resolution, code) combination. A "cell" in this project is one such combination.

Bench loop showing deploy, launch with timedemo, poll qconsole.log, parse fps, append to results.csv
Per-cell bench loop: deploy, launch with the right autoexec, poll qconsole.log for the result, append a row to the rolling CSV. Three runs per cell; first is treated as warm-up and dropped.

Each cell runs three independent timedemos; the median of runs 2 and 3 lands in the CSV (run 1 is warm-up and dropped). Polling uses integer sleep because Panther's /bin/sleep from 2003 can't parse fractional seconds. The console log is written through raw write() with no stdio buffering, which is why polling it for a regex match actually works.

Reliable shutdown matters because the early rounds taught me what hard crashes do to a 1999 Mac. A bad code change on the G3 would freeze the engine in a state where the OS was still up but I couldn't see anything, so my only option was a hardware reset — and twice that corrupted enough of Panther's filesystem on the SSD that I had to reinstall OS X 10.3 from the install discs. Every kill in the bench scripts now goes TERM-then-grace-then-KILL and SDL_Quit is given time to unwind the OpenGL context, because Panther's Rage 128 driver leaves the display LUT in a corrupt state if the process is yanked mid-frame. The screen goes dark or shows garbage, the OS is alive, but you can't see what you're doing — and if you reach for the power button you might be reinstalling the OS for the next hour.

parallel-bench.sh fans the loop out across all six machines at once. Six concurrent ssh sessions, six concurrent timedemos, six concurrent log polls, all appending to the same CSV. Row appends are atomic on Linux under PIPE_BUF (4 KB) and a row is about 80 B, so concurrent legs don't interleave. Wall time is roughly the slowest leg — the G3, which takes minutes where the iMac takes about a second — so a full sweep is about as long as benching the G3 alone. (The dedicated runner matters: kicking off six manual bench.sh legs from one shell degrades the G3 reading, so the parallel sweep goes through this script.)

A second runner, screenshot.sh, drives the engine's screenshot tga console command through demo1/2/3 across all machines and rsyncs the per-machine galleries back; that's how the screenshot grids further down get captured. And qsreboot.sh is the unattended-recovery tool that issues an OS-level shutdown -r over ssh and waits for the machine to come back up — load-bearing on yosemite when the Rage 128 LUT corruption flips the screen black mid-sweep.

While a sweep runs, all six Macs play the demos in unison on the bench with the sound on: six chassis humming together, six idle notes, the Tiger and Panther boot chimes if anything triggers a reboot. I watch them play through while the terminal collects the rows.

#Claude Code tooling

Claude Code does the actual coding. To make that productive across seven rounds the repo carries scaffolding every fresh session inherits.

CLAUDE.md captures the durable knowledge a session needs up front: hardware inventory, naming conventions, the cvars roll for per-target toggles, the rule that every change ships behind an opt-in flag. MISTAKES.md is append-only and records every approach we tried and reverted with dates and reasoning. New sessions read both before proposing a change.

On top of those, a ppc-ops skill tells Claude when and how to invoke the build, deploy and bench scripts, and two slash commands wrap them with the right arguments — /bench mini-g4 demo3 1024x768 for one cell, /bench all for the parallel sweep, /deploy quicksilver to build and ship one slice. Together they stop Claude from inventing inline ssh + make heredocs every time a change needs benching, and post-run summaries include a 3% threshold check for "is this a real win".

The static-analysis battery is plumbed in similarly. analysis/ holds per-tool log files and a triage index, and the analysers run 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 any of the things gcc 15 does, so the Linux target catches a different class of issue.

A typical session: I describe a phase, Claude proposes a plan, runs the analysers, drafts the change, drives the bench loop across all six machines, commits the code change separately from the bench commit, and writes the MISTAKES.md entry if the experiment didn't pan out. I read the diffs, sanity-check the numbers, and decide what to ship.

#Frames per second across the rack

Numbers are the round v7 wrap full-grid bench at commit f2df151d. Every cell is the median of three runs (run 1 dropped as warm-up).

Current build, six machines, all three demos. Each cell is fps; column headers are demo × screen resolution (1024×768 or 640×480).

Machine demo1
1024×768
demo1
640×480
demo2
1024×768
demo2
640×480
demo3
1024×768
demo3
640×480
yosemite (G3) 16.55 35.20 15.20 33.40 19.80 36.75
sawtooth (G4) 42.65 55.90 35.40 55.10 46.75 57.30
quicksilver (G4) 64.20 71.95 62.45 72.10 86.15 98.25
mini-g4 (G4) 49.40 86.50 39.30 74.80 68.20 114.35
mini-intel (Lion) 73.05 165.35 54.55 132.15 44.70 189.00
imac-2019 1835.25 2048.60 1853.25 2018.55 1731.70 1807.00

What the work bought. Below is the BEFORE → AFTER for every cell where there's a clean pre-optimisation baseline. The "before" rows are the earliest bench captured for each machine; the "after" rows are the v7 wrap binary with the full per-machine visual stack on (translucent water, alias-model shadows, GLSL water on the programmable G4s and the iMac, 16x anisotropic filtering, gl_texture_lodbias -1.5, emissive-fullbright dynamic lights — none of which the early build had).

Machine Demo Resolution Before fps After fps Δ
yosemite (G3) demo1 1024×768 7.70 16.55 +115%
yosemite (G3) demo3 1024×768 5.10 19.80 +288%
sawtooth (G4) demo3 1024×768 39.45 46.75 +18.5%
sawtooth (G4) demo3 640×480 43.95 57.30 +30.4%
imac-2019 demo3 1024×768 1544.80 1731.70 +12.1%
imac-2019 demo3 640×480 1714.00 1807.00 +5.4%
mini-g4 (round v6→v7) demo3 1024×768 47.90 68.20 +42.4%
mini-g4 (round v6→v7) demo3 640×480 78.40 114.35 +45.9%

The G3 demo3 number is the headline: 5.10 → 19.80 fps, +288%. Quake on a 449 MHz G3 with a 16 MB Rage 128 sits at the 20 fps comfort floor instead of well below it. The shipping default for the G3 is now 640×480 (commit e0c66f57), where demo1, demo2 and demo3 all run at 33–37 fps with the same visual stack on. 1024×768 is still there via the console (vid_width 1024; vid_height 768; vid_restart). The plain-English version: the early port at 1024 was 5–8 fps unplayable, the v7 build is 15–20 fps at 1024 and 33–37 fps at the 640 default. Round v7's surprise came on the 2005 mini G4. Phase 1's DrawGLPoly sky-state client-state hoist landed +42% / +46% on demo3 there because the Radeon 9200's ATI driver pays a steep penalty for the per-call glEnableClientState / glDisableClientState traffic the earlier sky path was issuing. Same patch was within-noise on every other GPU.

Demo1 and demo2 are where the build trades headline fps for the visual stack the early build didn't have. Sawtooth's demo2 1024 went 54.60 → 35.40 against its vanilla 1615d99a baseline — the cost of four alpha cvars on a 1999 fixed-function GPU. The iMac is the same shape: demo1 1024 went 2042 → 1835, paying about 10% to render translucent liquid, alias-model shadows and emissive dlights. Every machine still clears its comfort floor on every cell. Three of the six (quicksilver, mini-g4, mini-intel) joined mid-project, so they don't have a clean pre-optimisation baseline. Quicksilver's nearest equivalent is the 1615d99a baseline at demo1 640 windowed = 127.10 fps. Mini-g4 only has a v6→v7 delta because its first reliable rolling-CSV row is the v6 wrap.

#Visual stack, per machine

The other side of the work is what each machine actually renders. Most rows in the table below are upstream cvars set per machine via an autoexec inside the .app bundle. The fork adds four cvars (r_dynamic_distance, r_shadow_distance, r_emissive_lights, gl_texture_lodbias) plus a v6 engine fix that makes translucent water render correctly on un-watervis'd id1 maps. The HUD pickup-blink row is a regression fix in upstream code; everything else is configuration.

Feature Origin yosemite (G3) sawtooth (G4 fixed-fn) quicksilver (G4 prog.) mini-g4 (G4 prog.) mini-intel (Lion) imac-2019
Translucent water (r_wateralpha 0.6) upstream cvar (default 1) yes yes yes yes yes yes
Translucent lava/slime/teleporters upstream cvars (default 0 = fall back to r_wateralpha, opaque) yes yes yes yes yes yes
Translucent water without X-ray (NoVisPVS trigger) new in fork (v6) yes yes yes yes yes yes
Alias-model shadows (r_shadows 1) upstream cvar (default 0) yes yes yes yes yes yes
Shadow distance gate new in fork 512 512 512 512 unlimited
Dynamic light distance gate new in fork 768 768
Emissive-fullbright dlights (r_emissive_lights) new in fork (v7) radius 0.5, max 4 radius 0.5, max 6 radius 1.0, max 12 radius 1.0, max 12 radius 0.75, max 8 radius 1.5, max 32
GLSL water shader upstream (r_oldwater 0) yes yes yes (driver path)
16x anisotropic filtering upstream cvar (default 1) yes yes yes yes
Trilinear filtering (GL_LINEAR_MIPMAP_LINEAR) upstream cvar yes yes yes yes yes
Sharper distant mips (gl_texture_lodbias -1.5) new in fork (v4) yes (likely inert on GeForce2 MX) yes yes
Skip framebuffer clear (gl_clear 0) upstream cvar (default 1) yes yes yes yes yes
Default ship resolution autoexec 640×480 (since e0c66f57) 1024×768 1024×768 1024×768 1024×768 2560×1440
HUD pickup blink restored fork bug-fix yes yes yes yes yes yes

The yosemite column also picks up two G3-only perf trims that aren't in the table because no other machine uses them: gl_subdivide_size 256 (coarser warp tessellation, ~4× fewer triangles per warp surface, matters because the Rage 128 is per-vertex-cost-sensitive 1999 silicon) and r_particles 2 (square particles instead of triangle splats — CPU-cheaper, no visual difference at distance).

Translucent water/lava/slime/teleporters. The four alpha cvars all ship in upstream QuakeSpasm but default to opaque. Setting them to 0.6 per machine makes those surfaces show the geometry underneath — the single biggest visible difference between the upstream defaults and what these Macs can do. On the Radeon 9000/9200 it's a single fragment-uniform read in the existing GLSL water path; on the fixed-function GPUs (Rage 128, GeForce2 MX, GMA 950) it's a textured warp with per-pixel alpha blending. Cost on every machine landed inside the noise floor.

Translucent water without the X-ray glitch (new code in the fork, v6). Setting the alpha cvars below 1 on stock id1 maps exposes a long-standing upstream bug: BSPs were compiled assuming opaque water, so leaves on the far side of a liquid surface aren't in PVS. The renderer ends up blending translucent water against whatever's in the framebuffer behind it (sky, void, distant unrelated geometry) — that's the X-ray-through-floors and flicker on e1m1 pools when r_wateralpha < 1. Round v6 ports Spike's Mod_FindContentsTransparent from Ironwail and adds a trigger in R_MarkSurfaces that drops to Mod_NoVisPVS whenever a liquid alpha is below 1 and the worldmodel isn't already vis'd transparent. Cost is one full world-mark per frame on un-vis'd maps. Watervis-patched custom maps stay on the optimal-PVS path.

Skipping the per-frame framebuffer clear. gl_clear defaults to 1 upstream. Setting it to 0 was the biggest single-pass perf win of round v5: Quake's world plus sky covers 100% of the screen in normal play so the per-frame glClear(GL_COLOR_BUFFER_BIT) is redundant fillrate work. Z-buffer correctness is preserved. +9.3% on mini-g4 demo3 1024 (the biggest win on the matrix), +7.1% on quicksilver, +4.7% on the G3, +3.5% on Lion — and a 2.1% regression on sawtooth. Sawtooth's GeForce2 MX driver has a slow no-clear path, unique among the five PowerPC-era GPUs, so sawtooth alone keeps gl_clear 1.

Dynamic-light distance gate (new code in the fork). The Rage 128 has no fragment shaders, so each dynamic light is a full extra blending pass over the affected surfaces. On a 449 MHz G3, a single muzzle flash turns into a GPU-bound stutter. Claude added r_dynamic_distance (default 0, engine behaviour preserved) that skips dlights past a squared-distance threshold; the check is in R_PushDlights. The G3 sets it to 768, buying back +5.4% on demo3 640. The G4s and Lion ship with the cvar at 0 because they don't pay the same cost.

Shadow distance gate (new code in the fork). Same idea for alias-model shadows, round v3 — r_shadow_distance set to 512 on sawtooth, quicksilver, mini-g4 and mini-intel so distant model shadows stop costing fillrate. The G3 leaves the gate at unlimited (alias-model shadows are cheap on a Rage 128 once you're not also paying the per-dlight cost), and the iMac has the headroom to draw every shadow at any distance.

Emissive-fullbright dynamic lights (new code in the fork, round v7). Quake's wall textures carry "fullbright" pixels — the bright bits on buttons, fluorescent fixtures, computer screens, tech panels — that the engine has always rendered as glow without any actual light spilling into the room. gl_emissive.c walks the world's brush surfaces at map load, name-filters textures (light/lite/button/btn/basebutn/comp/tech/panel/screen/switch/exit), computes seed positions, and stores up to 128 of them. Per frame, R_PushEmissiveLights picks the nearest N seeds within r_dynamic_distance, applies the per-machine radius scale, and injects up to r_emissive_lights_max into cl_dlights[] — the same pipeline muzzle flashes use, no GLSL, so it works on every GPU in the rack. Three new cvars (r_emissive_lights, r_emissive_lights_radius, r_emissive_lights_max), default off in code; the per-machine autoexecs turn them on with radius and max counts tuned to the GPU, so the build ships with emissive lighting on across all six machines.

Sharper distant mips on the programmable G4s (new cvar in the fork, round v4). Quicksilver and mini-g4 ship with gl_texture_lodbias set to -1.5. The Radeon driver picks a coarser mip than it should at oblique angles, so distant brick walls and slipgate signs go soft. A negative LOD bias pulls the sampler toward sharper mips — visibly cleaner at distance with no aliasing crawl at -1.5.

HUD pickup-blink restore (bug fix). Upstream had unconditionally killed the on-pickup blink for keys, runes, sigils and hipnotic/rogue items, leaving four downstream && flashon branches as silent dead code. The 5 Hz blink phase from cl.time is restored at sbar.c:657; the inventory icons twinkle when you pick up a sigil again.

#Screenshots from each machine

These are demo captures from the bundled fat binary running on each machine in turn — round v7 timestamp 20260509T155106Z. Same source tree, same three demos. The visual stack varies per target (different GPU classes, different cvar tunings) and these shots lean toward action, lighting, and the visible results of the v6 watervis fix.

# yosemite — PowerMac G3 B&W, ATI Rage 128, 1999

The hardest machine in the matrix. 449 MHz G3, 16 MB Rage 128, no fragment shaders, no AltiVec. Ships with translucent water/lava/slime/teleporters, alias-model shadows on, emissive-fullbright dlights (radius 0.5, max 4 — tightest in the matrix because every dlight on the Rage 128 is a full extra blend pass), dynamic-light distance gate at 768 (the project's headline G3 fix), classic warp water, framebuffer-clear skip. Default ship resolution is 640×480 since e0c66f57 — at 640 every demo runs at 33–37 fps; 1024×768 stays available via console for users who want it (demo3 sits right on the 20 fps floor at that resolution).

yosemite — three grunts patrolling the demon-banner corridor with the player wielding a grenade launcher
yosemite — banner corridor, three grunts on patrol
yosemite — grenade in mid-air with explosion sparks against a torch-lit stone wall
yosemite — grenade mid-flight, sparks and torch light
yosemite — underwater pool view showing the stone passage below the surface
yosemite — underwater pool, see-through water surface
yosemite — pillared hall with three torches glowing in alcoves and arches receding into darkness
yosemite — pillared hall, torch-lit alcoves
yosemite — combat in a tall stone room, ogres in doorways and grenade-explosion sparks raining through the air
yosemite — combat scene, ogres and grenade sparks
yosemite — Quake Q-rune logo banner glowing in the slipgate area
yosemite — Q-rune banner, slipgate area

# sawtooth — PowerMac G4 AGP, GeForce2 MX, 1999

Fixed-function G4. Same generation as the G3 in GPU class — no fragment shaders — but with AltiVec and a faster bus. Ships with translucent water, alias-model shadows (flipped on in commit f3bea2f2 for visual parity with the G3 — the per-machine bench is pending so the FPS table above predates this), shadow distance gate at 512, dynamic-light distance gate, emissive-fullbright dlights (radius 0.5, max 6), trilinear filtering, and the framebuffer-clear cvar at 1 (the GeForce2 MX driver alone has a slow no-clear path).

sawtooth — pillared hall with a distant grunt and ammo box, gibs on the floor
sawtooth — pillared hall, distant grunt
sawtooth — torch-lit pillared corridor, super-shotgun in hand
sawtooth — torch-lit corridor
sawtooth — pool surface seen from above with stonework visible underneath
sawtooth — translucent pool, see-through water
sawtooth — vertical wooden room interior with overhead torch and ledges
sawtooth — vertical room with torch lighting
sawtooth — Quake Q-rune logo banner glowing in the slipgate area
sawtooth — Q-rune banner, slipgate area
sawtooth — chaotic combat scene with explosions, gibs, and dynamic-light effects
sawtooth — combat with explosion lighting

# quicksilver — PowerMac G4 733 MHz, Radeon 9000 Pro, 2001

The G4 with the most visual headroom. Programmable pipeline, GLSL water shader, 16x anisotropic filtering, trilinear, sharper distant mips at gl_texture_lodbias -1.5, alias-model shadows on with 512-unit distance gate, emissive-fullbright dlights at radius 1.0 / max 12. Of the four PowerPC machines this is the one that looks closest to a modern Quake build at 1024x768.

quicksilver — looking down at an ammo crate in a wooden room with mossy stone walls
quicksilver — opening corridor, ammo crate
quicksilver — gibbed grunts on the floor with another grunt visible in the doorway
quicksilver — gibs aftermath, grunt in doorway
quicksilver — pillared corridor with torches and an enemy in the distance
quicksilver — pillared corridor, distant enemy
quicksilver — looking up at a wooden ceiling section, mossy stone walls and an open ledge above
quicksilver — vertical room, looking up
quicksilver — slipgate hub with sparks, the Q logo on the floor, glowing E letters and ornate carved walls
quicksilver — slipgate hub, sparks and runes
quicksilver — slipgate area with the Q-rune floor logo, glowing E sigils and a distant torch
quicksilver — Q-rune floor, glowing sigils

# mini-g4 — Mac mini G4 1.25 GHz, Radeon 9200, 2005

Inherits the Quicksilver visual stack — same GLSL water, same shadow gate, same anisotropic filtering, same gl_texture_lodbias -1.5, same emissive radius/max (1.0 / 12). Its Radeon 9200 is the same generation as the 9000 in the Quicksilver, so the visual character is essentially identical. The mini's higher clock (1.25 GHz vs 733 MHz) shows up in framerate, not in pixels.

mini-g4 — opening corridor with ammo crate, mossy stone walls
mini-g4 — opening corridor
mini-g4 — gibbed grunts on the floor with another grunt visible in the doorway
mini-g4 — gibs aftermath
mini-g4 — pillared corridor with torches and a distant enemy, super-shotgun in hand
mini-g4 — pillared corridor, distant enemy
mini-g4 — pool surface seen from above with stonework visible underneath through translucent water
mini-g4 — see-through pool, stonework below
mini-g4 — vertical wooden room interior with overhead torch and stairs
mini-g4 — vertical room, torch lighting
mini-g4 — overhead view of an Ogre wielding a chainsaw, with a visible alias-model shadow on the tiled floor next to the Q-rune teleport pad
mini-g4 — Ogre with visible alias-model shadow

# mini-intel — Mac mini Core 2 Duo, GMA 950, Lion, 2007

Intel hardware but with the GMA 950, which has no GLSL water path of its own — so it falls back to the classic warp like the PowerPC fixed-function GPUs. Anisotropic filtering at 16x, trilinear, shadows on at 512-unit gate, framebuffer-clear skip on, emissive-fullbright dlights at radius 0.75 / max 8 (dialled back from the G4 trio because GMA 950 is fillrate-modest). No gl_texture_lodbias because the GMA's GL 1.4 driver doesn't reliably expose EXT_texture_lod_bias. Fillrate-bound at 1024 on demo3 but easily breaks 200 fps at 640.

mini-intel — opening corridor with ammo crate, mossy stone walls
mini-intel — opening corridor
mini-intel — banner corridor with four grunts patrolling, demon banner on the wall
mini-intel — banner corridor, four grunts
mini-intel — pillared corridor with torches and a distant enemy
mini-intel — pillared corridor, distant enemy
mini-intel — pool surface seen from above with stonework visible through the translucent water
mini-intel — see-through pool
mini-intel — vertical wooden room interior with overhead torch and stairs
mini-intel — vertical room with torch
mini-intel — slipgate hub looking down at the Q-rune floor logo and ornate carved walls
mini-intel — Q-rune slipgate

# imac-2019 — iMac 27" 5K, Radeon Pro 580X, Sequoia

The modern outlier, captured at 2560×1440 native. Same Lion-target binary as the mini-intel (the engine still uses the GL 1.x fixed-function path because it links against the 10.7 SDK from 2011), but the GPU has so much headroom that the render is silky and the demo runs at ~2000 fps. Visual stack pushed to the maximum: aniso 16, trilinear, shadows on with no distance gate (r_shadow_distance 0 = unlimited), r_dynamic_distance 0 = unlimited, all four alpha cvars on, gl_clear 0, emissive-fullbright dlights at the matrix-widest radius 1.5 / max 32. Useful as the sanity-check baseline against which the cumulative CPU-side work shows up.

iMac 2019 — banner corridor at 1440p with four grunts and a demon banner on the wall
imac-2019 — banner corridor, four grunts
iMac 2019 — pillared room interior at 1440p, ammo crate visible
imac-2019 — pillared room, ammo
iMac 2019 — armoured pillared hall with armor pickup, ammo box and a distant grunt
imac-2019 — pillared hall, armor pickup
iMac 2019 — vertical wooden room with overhead torch and stairs
imac-2019 — vertical room with torch
iMac 2019 — overhead view of the slipgate hub showing the Q-rune floor logo and ornate carved walls
imac-2019 — slipgate Q-rune from above
iMac 2019 — slipgate hub at 1440p showing the Q-rune floor, ornate carved walls and glowing E-letter sigils
imac-2019 — slipgate hub, glowing sigils

#After seven rounds

The fun bit was hearing it. Six machines running the same demos, the PowerMacs humming with their fans up, speakers chiming through pickups and explosions, all of them whipping through the timedemos one after another. The Lion mini sat in the middle of it building the fat binary and orchestrating each bench machine over SSH, which is its own quiet bit of devops satisfaction.

The 1999 G3 going from unplayable to playable is the big one, but all six machines now run with translucent water, alias-model shadows and emissive dynamic lights on. The game looks fantastic on every one of them.

Quake II next, I think. Maybe some other games from when I was a kid.

#Get the build

The current release is v1.1-round-v7 — round v7 wrap plus emissive lights enabled per machine. Four download bundles:

Bundle Size Runs on
Quakespasm-fat-universal-ppc750-ppc7400-x86_64.zip 4.9 MB All six benched Macs (recommended)
Quakespasm-ppc750-Panther.zip 4.0 MB G3 / 10.3.9 Panther+
Quakespasm-ppc7400-AltiVec-Tiger.zip 4.0 MB G4 / 10.4 Tiger+
Quakespasm-x86_64-Lion-or-newer.zip 3.9 MB Intel / 10.7 Lion+ (verified to Sequoia 15.7)

The fat universal binary is one Mach-O with all three slices stitched together, so it runs unchanged on Panther 10.3.9 through Sequoia 15.7 — dyld picks the right slice on each host. The bundle expects to live at ~/Desktop/quake/; release notes have full install steps and Gatekeeper handling.