Skip to content

Native macOS (Apple Silicon / arm64) build#4984

Closed
matthewdeaves wants to merge 5 commits into
dkfans:masterfrom
matthewdeaves:macos-arm64-build
Closed

Native macOS (Apple Silicon / arm64) build#4984
matthewdeaves wants to merge 5 commits into
dkfans:masterfrom
matthewdeaves:macos-arm64-build

Conversation

@matthewdeaves

@matthewdeaves matthewdeaves commented Jul 5, 2026

Copy link
Copy Markdown

Adds a native Apple Silicon (arm64) build of KeeperFX — a real Mach-O binary built with clang and Homebrew libraries, no Wine or emulation.

I saw #4552 is already working toward macOS support (thanks @PieterVdc). This is an independent take that I've been able to build and test on real hardware through to gameplay, so hopefully it's useful — either on its own or to compare against. Happy to adjust or combine.

📥 Try it (Apple Silicon)

A ready-to-run, self-contained KeeperFX.app is published here — no build needed:
https://github.com/matthewdeaves/keeperfx/releases/tag/macos-latest

Download and unzip it, drop KeeperFX.app next to your existing KeeperFX game data, and double-click. On first launch, right-click → Open if Gatekeeper prompts, and click Allow if macOS asks for folder access. (For anyone on #4552 who wanted to test on a real Mac — @EugenHotaj, @AI-Guru — this should make it a one-click try.)

What's included

  • Portability edits to a few shared headers: unpack some in-memory-only #pragma pack(1) structs (they hold pointers, and arm64's linker rejects unaligned statically-initialized pointers) and widen one unix include guard. No behaviour change on Windows — the struct layouts are unchanged on the 32-bit build, and the serialized / size-critical structs stay packed.
  • macos.mk + tools/build_macos_deps.sh to build the engine and its three source-only deps (plain -arch arm64, no deprecated linker flags).
  • tools/make_macos_app.sh — an optional packager that produces a self-contained KeeperFX.app. It bundles all non-system dylibs — including the ones loaded at runtime via dlopen, which otool -L can't see: the SDL3 that Homebrew's sdl2-compat loads, and the SDL2_mixer audio decoders — and then verifies every Mach-O in the bundle is free of /opt/homebrew and /usr/local paths. The engine self-locates its data next to the .app, and the bundle is ad-hoc signed with folder-access usage strings, so macOS shows a normal permission prompt on first launch and it runs from any location (Desktop included).
  • Docs (docs/MACOS_ARM64_PORT.md) and a README section.
  • A GitHub Actions workflow that builds and packages it on the macos-15 Apple Silicon runner.

How I tested it

Built and played on a MacBook Air (M5, macOS 26): main menu → land view → into level 1 of the original campaign. The original game data came from the Dungeon Keeper Gold release (its macOS app ships the DATA/SOUND files uncompressed). CI is green.

Notes / limitations

  • Tested on Apple Silicon only (not Intel).
  • The .app is ad-hoc signed, not notarized — first launch may need right-click → Open.
  • You still bring your own original Dungeon Keeper data and a KeeperFX data release; this only adds the native binary.

Disclosure: I put this together with the help of Claude Code, building and testing each step locally and in CI.

matthewdeaves and others added 2 commits July 5, 2026 18:03
Prep the shared source for non-DOS/Windows targets (native Linux and
macOS/Apple Silicon). No behavioural change on the existing builds — every
struct touched here is an in-memory-only runtime table, never serialized, and
its layout is unchanged on the 32-bit build (the members were already at
natural offsets).

- globals.h: include <unistd.h>/<signal.h> on __unix__ and __APPLE__ too,
  not only the legacy `unix` macro, so the POSIX paths compile off-DOS.
- Stop byte-packing in-memory-only structs (GUI buttons/menus, config
  NamedCommand/NamedField/ConfigFileData, GamekeySettings, DemoItem, and the
  TbLoadFiles/TbLoadFilesV2 loader lists). These hold pointers and function
  pointers; on arm64 a statically-initialized #pragma pack(1) struct places
  those pointers at unaligned offsets the linker rejects. Unpacking them lets
  the modern macOS linker emit them with no deprecated -ld_classic workaround.
  The serialized NetMessage struct and size-critical game-state structs stay
  packed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a native Apple Silicon build path that compiles KeeperFX to a
Mach-O arm64 binary, no emulation. Verified end-to-end: it launches,
loads original DK data, and runs into live gameplay.

- macos.mk: clang build using Homebrew libs via pkg-config
  (sdl2 + image/mixer/net, ffmpeg, luajit, openal-soft, libspng, minizip,
  miniupnpc, libnatpmp, zlib, curl). Uses -arch arm64 with no deprecated
  linker flags: the in-memory packed-pointer tables are unpacked in the
  shared headers, so the modern linker aligns every static pointer itself.
- tools/build_macos_deps.sh: builds astronomy/centijson/enet6 from source
  as arm64 static libs into deps/ where macos.mk expects them.
- .gitignore: exclude /game (a staged runtime install of copyrighted DK
  assets, which must never be committed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@EugenHotaj

Copy link
Copy Markdown

@matthewdeaves nice just tried it, this is very cool! The game starts up and I'm able to go into the first level but otherwise does not work: the imps are just stuck and don't do anything. Some game sounds also don't seem to be working. But this is very promising!

@matthewdeaves

Copy link
Copy Markdown
Author

Quick testing update: I've now played through level 1 and then level 2 of the original campaign back-to-back without issue on my MacBook Air (M5, macOS 26).

@matthewdeaves

matthewdeaves commented Jul 5, 2026

Copy link
Copy Markdown
Author

@matthewdeaves nice just tried it, this is very cool! The game starts up and I'm able to go into the first level but otherwise does not work: the imps are just stuck and don't do anything. Some game sounds also don't seem to be working. But this is very promising!

@EugenHotaj what type of Mac and version of MacOS are you running it on? I've just been playing perfectly on my MacBook Air through level 1 and 2. What is the source of the base original game files on your setup? I got mine from the DK1 gold edition on gog.com (bought today for ~99p)

@matthewdeaves

Copy link
Copy Markdown
Author

For reference, here's exactly what my working setup looks like on the MacBook Air (M5, macOS 26) — KeeperFX 1.4.0.5204 (53e67ff), original data from DK1 Gold (GOG). Worth diffing your folder structure and a couple of key file sizes against this, since "loads but imps don't act + no sound" is almost always the original game data rather than the arm64 binary.

Folder layout (KeeperFX.app sits alongside the game data):

KeeperFX-AppleSilicon/
├── KeeperFX.app
├── keeperfx.cfg
├── campgns/  creatrs/  fxdata/  ldata/  levels/
├── mods/  multiplayer/  music/  save/  scrshots/
├── data/    <- original DK1 + KeeperFX data (120 files)
└── sound/   <- original DK1 sound banks

Key original-game files and sizes (these must come from your original DK1 data — this is the usual culprit):

File Size (bytes) Note
sound/sound.dat 43,188,782 big uncompressed bank; if yours is far smaller, that's your "no sound"
sound/atmos1.sbk 508,146
sound/atmos2.sbk 506,838
sound/bullfrog.sbk 508,142
sound/speech_eng.dat 5,697,822
data/creature.jty 13,162,585 if mismatched/wrong version, creatures don't behave
data/creature.tab 146,656
data/palette.dat 768

If your sound.dat / data/ files differ a lot in size from these, the fix is to re-copy the uncompressed DATA/SOUND from a DK1 Gold install (the GOG macOS app ships them uncompressed) rather than from a compressed or older release.

What's the source of your original files, and what size is your sound/sound.dat?

matthewdeaves referenced this pull request in matthewdeaves/keeperfx Jul 5, 2026
…check

Bring the fork's release packager in line with PR #4984's macos-arm64-build
branch. dylibbundler only follows LC_LOAD_DYLIB entries, so libraries loaded at
runtime via dlopen()/SDL_LoadObject are invisible to it and must be added by
hand. Generalise the SDL3 special-case into an add_runtime_dylib() helper that
copies each such library in, rewrites its install id, and re-runs dylibbundler
over it (with --overwrite-files, so it does not erase Contents/libs) to pull in
its own transitive deps. Also mirror SDL2_mixer's decoder set defensively, and
verify every Mach-O in the bundle is free of /opt/homebrew and /usr/local paths.

This keeps the published KeeperFX.app.zip download built with the same packager
the PR ships. Verified on an M5 with Homebrew hidden from the loader: zero libs
resolved from /opt/homebrew and no loader errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matthewdeaves and others added 3 commits July 5, 2026 21:33
Package bin/keeperfx into a redistributable dist/KeeperFX.app that runs on any
Apple Silicon Mac with no Homebrew, and behaves like a normal Mac app:

- Self-locating engine (src/linux.cpp): the engine is the bundle's main
  executable. Finder launches apps with cwd=/, so on startup it resolves its
  own path and, when running from inside a *.app/Contents/MacOS/, chdir's to the
  folder containing the .app — where the user dropped the game data. Launched
  outside a bundle (a dev running bin/keeperfx) the working directory is left
  alone. This replaces an earlier bash-wrapper launcher.
- tools/make_macos_app.sh bundles all non-system dylibs via dylibbundler, with
  load paths rewritten to @executable_path/../libs. dylibbundler only follows
  LC_LOAD_DYLIB entries (what `otool -L` shows), so libraries pulled in at
  runtime via dlopen()/SDL_LoadObject are invisible to it and are added by an
  add_runtime_dylib() helper that copies each one in, rewrites its install id,
  and re-runs dylibbundler over it (with --overwrite-files, so it does not erase
  Contents/libs) to pull in that library's own transitive deps. This covers:
    * libSDL3 — Homebrew's sdl2 is sdl2-compat, which dlopens SDL3 at runtime;
      the shim's first search path is @loader_path/libSDL3.dylib (unversioned).
    * SDL2_mixer's audio decoders (vorbis/FLAC/mpg123/opus/wavpack/...), mirrored
      defensively under both versioned and unversioned names.
  A final pass then verifies every Mach-O in the bundle (main binary + all
  bundled dylibs) is free of /opt/homebrew and /usr/local references.
- Info.plist declares NSDesktop/Documents/DownloadsFolderUsageDescription and
  the whole bundle is ad-hoc signed, so macOS shows a normal permission prompt
  the first time it reads data from a privacy-protected folder (Desktop/
  Documents/Downloads). Click Allow once and it runs from any location.
- Generates the .icns from res/ PNGs via iconutil.

.gitignore: exclude /dist (build output of the bundler).

Verified on an M5 MacBook Air with Homebrew hidden from the loader
(DYLD_FALLBACK_LIBRARY_PATH=/nonexistent): the app launches, loads zero
libraries from /opt/homebrew, and resolves every dependency — including the
dlopen'd libSDL3 and all music decoders — from inside the bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/MACOS_ARM64_PORT.md: the full port write-up — the header edits arm64
  required, the Homebrew prerequisites, build/run/package steps, the
  self-locating .app, the sdl2-compat/SDL3 runtime gotcha, and the folder-access
  permission prompt. Notes where to source the original DK data.
- README.md: add a "macOS (Apple Silicon)" section (build, package, required
  original-DK files, drop-in layout, first-launch permission prompt) and list
  the native Linux/macOS arm64 builds under Features.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds KeeperFX on the GitHub-hosted Apple Silicon runner (macos-15):
installs the Homebrew deps, builds the source-only deps via
tools/build_macos_deps.sh (cached, keyed on that script's hash), runs
`make -f macos.mk`, asserts the output is a native arm64 Mach-O, then
packages the self-contained KeeperFX.app, checks no Homebrew path leaked
into it, and verifies the bundle signature. Uploads both the bare binary
and the .app.

Unlike the PR-only Test Prototypes workflow, this also runs on push to
master (path-filtered to macOS-relevant files) so the mainline macOS build
stays validated. Also available via workflow_dispatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewdeaves

Copy link
Copy Markdown
Author

Packaging hardened + download rebuilt and verified

I've tidied this branch and made the .app packaging robust for libraries that are loaded at runtime (dlopen / SDL_LoadObject), which dylibbundler can't discover because they don't appear in otool -L. The bundle commit now:

  • Adds an add_runtime_dylib() helper: for each such library it copies it into Contents/libs, rewrites its install id to @executable_path/../libs, and re-runs dylibbundler over it (with --overwrite-files, so it doesn't erase the libs directory) to pull in that library's own transitive deps. This covers the dlopen'd libSDL3 (Homebrew's sdl2 is sdl2-compat) and, defensively, SDL2_mixer's audio decoders.
  • Adds a verification pass that walks every Mach-O in the bundle (main binary + all bundled dylibs) and fails the build on any leftover /opt/homebrew or /usr/local reference.

Verified end-to-end on an M5 MacBook Air. I downloaded the published KeeperFX.app.zip (not a local build), dropped it next to a real Dungeon Keeper install, and launched it with Homebrew hidden from the dynamic loader:

env DYLD_FALLBACK_LIBRARY_PATH=/nonexistent \
    KeeperFX.app/Contents/MacOS/keeperfx

Result: 0 libraries loaded from /opt/homebrew, no loader errors, and every dependency — including the dlopen'd libSDL3 and all music decoders — resolved from inside the bundle. The linked download is fully self-contained (78 bundled dylibs, valid ad-hoc signature).

The one-click download reflects this exact packager:
https://github.com/matthewdeaves/keeperfx/releases/tag/macos-latest

On the "some sounds don't work" report

@EugenHotaj — while chasing this I found that on a Homebrew build, SDL2_mixer's decoders are actually link-time dependencies (they show in otool -L libSDL2_mixer), so dylibbundler was already bundling them transitively; the decoders were present in the .app all along. So missing audio is most likely not a bundling gap. The more probable causes:

  1. How it was obtained. A raw GitHub CI artifact zip is re-zipped by GitHub, which strips the ad-hoc code signature and the executable bit and can break launch/behaviour. Please use the release download above instead.
  2. Game data. "Imps stuck + some sounds missing" fits a DATA/SOUND mismatch more than the arm64 binary. My working data is from Dungeon Keeper Gold, which ships DATA/SOUND uncompressed (the earlier folder listing has exact file sizes to diff against).

If you still get no sound from the release build, running it once from Terminal and pasting the output would show whether any decoder fails to load:

KeeperFX.app/Contents/MacOS/keeperfx 2>&1 | tee /tmp/kfx.log

@EugenHotaj

Copy link
Copy Markdown

@matthewdeaves same issue with the latest download unfortunately. I'm trying it directly on my keeperfx folder which works with wine, so I think the folder structures / files should be good.

I have an M2 MacBook Air and also got the game from GoG (a few years ago).

One thing I can think of is that I'm using an old version of keeperfx (1.1) and your version seems to be 1.4, so there could be some conflict. I will try doing a "clean install" of a 1.4.x keeperfx and try again

@EugenHotaj

Copy link
Copy Markdown

Ok that was indeed the issue :) works beautifully

@hayesey

hayesey commented Jul 6, 2026

Copy link
Copy Markdown

I'm having the same issue with certain sounds not working in-game.

I have done a fresh/clean installation of KeeperFX 1.4 using the Windows download and Wine. Let this copy the DK files from my original GOG download of DK-Gold, verified this all works by running it with Wine, including all sounds.

Then downloaded your latest release (from 19 hours ago at the time of writing this) and placed the app file into the KeeperFX folder.

It runs fine but some sounds don't work such as the "shovel" noise when marking a square for digging or the "put down" noise when laying down some lair etc... Noises such as the imps actually digging the marked squares does work.

I ran from the terminal as you suggested but the resulting log file is empty. I also ran it from the terminal without piping to tee and still no output at all.

I am using a Macbook Air M1 from 2020 with Macos 26.5.1

@matthewdeaves

Copy link
Copy Markdown
Author

I'll have a think about what I could do to investigate from this end. maybe a bit of a report tool or something that outputs info about your DK1 install that you can review and post.... might help me pinpoint whats going on. Be a few days till I'm back in action though, thanks for trying it!

@matthewdeaves

Copy link
Copy Markdown
Author

macOS sound troubleshooting — for anyone hitting "some in-game sounds don't play" (@hayesey, @EugenHotaj), the latest build now bundles a small read-only sound diagnostic so we can pin down what's happening on your machine.

Grab the current download: https://github.com/matthewdeaves/keeperfx/releases/tag/macos-latest

With KeeperFX.app next to your game data, run this in Terminal (drag the app in to get its path, then add the rest):

/path/to/KeeperFX.app/Contents/MacOS/keeperfx-sndcheck

It checks your sound.dat against a known-good DK-Gold baseline, lists your OpenAL output device(s) (a wrong default output device is a common cause), decodes and buffers every sample and flags any failures, shows your keeperfx.cfg audio settings, plays a few key sounds, and saves keeperfx-soundcheck.txt next to the app.

It is read-only — it never changes your game — and the report contains no personal info (paths are shown as ~, no username). Please attach that keeperfx-soundcheck.txt (or paste the Terminal output) and note which of the played sounds you did or did not hear.

@moretti

moretti commented Jul 7, 2026

Copy link
Copy Markdown

@matthewdeaves not sure if it helps, but I also tried to port the game to apple silicon a few days ago and fixed some unaligned reads along the way. A segmentation fault occurred after loading a savegame (fixed in my branch). I kind of abandoned the project because after posting on Discord the team didn't like the usage of LLMs / quality of the code, but perhaps it might help you with this:
master...moretti:keeperfx:macos-port

@hayesey

hayesey commented Jul 7, 2026

Copy link
Copy Markdown

Here is my soundcheck file.

I actually hear all the sounds played when running this! But still not in the game itself. I also cloned your repo and built it from scratch myself but still have the same issue. You'll see that I am using a bluetooth headset for audio but I have also tried it using the macbook built in speakers.

keeperfx-soundcheck.txt

@matthewdeaves

Copy link
Copy Markdown
Author

@matthewdeaves not sure if it helps, but I also tried to port the game to apple silicon a few days ago and fixed some unaligned reads along the way. A segmentation fault occurred after loading a savegame (fixed in my branch). I kind of abandoned the project because after posting on Discord the team didn't like the usage of LLMs / quality of the code, but perhaps it might help you with this: master...moretti:keeperfx:macos-port

@moretti thats fair enough, it’s their repo/work and AI and its impact on the world/life is a hot topic. I just wanted to play dungeon keeper again on my mac. Think I’ll just close this PR and do what I can on my own fork.

@hayesey

hayesey commented Jul 7, 2026

Copy link
Copy Markdown

@matthewdeaves are you going to continue your fork? I see it has been deleted or made private. I would be keen to keep helping you test it and get it to a stage where hopefully it can be merged into the main repo, I've been wanting a native mac version for a long time!

@PieterVdc

Copy link
Copy Markdown
Member

this one is cleaner compared to the other one I'd say
though for a full release I'd prefer to first have the userdata cleanly seperated from other stuff,
and there's already some work in progress to move to cMake fully so that'll be a bit cleaner

but those are tough ones to get an l don't think an llm could do cleanly,

if you for the packing stuff if you could split that off and remove the comments as to why packing isn't needed there that would get merged

@EugenHotaj

Copy link
Copy Markdown

macOS sound troubleshooting — for anyone hitting "some in-game sounds don't play" (..., @EugenHotaj)

Fwiw everything works perfectly for me now that I updated to KeeperFX 1.4. I no longer have any sound issues and actually the narrator's voice on the world map is much better than with Wine (sounds a bit choppy and cuts out).

Re the AI stuff -- I get not wanting to land ai slop but would love to have a working native Mac version, even if it's just in "alpha" mode or some feature branch. At least until it can be landed properly.

@matthewdeaves matthewdeaves deleted the macos-arm64-build branch July 8, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants