This branch ports Cockatrice III (a BasiliskII derivative) from 32-bit-only builds to native 64-bit builds on macOS (Apple Silicon and Intel) and Windows (x64 and ARM64), while keeping the existing 32-bit Windows build working.
The historical release notes live in README / README.old. This file only covers what changed to make 64-bit builds possible.
BasiliskII's 68k CPU emulator (UAE) and memory subsystem were written with the
assumption that a host pointer fits in 32 bits. Several places in the codebase
either truncated a pointer into a 32-bit UAE register or typedef'd uintptr
to a fixed 32-bit integer. On a 64-bit host that truncation silently drops the
top 32 bits of a real pointer, which reliably crashes or corrupts memory as
soon as the emulator touches Mac RAM/ROM above the low 4GB of address space
(or, in practice, whenever the host allocator hands back a 64-bit address, which
it always does).
-
uintptris now a real pointer-sized integer. BasiliskII/mingw/sysdeps.h previously hardcodedtypedef unsigned int uintptr;(32-bit, always). It now usestypedef uintptr_t uintptr;from<stdint.h>. The new BasiliskII/OSX64/sysdeps.h does the same. This is whatuae_cpu/memory.cpprelies on forRAMBaseDiff/ROMBaseDiff/FrameBaseDiffto correctly round-trip a host pointer. -
Ethernet packet delivery no longer truncates a host pointer into a 32-bit UAE register. BasiliskII/SDL/sdl_pcap.cpp used to pass the host packet buffer address as
r.a[0] = (uint32)p + 14, which loses the high bits of the pointer on 64-bit hosts. It now stashes the pointer out-of-band viaEtherSetPacketData()and clearsr.a[0]; the newether_packet_dataside channel in BasiliskII/ether.cpp (declared in BasiliskII/include/ether.h) is whatEtherReadPacket()reads from instead of trusting the 32-bit register. BasiliskII/emul_op.cpp was updated to match the newEtherReadPacketcalling convention. -
Unaligned memory access helpers use compiler builtins instead of hand-rolled bit tricks.
do_get_mem_long/do_put_mem_long/etc. insysdeps.h(mingw and OSX64) now use__builtin_bswap32/__builtin_bswap16and are enabled for__x86_64__,__aarch64__,_M_X64, and_M_ARM64, in addition to the original__i386__/__powerpc__/__m68k__set. -
Fixed a real logic bug in 64-bit disk positioning. BasiliskII/disk.cpp combined the high and low 32 bits of a 64-bit disk offset with
||(logical OR) instead of|(bitwise OR), which is only correct by accident when the low bits happen to be non-zero. This is unrelated to the host architecture but was caught while auditing the 64-bit code paths. -
Removed other latent 32-bit assumptions:
- BasiliskII/SDL/video_sdl.cpp:
SDLscreen->pixelsis avoid*; pointer arithmetic on it now casts touint8*first (this silently "worked" via GCC's non-standardvoid*arithmetic extension, but fails to compile with a strict/newer Clang toolchain). - BasiliskII/dummy/audio_dummy.cpp:
audio_sample_rateschanged fromuint32toint32to match the signature expected elsewhere. - BasiliskII/dummy/user_strings_dummy.cpp
and BasiliskII/dummy/cdrom_dummy.cpp:
const-correctness andbool/FALSEcleanups needed by modern C++ compilers (Clang/GCC 8+) that reject the old code as ill-formed or warn it into a build failure under-Werror-adjacent settings. - BasiliskII/SDL/main_sdl.cpp: Windows header
includes changed from backslash (
SDL\SDL.h) to forward-slash (SDL/SDL.h) paths, since MSYS2/MinGW toolchains don't treat\as a path separator in#include. DrMinGW crash-handler integration (exchndl.h,ExcHndlInit()) is now gated to 32-bit x86 (or an explicitUSE_EXCHNDLdefine) since DrMinGW doesn't support x64/ARM64. - BasiliskII/mingw/config.h:
SIZEOF_VOID_P/SIZEOF_CHAR_Pare now computed correctly (8 on_WIN64/__x86_64__/__aarch64__, 4 otherwise) instead of being hardcoded for 32-bit.
- BasiliskII/SDL/video_sdl.cpp:
BasiliskII/OSXarm(the initial Apple Silicon-only port) was renamed toBasiliskII/OSX64and its Makefile now builds either Apple Silicon or Intel Macs from one tree via anARCHvariable (arm64by default, orx86_64/amd64), selecting the right SDL prefix and-archflag automatically. ARM uses native Homebrew (/opt/homebrew). An Intel slice on Apple Silicon isclang -arch x86_64plus the committed prefix indist/dependencies/osx/intel(rebuild withdist/dependencies/osx/rebuild-intel-sdl.shwhen bumping SDL). The bundledm68k-rsstaticlib is built withcargo --targetfor the same slice (aarch64-apple-darwinorx86_64-apple-darwin; the Makefile runsrustup target add).make universalin that directory builds both slices andlipos them into a fatCockatriceIII(ARM Homebrew SDL plus the committed Intel prefix).make app(ormake app-universal) wraps that binary into aCockatriceIII.appbundle with a generatedInfo.plist(APP_VERSION=x.y.zsetsCFBundleVersion/CFBundleShortVersionString), copiesdist/Quadra800.romand theCockatriceIII.icnsapp icon intoContents/Resources, and, ifentitlements.plistis present, codesigns the whole bundle the same way the plain binary is signed.main_sdl.cpp's ROM lookup falls back toContents/Resources(via_NSGetExecutablePath) when the configured ROM path isn't found relative to the working directory, so the bundle's ROM is found regardless of how it was launched.BasiliskII/mingw/Makefilewas rewritten to build Windows x86, x64, or ARM64 from the same tree using whatever MSYS2 toolchain/MSYSTEM (MINGW32/MINGW64/CLANGARM64) invokes it, usingsdl-configwhen available instead of hardcoded library paths.
.github/workflows/build-and-release.yml
builds all six targets on every push, on pull requests into main, and on
v* tags (which also cuts a GitHub Release):
| Target | Runner | Build dir |
|---|---|---|
| osx-arm | macos-latest (Apple Silicon, native arm64) | BasiliskII/OSX64 |
| osx-amd64 | macos-latest (Apple Silicon, cross x86_64) | BasiliskII/OSX64 |
| osx-universal | macos-latest (Apple Silicon, lipo fat) | BasiliskII/OSX64 |
| win32-x64 | windows-latest (MINGW64) | BasiliskII/mingw |
| win32-x86 | windows-latest (MINGW32) | BasiliskII/mingw |
| win32-arm | windows-latest (CLANGARM64) | BasiliskII/mingw |
Reproduce a macOS CI job on an Apple Silicon machine with
scripts/ci-osx-build.sh (arm64, amd64, or
universal). That script is what the workflow runs.
Each job packages its build (.dmg on macOS, .zip on Windows) alongside the
files in dist/ and uploads it as a build artifact; on a version tag
the artifacts from all targets are attached to a single GitHub Release. Each
macOS .dmg carries both the flat CockatriceIII binary and a double-
clickable CockatriceIII.app bundle (built via make app /
make app-universal in BasiliskII/OSX64, ROM baked into
Contents/Resources at build time), each with its own copy of
prefs/xpram/archive from dist/ since the app resolves those relative to
its own working directory.
The workflow triggers on push to any branch (not just main), so pushing
a feature/port branch like osx-arm runs the full build matrix without
needing to merge first.
Cockatrice III features a modular CPU engine abstraction layer (CPUEngine), allowing seamless switching between different 680x0 execution engines. musashi is always built in; uae (Amiberry) and m68k_rs are optional per-port (a build opts out with ENABLE_AMIBERRY_CPU=0 / ENABLE_M68K_RS_CPU=0, e.g. the Windows/MinGW port ships Musashi only). Requesting uae or m68k_rs on a build that doesn't have it is a hard failure at startup rather than a silent fallback to Musashi; any other unrecognized value falls back to Musashi with a warning.
The earlier
syn68kandemu68backends (and the classic Mac-only build path) were retired;m68k_rsis their replacement as the third engine.
graph TD
A[Prefs / Configuration: cpu_emulator] --> B[CPUEngine Dispatcher: cpu_engine.cpp]
B -->|cpu_emulator musashi| C[Musashi 680x0 C interpreter]
B -->|cpu_emulator uae| E[Amiberry 680x0 interpreter + ARM64/x86-64 JIT]
B -->|cpu_emulator m68k_rs| F[m68k-rs Rust interpreter / batch executor]
C --> G[Mac OS Memory Banking: RAMBaseHost / ROMBaseHost]
E --> G
F --> G
G --> J[EmulOp & Toolbox Trap Hooks: 0x71xx / 0xAxxx]
J --> K[Peripheral Subsystems: SCSI / Video / Audio / Ethernet]
musashi(Default): Portable, cycle-accurate C interpreter (Musashi 4.5+), hardcoded to 68040 in this port. No translator — thejit/jitfpuprefs are ignored on this engine.uae: Amiberry 680x0 core — cycle-accurate interpreter, or withjit trueits ARM64/x86-64 compemu JIT — with SoftFloat 68881/68882/68040 FPU. Follows Apple's W^X rule on Apple Silicon (oneMAP_JITregion toggled withpthread_jit_write_protect_np).m68k_rs: m68k-rs, a Rust 680x0 core vendored as a static library (BasiliskII/vendor/m68k-rs, glued in viaBasiliskII/m68k_rs/m68k_rs_glue.cpp). Runs as a cycle-accurate interpreter by default;jit trueswitches it to a decoded-op batch executor with an optional direct-RAM "fastmem" window (m68k_rs_fastmem). Requires Rust 1.93+ to build (cargo, see docs/cpu-engine-m68k-rs.md); passes the same 68k opcode-battery test suite as Musashi (118/122 checks — four Musashi BCD/CHK2/CMP2 fixtures are intentionally skipped due to modeling differences).
Set the active CPU core in your .basilisk_ii_prefs / BasiliskII_prefs configuration file:
cpu_emulator uae # Options: musashi (default), uae, m68k_rs
jit true # Enable JIT/batch execution (uae, m68k_rs only; ignored by musashi)
jitfpu true # Also JIT-compile FPU instructions (requires jit true)
jitcachesize 8192 # JIT/translation cache size in KB (default: 2048 KB)
m68k_rs_fastmem off # m68k_rs only: off (default) | ram | multi | legacy direct-RAM window
Basilisk II boots a real Mac ROM image rather than emulating one, so hardware probes that would
hang or crash against nothing are patched out and a handful of routines are redirected to host
code (EmulOp(), 0x71xx). That happens in two passes:
- ROM patches (
PatchROM()→patch_rom_32()/patch_rom_classic()in BasiliskII/rom_patches.cpp) mutate the loaded ROM image once, beforeStart680x0()runs any 68k code. - Resource patches (
CheckLoad()in BasiliskII/rsrc_patches.cpp) run every time the System loads a ROM/System resource, via a stub spliced into the ROM's ownjCheckLoadhook ($07F0) — this is how patches reach System-file code that doesn't ship in the ROM at all (Time Manager, ADB, SCSI, Sound, LocalTalk).
This branch reworked both passes for safety and traceability, largely by cross-referencing Apple's own SuperMario ROM source tree to find out what each patched routine actually does. See docs/rom-patches-vs-supermario.md for the full per-patch mapping and docs/basilisk-ii-boot-and-patch.md for the end-to-end boot call graph.
- Every ROM patch attempt is recorded and printed (
[ROM-PATCH] name @ offsetorMISSED), and every resource patch likewise ([RSRC-PATCH] ...); both logs are now flushed immediately after each line instead of sitting in a block-bufferedstdoutthat could be lost if the process died before exit. - The handful of ROM patches that target a bare fixed offset (
0x1142,0x1b8f4,0x9bc4,0xa296,0xb2c6a,0xb2d2e,0x5b78, …) now verify the instruction bytes they expect to overwrite first, and fail with a namedVERIFY FAILEDinstead of scribbling over unrelated code when a ROM's layout doesn't match. - Trap-table lookups (
find_rom_trap()) return 0 for both "trap not implemented" and "trap not found" — every required call site now goes throughrequire_rom_trap(), which treats a miss as a hard failure instead of writing a patch over the ROM header. - Two fixed-offset resource patches with no signature to search for (
'sift'/'thng' -16563, the Sound Manager audio-component patches, and'ltlk' 0) used to write unconditionally; a truncated resource meant a heap overwrite past the end of its handle rather than a missed patch. They now check the resource size first and log a miss instead. - Two real out-of-bounds scans were found and fixed under AddressSanitizer:
find_rsrc_data()underflowed its unsigned bound when a resource was shorter than the signature being searched for, andpatch_idle_time()could scan before the start of its buffer.
Where SuperMario shows a documented, ROM-version-independent way to install something, Cockatrice now uses it instead of guessing a byte offset — but only where the timing allows it:
InstallRuntimeTraps()installsMicroseconds,PowerOff, andADBOpat runtime via_SetOSTrapAddressfrom a small stub block allocated in the System heap — the sameleaResident/_SetTrapAddressidiom Apple's own Time Manager patch uses — rather than overwriting the ROM's copy of each trap. This only works for traps confirmed, by instrumenting a real boot, to not be called beforeInstallDrivers()runs (the trap dispatcher has to exist first).BlockMove,InsTime,SCSIDispatch, andCheckLoadare called earlier and must remain ROM patches.InstallVBLHandler()takes over the 60 Hz VBL interrupt through the ROM's ownjVBLIntvector (Lvl1DT+4, low-memory$196) instead of overwriting a hardcoded ROM offset. The ROM continuation address is read out of the vector the ROM itself installed, rather than assumed, and the site is byte-verified before anything is written. The VIA1 level-1 dispatcher at0x9bc4is still a forced byte patch — Cockatrice has no emulated VIA1 to compute a real IFR/IER pending-interrupt mask from, so the value has to be hardcoded regardless of mechanism.
basilisk_patches_test(BasiliskII/tests) replays the ROM patch pass and checks it against a fixture manifest (tests/basilisk/fixtures/quadra800_patches.txt); that manifest has itself been cross-checked against a real Musashi boot toHasMacStarted(57 of 59 records identical, two expected differences documented in docs/rom-patches-vs-supermario.md).basilisk_rsrcpatch_testlinks the realCheckLoad()into the test suite for the first time (it was previously stubbed out entirely) and drives it with synthetic resources built from the actual Apple ROM source sequences, run under AddressSanitizer.
New preference keys added on this branch, on top of the existing BasiliskII set (ramsize,
screen, rom, ether, bootdrive, …):
| Key | Type | Default | Effect |
|---|---|---|---|
cpu_emulator |
string | musashi |
Active 680x0 engine: musashi, uae, or m68k_rs. See Multi-Engine 680x0 CPU Architecture. |
jit |
bool | false |
Enable JIT / batch execution. No effect on musashi. |
jitfpu |
bool | false |
Also JIT-compile FPU instructions. Requires jit true; forced off otherwise. |
jitcachesize |
int32 | 2048 |
JIT/translation cache size, in KB. |
m68k_rs_fastmem |
string | off |
m68k_rs only: off, ram, multi, or legacy direct-RAM window. |
dump_memory |
bool | false |
On an unhandled guest System Error, write a binary RAM snapshot to dump_file before halting. Engine-independent. |
dump_file |
string | /tmp/memory.bin |
Output path for the dump_memory crash snapshot. |
scsi_debug |
bool | true |
Verbose SCSI and CD-ROM logging. |
toolbox_hooks |
bool | false |
Master switch for the Toolbox trap-hook registry (toolbox_traps.cpp). Required by the menu bar bridge and window mirroring below; off by default because it patches guest trap vectors at boot. |
mdi_windows |
bool | false |
Experimental. Mirror each guest Mac OS window into its own host window instead of one flat framebuffer. Requires toolbox_hooks true. |
window_redirect |
bool | false |
Experimental. Give each mirrored window its own offscreen pixel buffer, so covered windows don't show whatever is on top of them. Off is the more compatible mode. |
native_alerts |
bool | false |
Experimental. Rebuild guest alert/dialog boxes as native host controls instead of mirroring their pixels. |
cpu (the old numeric CPU-type preference) is still accepted for prefs-file compatibility but is
unused: the CPU model is hardcoded to 68040.
Behind toolbox_hooks (off by default) is a registry that installs Toolbox trap trampolines at
boot (BasiliskII/toolbox_traps.cpp, BasiliskII/include/toolbox_traps.h) so portable code can
hook specific _A-line traps without patching the ROM. Two subsystems build on it today:
- Guest menu bar bridge (
BasiliskII/toolbox_menu.cpp) snapshots the guestMenuListand rebuilds the native macOS menu bar (NSApp.mainMenu) from it viaBasiliskII/bridge/darwin/macos_menu_bridge.mm, so the emulated Mac's own menus appear as a real host menu bar. A from-scratch Classic-style (Chicago/Platinum, host-composited) menu bar is designed but not yet implemented — see docs/classic-menu-bar-compositor.md. - Guest window mirroring (
BasiliskII/toolbox_window.cpp,BasiliskII/bridge/darwin/macos_window_bridge.mm) turns each Mac OS window into a real hostNSWindowwith native chrome, so a Classic application can look native while its content stays authentically Classic. Controlled bymdi_windows,window_redirect, andnative_alertsabove. Status: experiment, Phase 1 working, with known limitations around overlapping/redirected windows, desk accessories, and window-kind detection — see docs/guest-window-mirroring.md for the full design writeup, debugging aids (COCKATRICE_WINDOW_DUMP,COCKATRICE_WINDOW_SELFTEST), and known limitations.