This guide turns a Raspberry Pi 5 into an appliance that powers on straight into Mac OS 9.2.1, with working sound, a big-button game launcher a young child can operate alone, and a clean way to keep adding classic games (CD images, floppy images, installers).
It is written so someone comfortable with a Linux terminal — but new to QEMU and classic Macs — can follow it end to end.
What you provide: You must supply your own Mac OS 9 install media and game images. Mac OS 9 and its ROMs remain Apple's copyrighted property; this guide does not cover obtaining them.
- What you need
- Why a custom QEMU build (qemu-screamer)
- Build qemu-screamer on the Pi
- Install Mac OS 9.2.1
- The working launch command
- Boot the Pi straight into Mac OS 9 (appliance mode)
- Getting content into Mac OS 9
- Virtual CDs: running games that need their disc
- The kid-friendly Launcher
- Per-game launch scripts
- Troubleshooting
- Appendix: all the scripts in one place
- Raspberry Pi 5 (4 GB or 8 GB), running 64-bit Raspberry Pi OS (Bookworm) with the desktop.
- A Mac OS 9.2.x install ISO (this guide uses 9.2.1).
- Your game images (
.iso, hybrid Mac/PC discs, etc.). - Patience for one from-source compile (~20–40 minutes on a Pi 5).
A note on paths: this guide assumes your Linux username is adamf and that your
working files live under ~/macos9. Substitute your own username throughout
(whoami will tell you what it is).
The stock qemu-system-ppc you can install with apt emulates a PowerMac G4
(mac99) perfectly well — except it has no working sound. The PowerMac's audio
chip ("Screamer") is not in mainline QEMU. To get sound in Mac OS 9 you must build a
community fork that adds the Screamer device and ships a matching, sound-enabled
OpenBIOS.
A pleasant surprise: the screamer-v9.1.0 branch is rebased onto a modern QEMU
and its bundled pc-bios already handles video and color depth correctly — so on
this build you do not need any separate VGA/256-color driver patch that older
guides mention. One build gets you sound and correct video.
sudo apt update
sudo apt install -y git build-essential ninja-build meson pkg-config python3 \
libglib2.0-dev libpixman-1-dev libsdl2-dev libgtk-3-dev libvte-2.91-dev \
libpulse-dev libslirp-dev libfdt-dev zlib1g-dev flex bisonTwo of these are easy to overlook and cause silent failures later:
libpulse-dev— without it the build can't talk to the Pi's audio (PipeWire's PulseAudio layer), and you get a working emulator with no sound.libslirp-dev— required for-netdev usernetworking; without it the guest has no network.
Use screamer-v9.1.0, not the bare screamer branch (which is old QEMU 4.2-era
code that won't compile on the Pi's toolchain):
cd ~
git clone --recursive -b screamer-v9.1.0 https://github.com/mcayland/qemu.git qemu-screamer
cd qemu-screamer--recursive matters: it pulls the screamer-enabled OpenBIOS submodule.
mkdir build && cd build
../configure \
--target-list=ppc-softmmu \
--audio-drv-list=pa,sdl \
--enable-pa --enable-sdl --enable-gtk --enable-slirp \
--disable-werror
make -j4--disable-werrorstops a stray compiler warning (common when newer GCC meets older code) from aborting the whole build.- If
configurereports a missing dependency, it names it — install the matching-devpackage and re-run. - The finished binary lands at
~/qemu-screamer/build/qemu-system-ppc(modern QEMU is ninja-based and outputs tobuild/).
The sound-enabled QEMU must use the OpenBIOS that ships with it; the stock one
produces no sound. That BIOS is right inside your build, so simply point -L at it:
~/qemu-screamer/build/pc-bios
Verification trick: once Mac OS 9 is running, open the Sound control panel →
Output tab. If it lists "Spatializer Audio Laboratories", you are running the
correct screamer OpenBIOS. If it only shows "built-in," your -L is pointing at the
wrong BIOS folder and you'll get silence.
Create a hard-disk image for the OS to live on:
mkdir -p ~/macos9
qemu-img create -f qcow2 ~/macos9/macos9.img 4GBoot from the install ISO (note -boot d = boot from CD):
export QEMU_AUDIO_DRV=pa
~/qemu-screamer/build/qemu-system-ppc \
-M mac99,via=pmu -m 896 \
-L /home/adamf/qemu-screamer/build/pc-bios \
-prom-env 'vga-ndrv?=true' \
-hda /home/adamf/macos9/macos9.img \
-cdrom /home/adamf/macos9/MacOS921.iso \
-boot d -g 1024x768x32 -display sdl \
-device usb-kbd -device usb-mouse \
-netdev user,id=net0 -device sungem,netdev=net0Then, inside the emulator:
- Open Utilities → Drive Setup, select the uninitialized disk, click Initialize, and format it as Mac OS Extended.
- Run Mac OS Install and install onto that freshly formatted disk.
- When it finishes, Special → Shut Down.
After install, you boot from the hard disk by switching -boot d to -boot c and
removing the -cdrom line (see the next section).
Sound prerequisite: keep guest RAM under 1024 MB (we use
-m 896), and after first boot open the Memory control panel and turn Virtual Memory ON. The screamer audio will not work without both.
This is the everyday command once the OS is installed. Save it as a script
(~/macos9/start-os9.sh).
#!/bin/bash
export QEMU_AUDIO_DRV=pa
~/qemu-screamer/build/qemu-system-ppc \
-M mac99,via=pmu -m 896 \
-L /home/adamf/qemu-screamer/build/pc-bios \
-prom-env 'vga-ndrv?=true' \
-hda /home/adamf/macos9/macos9.img \
-boot c -g 1024x768x32 -display sdl \
-device usb-kbd -device usb-mouse \
-netdev user,id=net0 -device sungem,netdev=net0 \
-full-screen
sudo poweroff # optional: power the Pi off when Mac OS 9 is shut downMake it executable: chmod +x ~/macos9/start-os9.sh
Key choices, and why:
| Flag | Why it's there |
|---|---|
QEMU_AUDIO_DRV=pa |
This screamer fork predates QEMU's modern audio API, so -audiodev errors out. The environment variable is the correct way to route sound to PipeWire. |
no -device screamer |
On this fork the Screamer is wired into the mac99 machine automatically. Adding it as a device is unnecessary (and it isn't a pluggable device). |
-m 896 |
Under the 1024 MB ceiling required for sound. |
-display sdl |
The default GTK/Wayland path makes the mouse uncontrollable in Mac OS 9. SDL fixes the pointer. |
-device usb-mouse (not usb-tablet) |
For 9.2.x, a plain USB mouse + keyboard is the reliable combo. The tablet is experimental and tends to freeze. |
-full-screen |
Hides the Linux desktop so it feels like a real Mac. |
sudo poweroff |
Makes "Shut Down" in Mac OS 9 power off the Pi, like real hardware. Optional. |
For passwordless poweroff, add a sudoers rule with sudo visudo:
adamf ALL=(ALL) NOPASSWD: /sbin/poweroff
Goal: power on → Mac OS 9 fullscreen, no Linux visible, no typing.
sudo raspi-configSystem Options → Boot / Auto Login → Desktop Autologin. While you're there, Display Options → Screen Blanking → disable, so the screen doesn't blank during play.
Raspberry Pi OS Bookworm defaults to the labwc Wayland compositor (Wayfire on older releases). Rather than edit a compositor-specific config, use the freedesktop autostart folder, which works on X11, Wayfire, and labwc:
mkdir -p ~/.config/autostart
nano ~/.config/autostart/macos9.desktop[Desktop Entry]
Type=Application
Name=Mac OS 9
Exec=/home/adamf/macos9/start-os9.sh
Terminal=false
X-GNOME-Autostart-enabled=trueReboot. The Pi should go: power-on → (brief desktop flash) → Mac OS 9 fullscreen. Set a black desktop wallpaper in Linux to hide that momentary flash.
Mac files carry resource forks and type/creator codes that plain Linux file copies strip away. Data files (images, ROMs, save files) survive a raw copy; applications do not — they arrive as dead, blank-icon files. Choose the transfer method accordingly.
A second disk image you pass between Linux and Mac OS 9 like a USB stick.
qemu-img create -f raw ~/macos9/exchange.img 2GAdd it to the launch command:
-drive file=/home/adamf/macos9/exchange.img,format=raw,media=diskFormat it once, inside Mac OS 9 with Drive Setup (an OS-9-initialized disk gets
the Apple partition map QEMU expects; a bare mkfs from Linux often won't mount).
Golden rule: never mount the exchange disk in Linux and Mac OS 9 at the same time — that corrupts it. Shut QEMU down first.
Mounting it on the Pi (QEMU off):
sudo losetup -fP --show ~/macos9/exchange.img # prints e.g. /dev/loop0
lsblk /dev/loop0 # find the HFS+ partition (usually p2)
sudo mkdir -p /mnt/exchange
sudo mount -t hfsplus /dev/loop0p2 /mnt/exchange
# ...copy files...
sudo umount /mnt/exchange
sudo losetup -d /dev/loop0If it mounts read-only or complains it wasn't cleanly unmounted, add -o force. If
hfsplus is unknown, sudo apt install hfsprogs && sudo modprobe hfsplus.
.sit archives (StuffIt — the classic Mac equivalent of .zip) are a great fit for
this route: they're a single data-fork file, so they survive the Linux copy intact,
and you expand them inside Mac OS 9 with StuffIt Expander (drag the file onto
its icon if double-click doesn't work).
For transferring whole apps, FTP with MacBinary keeps the resource fork intact. The robust direction through QEMU's NAT is to run the FTP server on the Pi and an FTP client inside Mac OS 9 (Fetch/Anarchie) in passive mode — that needs no port forwarding at all. Alternatively, run NetPresenz in Mac OS 9 and forward a port:
-netdev user,id=net0,hostfwd=tcp::2121-:21 -device sungem,netdev=net0Attach an ISO as a CD:
-cdrom /home/adamf/macos9/games/somegame.isoTo swap discs without rebooting, expose the QEMU monitor and use change. Add a
monitor socket to the launch command:
-monitor unix:/tmp/qemu-mon,server,nowaitThen from any shell:
echo "change ide1-cd0 /home/adamf/macos9/games/nextgame.iso" | socat - UNIX-CONNECT:/tmp/qemu-mon(If change says no such device, run info block in the monitor to see the real
CD device name.)
The emulated G4 has no floppy drive (-fda fails on mac99). Handle floppy
images entirely inside Mac OS 9: transfer the .img/.dsk in, then drag it onto
Disk Copy (built in, version 6.3.3), which mounts it like an inserted floppy.
.smi files are self-mounting — just double-click. To extract files from an HFS
floppy on the Pi instead: hmount floppy.img, hls, hcopy -m "File" ./File,
humount (from the hfsutils package; -m preserves the resource fork).
Most classic games expect their CD in the drive at runtime and won't install fully to disk. Swapping physical CDs by hand is painful and not something a child can do. The fix is the Virtual CD/DVD-ROM Utility (from Macintosh Garden), which mounts an ISO file so games believe a real disc is present — it passes most copy-protection checks that plain Disk Copy mounts fail.
Setup, once:
- Get the Utility into Mac OS 9 and expand it (it includes a companion AutoTyper).
- Run each game ISO through the AutoTyper once — this flags the file so it mounts as a CD when opened (double-click or via a script).
- Store all your game ISOs on a big second disk image (
games.img, format in Drive Setup) so Mac OS 9 always sees the files.
Hybrid discs mount two volumes. A Mac/PC hybrid ISO appears as two desktop
icons: the sane-named, disk-icon volume is the HFS (Mac) side — use this one
for launching; the short-named, DVD-icon volume is the ISO 9660 side (names
get truncated/uppercased, e.g. ODELL_DOWN_UNDER) — ignore it for launching, but
remember it when ejecting (see below).
Note: a few games used deeper disc-geometry protection and may still refuse a virtual
mount. For those rare cases, mount that one game's ISO via -cdrom at boot.
Mac OS 9 has a built-in Launcher that is perfect for a young child: a window of big, single-click buttons.
- Make an alias of each game (or each launch script — see next section): select it, File → Make Alias.
- Drop the aliases into System Folder → Launcher Items.
- Open Launcher (Control Panels), ⌘-click the window, choose Large buttons.
- In General Controls, tick Show Launcher at system startup.
Now: power on → Mac OS 9 → full screen of big game buttons → she taps one to play.
To give each button the game's real artwork: Get Info (⌘-I) on the game/CD, click its icon top-left, ⌘-C; then Get Info on your launcher app, click its icon, ⌘-V. (Set icons after the file is in Mac OS 9 — copying through the Linux mount strips them.)
For a seamless one-tap experience — especially for games that need their CD mounted before the app starts — use a small AppleScript per game that mounts the disc, launches the game, waits for it to quit, then ejects everything. Each script is saved as an application and placed in the Launcher.
Dependencies (install into System Folder → Scripting Additions):
- Jon's Commands — sets screen resolution and color depth.
- ProcessInfo — helps the wait-loop detect when the game has quit.
Both are on Macintosh Garden, distributed as .sit.
In Script Editor, File → Save As, and set Kind = Application (not "Script", which opens in the editor when clicked; not "Run Only", which strips the source you still want to edit). Tick Never Show Startup Screen. Confirm with Get Info that it reads "Kind: application program".
The game app lives on the mounted CD.
-- (1) Mount the CD (an AutoTyped ISO on your games disk)
open file "Games:RR_1st_Grade_Mac.iso"
delay 3 -- let the virtual CD appear
-- (2) Set display (copy the exact depth/resolution line from Jon's Commands;
-- 256 colors = depth 8; pick 640x480 or 800x600 to match the game)
-- (3) Launch the app ON THE MOUNTED CD (path starts at the Mac volume's name)
tell application "Finder"
open file "Reader Rabbit 1st Grade:Reader Rabbit 1st Grade"
end tell
-- (4) Wait for it to quit
delay 5
repeat while (exists process "Reader Rabbit 1st Grade")
delay 2
end repeat
-- (5) Clean up: eject every removable disc (handles both hybrid volumes)
tell application "Finder"
eject (every disk whose ejectable is true)
end tellThe app is installed on the hard disk; the CD must be mounted first so the app finds its data. Only the launch path changes (it points at the installed app), and the mount-before-launch order — which the script guarantees — is what makes it work.
-- (1) Mount the data CD
open file "Games:JumpStart 1st Grade 2000.iso"
delay 3
-- (2) Set display (Jon's Commands; depth 8 for 256-color titles)
-- (3) Launch the INSTALLED app on the hard disk
tell application "Finder"
open file "Macintosh HD:Games:JumpStart 1st Grade:JumpStart 1st Grade"
end tell
-- (4) Wait for it to quit
delay 5
repeat while (exists process "JumpStart 1st Grade")
delay 2
end repeat
-- (5) Clean up
tell application "Finder"
eject (every disk whose ejectable is true)
end tell- Get exact disc names by running, while mounted:
tell application "Finder" to get name of every disk. Hybrid ISO volume names are often truncated in Get Info, so don't type them from memory. - Eject everything (
eject (every disk whose ejectable is true)) is more robust than naming volumes — it clears both halves of a hybrid disc automatically. - If the wait-loop never ends, the process name is wrong; use ProcessInfo to find the real one.
- Your three button types — self-contained CD (alias of AutoTyped ISO or Template A), installed-plus-CD (Template B), and fully-installed (alias of the app) — all look identical to the child. Only the wiring differs.
No sound. Check the Sound control panel shows "Spatializer Audio Laboratories"
(confirms correct OpenBIOS via -L); confirm QEMU_AUDIO_DRV=pa; confirm guest RAM
< 1024 MB and Virtual Memory is ON. Expect the audio to be somewhat crackly/choppy
even when correct — that's normal for the screamer fork. Make QEMU the only thing
using the Pi's audio.
Mouse drifts or becomes uncontrollable. Use -display sdl. Use -device usb-mouse, not usb-tablet. Ctrl+Alt+G releases a captured pointer back to Linux.
make fails on the build. With --disable-werror most warnings won't stop it; if
a hard error appears, it's usually one file needing a small patch on newer GCC.
Login loop in Raspberry Pi OS (screen flashes back to the login prompt). Common
causes, checked from a text console (Ctrl+Alt+F2): a root-owned ~/.Xauthority (from
running a GUI app with sudo) — chown it back or delete it; a full disk (df -h);
or missing desktop packages — sudo apt install --reinstall raspberrypi-ui-mods lxsession openbox lightdm after sudo dpkg --configure -a.
Autostart .desktop doesn't fire. Verify the Exec= path and that the script is
executable; test the script by hand first. As a fallback, a systemd user service
launches independently of the compositor.
Game screen is garbled. It likely needs 256 colors — set the depth in the Monitors control panel (and in the per-game script via Jon's Commands).
#!/bin/bash
export QEMU_AUDIO_DRV=pa
~/qemu-screamer/build/qemu-system-ppc \
-M mac99,via=pmu -m 896 \
-L /home/adamf/qemu-screamer/build/pc-bios \
-prom-env 'vga-ndrv?=true' \
-hda /home/adamf/macos9/macos9.img \
-boot c -g 1024x768x32 -display sdl \
-device usb-kbd -device usb-mouse \
-netdev user,id=net0 -device sungem,netdev=net0 \
-full-screen
sudo poweroff[Desktop Entry]
Type=Application
Name=Mac OS 9
Exec=/home/adamf/macos9/start-os9.sh
Terminal=false
X-GNOME-Autostart-enabled=true# exchange disk (data transfer)
-drive file=/home/adamf/macos9/exchange.img,format=raw,media=disk
# games disk (holds AutoTyped ISOs)
-drive file=/home/adamf/macos9/games.img,format=raw,media=disk
# a game CD at boot (for the rare game that refuses a virtual mount)
-cdrom /home/adamf/macos9/games/somegame.iso
# monitor socket (scripted disc hot-swap)
-monitor unix:/tmp/qemu-mon,server,nowait
# FTP port forward (if running NetPresenz inside Mac OS 9)
-netdev user,id=net0,hostfwd=tcp::2121-:21 -device sungem,netdev=net0sudo losetup -fP --show ~/macos9/exchange.img
lsblk /dev/loop0
sudo mount -t hfsplus /dev/loop0p2 /mnt/exchange # add -o force if needed
# ...copy files...
sudo umount /mnt/exchange
sudo losetup -d /dev/loop0Built and tested on a Raspberry Pi 5 (64-bit Raspberry Pi OS, Bookworm) with the
qemu-screamer screamer-v9.1.0 fork and Mac OS 9.2.1. The screamer build's bundled
pc-bios provided correct video and color depth, so no separate VGA driver patch was
needed.