Building the project for Amiga systems

Guide how to build, deploy and run Jazz² Resurrection on AmigaOS 3.x, AmigaOS 4.1 and MorphOS.

Three Amiga systems are supported, and one thing they share decides the shape of all of them: none of them has graphics hardware the game's shader pipeline can use. The classic machines have no texturing or blending at all (RTG cards accelerate blits and fills, nothing that helps here), so there the CPU software rasterizer (Software RHI, which the desktop and Vita builds already ship) is not a fallback but the renderer.

The two PowerPC systems do have 3D hardware, and what each of them offers the game is a fixed-function OpenGL 1.x - TinyGL on MorphOS, MiniGL over Warp3D on AmigaOS 4. That is exactly the shape the game's console backends target, so both are driven by the LegacyGL rendering backend (see Hardware rendering through TinyGL), which is the default on both – Software stays selectable for a machine whose card has no 3D driver. Everything below that differs:

SystemToolchain (CMake toolchain file)RendererWindow/inputAudioDetails
AmigaOS 3.x (m68k)AmigaPorts' m68k-amigaos-gcc (amiga.cmake)Softwareown backend (Intuition + RTG)AHIAmigaOS 3.x (m68k)
AmigaOS 4.1 (PowerPC)adtools (os4.cmake)LegacyGL (MiniGL)SDL2SDLAmigaOS 4.1 (PowerPC)
MorphOS (PowerPC)MorphOS SDK (morphos.cmake)LegacyGL (TinyGL)SDL2SDLMorphOS (PowerPC)

The design document behind all three, including the hardware research the classic port is based on, is Docs/AmigaPortDesign.md in the repository.

AmigaOS 3.x (m68k)

The classic Amiga build is a native AmigaOS 3.x/m68k port that covers the whole fast-Amiga spectrum from a single executable: PiStorm/Emu68 accelerators, Apollo Vampire boards and accelerated 68060 machines with an RTG graphics card. The rasterizer presents into a Picasso96/CyberGraphX chunky framebuffer, and what differs between machines is not the code path but the size of the work, which is chosen at startup by measuring the machine — see Performance presets and autodetection.

PropertyValue
ToolchainAmigaPorts' m68k-amigaos-gcc (GCC 16.2, prebuilt release; cmake/toolchains/amiga.cmake)
C runtimelibnix (-mcrt=nix20)
Rendering backendSoftware (pinned – any other value is a configure error), forced to 16-bit surfaces
Window/input backendAmiga (Sources/nCine/Backends/Amiga/) — Intuition screen, IDCMP input, lowlevel.library gamepads
Audio backendAHI (Sources/nCine/Audio/Backends/Amiga/) — software mixer into ahi.device
Musiclibxmp (NCINE_WITH_XMP, downloaded and built by the CMake configure), reads the original .j2b modules directly
Build artifactbuild/amiga/dist/ — the Jazz2 executable next to its Content/ tree

What it runs on

The port targets machines that are, in CPU terms, at or above the consoles the game already runs on. The table below is the honest picture, not a compatibility list.

MachineVerdict
PiStorm / Emu68 (Pi3A+, CM4, Pi4)The primary target. Dreamcast-class or better, RTG framebuffer only — exactly what the software renderer wants.
Apollo Vampire V4 / V1200 (68080 + SAGA)Supported, with the AMMX scanline kernels enabled automatically. Expect reductions rather than a full-resolution game.
68060 + RTG card (Picasso IV, ZZ9000, …)The floor. This is the Low preset — heavily reduced and experimental.
68030/68040, or any machine without an FPUNo. The build refuses to start without an FPU, and the update loop is floating point throughout.
Stock AGA machines (no RTG)Not supported. A chunky-to-planar demake tier is discussed in the design document as a stretch goal, and is not part of this build.

An RTG screen mode is required: the port opens a CyberGraphX/Picasso96 screen and presents into it. At least 8 MB of fast RAM is realistic – the game itself asks for far more than a stock machine has.

Toolchain

The cross-compiler is AmigaPorts/m68k-amigaos-gcc, which is published as a prebuilt release for Linux x86_64 and macOS arm64 – so no cross-compiler has to be built. The releases are GCC 16.2; GCC 13 is the floor, this codebase being C++17 throughout.

# The archive's own directory IS the prefix - the toolchain is relocatable, so unpack it anywhere
curl -fsSLO https://github.com/AmigaPorts/m68k-amigaos-gcc/releases/download/v16.2-rc6/m68k-amigaos-gcc-16.2-rc6-Linux-x86_64.tar.xz
tar xJf m68k-amigaos-gcc-16.2-rc6-Linux-x86_64.tar.xz -C /opt
export AMIGA_INST=/opt/m68k-amigaos-gcc-16.2

The release already carries everything this port compiles against except one library. NDK 3.2, the CyberGraphX and AHI headers** (which the NDK itself does not include) and vasm (vasmm68k_mot, needed only for NCINE_WITH_AMMX – the AMMX kernels are hand-written assembly that no GCC can emit) are all in the archive. What is left is zlib, the one third-party library the game cannot run without:

export PATH="$AMIGA_INST/bin:$PATH"
curl -LO https://zlib.net/zlib-1.3.1.tar.gz && tar xf zlib-1.3.1.tar.gz && cd zlib-1.3.1
CC=m68k-amigaos-gcc AR=m68k-amigaos-ar RANLIB=m68k-amigaos-ranlib \
    CFLAGS='-mcpu=68060 -O2 -mcrt=nix20' ./configure --prefix=$AMIGA_INST/m68k-amigaos --static
make -j$(nproc) libz.a && make install

What the toolchain file sets, and why

cmake/toolchains/amiga.cmake is small, but three of its flags are load-bearing and must not be "cleaned up":

  • -mcpu=68060 with an explicit -m68881. The CPU flag alone does not imply the FPU — m68k-amigaos-gcc -mcpu=68060 -Q --help=target reports -msoft-float [enabled] — so without -m68881 every floating-point operation in the update loop compiles into a libgcc call. Measured under an emulated 68040: 1550 ns per multiply-add soft, 300 ns with the FPU, the integer side of the same benchmark unchanged. It is also an ABI trap, because the libnix libm the link resolves to (libm020/libm881/libm.a) is the hard-float one either way: it returns floating point in fp0, and soft-float code reads the return value from d0. That mismatch is what made fminf(), fmaxf(), rint() and nearbyint() return garbage (round() and trunc() appeared to work only because their result bits are left in d0 as well). -print-multi-directory reports . for this flag combination, which looks alarming, but the link resolves the libm020/libm881 variant of every library — which is what actually matters.
  • -fno-optimize-sibling-calls — this is correctness, not tuning. The m68k-amigaos binutils resolves a sibling call (bra.l, and ld's own 60FF/61FF4EB9 conversion) to a weak symbol against the wrong address, so any tail call into an inline or template function jumps into garbage. Ordinary jsr calls to the same symbols are fine, which is why the mitigation is this narrow. -fno-weak is not an alternative: it also removes weak data*, and template statics such as Vector2f::Zero then fail to link.
  • -std=gnu++17, pinned rather than left to CMake. target_compile_features(cxx_std_17) is a minimum*, so on a compiler that already defaults higher – GCC 16 defaults to gnu++20 – CMake adds no -std flag at all and this target alone would build as C++20. Beyond the inconsistency, the toolchain's <math.h> pulls using std::lerp; into the global namespace under C++20, which is ambiguous against the engine's own nCine::lerp at every call site.

The toolchain also force-includes cmake/toolchains/amiga-libstdc++-c99.h into every C++ translation unit, the same arrangement the PlayStation 3 build uses: the m68k-amigaos libstdc++ is configured without _GLIBCXX_USE_C99_MATH_TR1, so <cmath> declares none of the C99 maths set inside std:: even though the library provides it globally.

Preparing the content tree

Unlike the consoles that play from read-only media, an Amiga has somewhere to write: Source and Cache are resolved next to the executable, so an original Jazz Jackrabbit 2 installation copied into Source is converted on the machine on the first start, exactly as on the desktop. It is only slow* — the conversion decodes every animation, tileset and level and writes a compressed package out of them, which is minutes of work on a desktop CPU and a great deal more on a 68060 — so the build is normally pointed at a prebaked tree produced once on the host with AssetPacker instead:

cmake -B ./build/amiga/ -D CMAKE_TOOLCHAIN_FILE=./cmake/toolchains/amiga.cmake \
    -D CMAKE_BUILD_TYPE=Release -D NCINE_CONTENT_DIR=./build/ConsoleContent

Point NCINE_CONTENT_DIR at the repository's Content directory instead and the build carries only the base assets — the game then starts and shows the "content missing" message until an original installation is placed in Source next to it. Neither route has been timed on real hardware yet.

Building

export AMIGA_INST=/opt/amiga
export PATH="$AMIGA_INST/bin:$PATH"

cmake -B ./build/amiga/ -D CMAKE_TOOLCHAIN_FILE=./cmake/toolchains/amiga.cmake \
    -D CMAKE_BUILD_TYPE=Release -D NCINE_CONTENT_DIR=./build/ConsoleContent
cmake --build ./build/amiga/ --parallel $(nproc)

AMIGA_INST is the toolchain prefix, and it is checked before anything is configured: the toolchain file wants $AMIGA_INST/bin/m68k-amigaos-gcc and the NDK headers to be there, with zlib installed beside them (see Toolchain). The AmigaOS 3.x job in .github/workflows/amiga.yml is the same recipe end to end – download the release, cross-compile zlib, configure, build – if an unattended one is what is wanted.

The build stages build/amiga/dist/, which is the directory to copy to the Amiga: the Jazz2 executable with its Content/ tree next to it. Jazz2.config is written into that same directory, which is the conventional home of a program's settings on AmigaOS.

Two notes specific to this target. Link-time optimization is off (NCINE_LINKTIME_OPTIMIZATION) and Release compiles at -O2 rather than -O3, for the same reason as the consoles: this is the least-tested compiler in the project and code size matters. And threads are off — there is no std::thread on m68k-amigaos — so everything, including audio decoding, runs on the main thread.

Deploying and running

On real hardware, copy dist/ anywhere on a hard drive and run the executable from a Shell or its icon. It opens its own screen and closes it on exit. The stack is set from the executable's own __stack cookie (1 MB), so no Stack command is needed, but a Stack 1048576 before it does no harm if you start it from a script.

For development the port is exercised under FS-UAE with AROS 68k, whose open-source Kickstart replacement ships CyberGraphX and AHI:

# jazz2.fs-uae - AROS boots to Workbench in about ten seconds like this
amiga_model = A4000
cpu = 68040
jit_compiler = 0
kickstart_file = /path/to/aros/boot/amiga/aros-rom.bin
hard_drive_0 = /path/to/aros/dh0
hard_drive_1 = /path/to/jazz2/build/amiga/dist

The game writes its log to standard output unbuffered, so redirecting it to a file on the emulated disk gives a complete trace even after a crash:

Jazz2:Jazz2 >Jazz2:jazz2-log.txt

A build configured with -DDEATH_DEBUG=ON also accepts /level <episode>/<level>, which boots straight into a level and is by far the fastest way to exercise the renderer:

Jazz2:Jazz2 /level prince/01_castle1 >Jazz2:jazz2-log.txt

Performance presets and autodetection

There is no configuration to get right before the game starts. AmigaPlatform::RunPerformanceProbe() times a 16-bit blend loop and an FPU dependency chain at startup and picks one of four presets, which then drive the screen mode and the audio mixing rate:

PresetPixel budget (screen mode is chosen to fit)Mixing rate
Ultra640×51244100 Hz
High640×36044100 Hz
Medium384×30022050 Hz
Low320×22022050 Hz

The screen mode is not requested by number: the display database is searched for the RTG mode whose logical* resolution (the 720×405-clamped aspect fit the game renders at) is largest within the preset's pixel budget. That is why a 640×400 mode is chosen over a 640×480 one where both exist — it yields more logical pixels for less work.

Two environment variables override what the machine reports, which is what you want when bringing the port up on a new accelerator:

VariableEffect
NCINE_PRESETlow, medium, high or ultra — forces the preset instead of measuring it.
NCINE_NO_AMMXSet to anything to keep the scalar scanline kernels on an Apollo 68080. The AMMX kernels are written to be bit-identical to the scalar ones (SwBackendHarness verifies that on hardware), so this is a diagnostic, not a workaround anyone should need.

Limits and known issues

  • An FPU is required, and on a 68040/68060 so is its support library. The binary is compiled for hardware floating point (-mcpu=68060 -m68881) and the game's update loop is floating point throughout, so it exits with a message rather than trapping on a machine without an FPU at all. Neither the 68040 nor the 68060 implements the whole 68881 set in silicon, though — FMOVECR and the extended-precision forms GCC emits are left to Motorola's FPSP — so Libs:68040.library (resp. 68060.library) has to be installed for SetPatch to load. Every accelerator ships it and Workbench 3.5/3.9 include it, but a bare Workbench 3.1 does not, and the symptom there is an immediate Recoverable Alert 0006 8881 rather than anything the game gets to report. The Apollo 68080 and Emu68 implement the instructions natively and need nothing.
  • RTG only. There is no chunky-to-planar path, so an AGA-only machine cannot run this build.
  • No threads. WITH_THREADS is off, so audio streaming decodes synchronously on the main thread and the multiplayer client/server are not built.
  • Music is libxmp, not libopenmpt. libopenmpt's mixer is written for machines two orders of magnitude faster than a 68060. libxmp reads the original .j2b Galaxy modules directly, so the soundtrack needs no conversion - what it cannot read is MO3, so the four .mo3 tracks are silent here (the PSP shares both facts, see Limits and known issues). The library is downloaded and built by the CMake configure (cmake/Findlibxmp.cmake) rather than vendored.
  • ahi.device implementations disagree about queueing. The AHI autodocs describe a queue in which a request carrying ahir_Link starts the moment the one before it finishes – AROS 68k's device accepts such a request and then never plays or replies to it, and it also stops servicing a queue that has run dry — which one long level-loading frame is enough to cause. The backend therefore measures linking at startup with two throwaway requests of its own and restarts a queue that has not retired anything for a second. Both mechanisms are inert on a device that behaves as documented – the log line Audio device initialized: … states which mode is in use.
  • libnix does not ship every C99 function. exp2, exp2f, log2f, strnlen and wcstoul are missing entirely and are defined in Sources/nCine/Backends/Amiga/AmigaLibcCompat.c, along with the POSIX entry points AmigaOS has no equivalent for (flock, fchmod, getpwuid, realpath, gethostname) and __xpg_strerror_r, which libstdc++ references by name. What is not in that file any more is a replacement for round, trunc, fmin, fmax, rint or nearbyint: those returned garbage under the old soft-float flags and are correct as libnix ships them once the build is hard-float (see What the toolchain file sets, and why).
  • Emulator timings are worth exactly one thing: A/B comparisons. FS-UAE interpreting a 68040 runs gameplay at well under one frame per second, and its absolute numbers say nothing about a PiStorm — but they are stable and they scale with the code, which is enough to compare two builds of the same thing. The startup probe is the instrument: it prints … ns/composited pixel, … ns/FPU multiply-add into the log on every boot, and the soft-float regression above was found by reading the second of those two numbers. Note that the integer figure it reports under emulation (around 330 ns/pixel here) happens to land in the same band as a real 68060/50, while the emulated FPU does not — so the two halves of that line are not comparable with each other. Anything that is not a comparison between two builds belongs on hardware.

AmigaOS 4.1 (PowerPC)

AmigaOS 4 needs no backend of its own: it has a maintained SDL2, so the window and input side is the SDL2 backend the desktop builds use, and audio is the SDL backend (Sources/nCine/Audio/Backends/SDL/) - the same software mixer as the classic Amiga's, handing its blocks to SDL_QueueAudio() instead of to ahi.device, because the AmigaOS 4.1 SDK has no OpenAL.

Hardware rendering through MiniGL

The renderer is the LegacyGL backend on MiniGL, the SDK's fixed-function OpenGL over Warp3D (libGL.a plus minigl.library – SDL2's own SDL_opengl.h includes its headers on this platform). It is the same backend MorphOS uses for TinyGL, so Hardware rendering through TinyGL describes what it does – two things differ here:

  • MiniGL has the texture combiners it needs - it advertises GL_EXT_texture_env_combine, whose enumerants are the ARB ones under _EXT names (GL_COMBINE_EXT is GL_COMBINE is 0x8570) - but it has no framebuffer objects at all, not even entry points to call. So the render-target path is not probed here the way it is on TinyGL: the copy-back form is compiled in and the framebuffer-object one is compiled out (RHI_LEGACYGL_HAS_FBO in LegacyGlApi.h).
  • Mirrored repeat is not offered, so that wrap mode degrades to a plain repeat rather than being sent to a driver that would answer GL_INVALID_ENUM and leave the sampler as it was.

The other road to this hardware is Warp3D Nova's ogles2.library, which the engine's ES2 profile (NCINE_RHI_GL_PROFILE=ES2 - the one the PS Vita's vitaGL build uses) would match - that would be the shader pipeline rather than this fixed-function one. What is missing is not engine support but the SDK: it carries no GLES2 headers or link library for it. Software remains selectable (-D NCINE_PREFERRED_RHI=Software) for a machine with no 3D driver installed – the CPU on these machines is PC-class, so it is a real option rather than a compromise.

Toolchain and building

The cross-compiler is adtools (ppc-amigaos-gcc 11.5) together with the AmigaOS 4.1 SDK. Both come in one maintained container image, so nothing has to be installed - run the build inside it with the repository mounted at its own path:

docker run --rm -v "$PWD:$PWD" -w "$PWD" docker.io/walkero/amigagccondocker:os4-gcc11 bash -lc '
    cmake -B ./build/os4/ -D CMAKE_TOOLCHAIN_FILE=./cmake/toolchains/os4.cmake \
        -D CMAKE_BUILD_TYPE=Release -D NCINE_CONTENT_DIR=./build/ConsoleContent
    cmake --build ./build/os4/ --parallel $(nproc)'

(Podman takes the same arguments – add --security-opt label=disable where SELinux is enforcing.) The toolchain file expects the adtools prefix at the container's /opt/ppc-amigaos – set OS4_INST to build against a host installation instead. Either way the result is staged into build/os4/dist/ - the Jazz2 executable next to its Content/ tree, copied to the machine as it is. The executable is stripped on the way: this toolchain leaves DWARF in even in a Release build, which is two thirds of its size.

The same two commands run in CI, in the container job of .github/workflows/amiga.yml.

What differs from a desktop build

  • No online multiplayer. The bsdsocket stack here is IPv4-only - there is no <netinet/in6.h> and no struct in6_addr in the SDK - while the bundled ENet is built around IPv6 addresses with IPv4-mapped ones inside them. The WebSocket transport is out for a smaller reason: IXWebSocket includes <poll.h>, and the SDK has none (the OS offers select() and WaitSelect()). Local splitscreen is unaffected.
  • Threads are on, unlike the classic Amiga and MorphOS, but the engine's synchronisation layer calls the pthread API directly and this toolchain's -athread=pthread flavour cannot be used - its gthr-amigaos-pthread.o is not shipped. The build therefore uses -athread=native for the compiler's gthreads and links the SDK's own libpthread. Thread affinity, thread names, barriers and pthread_kill() are all missing from that library, so those paths are compiled out.
  • libatomic is linked explicitly: this is a 32-bit PowerPC, so a 64-bit atomic load is a library call rather than an instruction.
  • -Uamiga is part of the machine flags. The compiler predefines both AMIGA and a lowercase amiga, the way ancient compilers predefined unix, and the lowercase one breaks any C++ with an identifier of that name - libopenmpt has common_encoding::amiga.

MorphOS (PowerPC)

MorphOS is built the same way as AmigaOS 4 - SDL2 for window and input, the SDL audio backend, and the same LegacyGL renderer on the system's own fixed-function OpenGL (Hardware rendering through TinyGL) - with a different toolchain and a different set of system quirks. What TinyGL does not have is shaders, which is why that backend is a fixed-function one rather than the engine's OpenGL backend.

Toolchain and building

The MorphOS SDK (ppc-morphos-gcc 11.3, at /gg) comes as a container image as well. One thing has to be built before the game itself: the SDK ships no zlib, and the game cannot run without it (PakFile).

docker run --rm -v "$PWD:$PWD" -w "$PWD" -e MORPHOS_DEPS=/tmp/morphos-deps \
    docker.io/walkero/amigagccondocker:mos-gcc-amd64 bash -lc '
    curl -fsSL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz | tar xz -C /tmp
    cd /tmp/zlib-1.3.1
    CC=ppc-morphos-gcc CFLAGS="-O2 -noixemul" ./configure --prefix=$MORPHOS_DEPS --static
    make -j $(nproc) libz.a && make install

    cd '"$PWD"'
    cmake -B ./build/morphos/ -D CMAKE_TOOLCHAIN_FILE=./cmake/toolchains/morphos.cmake \
        -D CMAKE_BUILD_TYPE=Release -D NCINE_CONTENT_DIR=./build/ConsoleContent
    cmake --build ./build/morphos/ --parallel $(nproc)'

MORPHOS_DEPS is where the toolchain file looks for cross-compiled dependencies – MORPHOS_SDK points at an SDK somewhere other than /gg. The result is staged into build/morphos/dist/, and .github/workflows/amiga.yml runs these same steps in CI.

Running it, and what has to be installed first

MorphOS ships no SDL2 - the SDK's libSDL2.a is a stub that opens sdl2.library at load time, and that library is a separate download ("SDL 2.32.11 Libraries" on MorphOS Storage, which also carries sdl2_image/mixer/ttf). Without it in LIBS: the game does not start at all: the loader puts up a requester before main() runs. Nothing else has to be installed - the port deliberately does not depend on a TCP/IP stack (see the note about bsdsocket.library below).

The port can be tested without MorphOS hardware, because QEMU emulates the Pegasos II that MorphOS runs on, and its built-in open firmware boots the MorphOS ISO directly:

# boot.img is extracted from the top level of the MorphOS ISO
qemu-system-ppc -machine pegasos2 -m 1024 -rtc base=localtime     -device ati-vga,romfile="" -cdrom morphos-3.20.iso -kernel boot.img -serial stdio

Hardware rendering through TinyGL

MorphOS' 3D interface is TinyGL (tinygl.library, part of the system on machines whose graphics card it supports - Radeon, Voodoo, Permedia). It is an OpenGL 1.x with the 1.3 texture combiners, framebuffer objects and vertex arrays, and no programmable stage at all. That is exactly the shape the game's console backends target, so it is served by a backend of the same family rather than by the engine's OpenGL one:

It is the default on both PowerPC Amigas, so an ordinary build already uses it – the CPU rasterizer is one option away for a machine whose graphics card has no 3D driver:

cmake -B ./build/morphos/ -D CMAKE_TOOLCHAIN_FILE=./cmake/toolchains/morphos.cmake \
    -D CMAKE_BUILD_TYPE=Release -D NCINE_PREFERRED_RHI=Software

LegacyGL (Sources/nCine/Graphics/RHI/LegacyGL/) is a fixed-function backend in the same sense as the Dreamcast's PowerVR or the PSP's GE one: it consumes the same transpiled fixed_function effect tables (Shaders/Generated/LegacyGlGeneratedEffects.h) and expresses each effect's texture stage as a GL_COMBINE program, so the game runs the direct tier - the scene goes straight to the display at the logical resolution and the CPU lightmap is composited by the device instead of a shader pass. Three things about it are worth knowing:

  • Indexed textures are baked, not palettised. The consoles resolve an index through hardware (CLUT, TLUT, palette bank) – legacy GL has no such thing left (GL_EXT_paletted_texture is gone from every driver and TinyGL never had it), so every indexed image is baked through its palette row into RGBA8 before it is uploaded.
  • Textures are padded to powers of two where the GL insists on them. Sampling a non-power-of-two texture is a GL 2.0 feature, so both that and GL_MAX_TEXTURE_SIZE are probed at startup (the reported limit is capped at 2048 - a desktop driver answers 16384, which is not a size anything here should build). Tileset atlases are laid out 15 tiles to a row and chunked 512 texels tall, exactly as on the fixed-function consoles, so a chunk is one texture whether or not the padding applies – anything larger that is prebaked is split into pages internally rather than rejected. NCINE_NO_NPOT in the environment forces the padded path.
  • Render targets prefer a framebuffer object and fall back to copying. Whether FBOs work is probed at startup rather than assumed - TinyGL declares the entry points unconditionally - and where they do not, an off-screen pass is drawn into the back buffer and lifted into its texture with glCopyTexSubImage2D(), which is a GL 1.1 operation. Setting NCINE_NO_FBO in the environment forces that path, which is how it can be exercised on a desktop.

What differs from a desktop build

The MorphOS SDK's headers are the C-era Amiga ones, and three of their habits collide with a modern C++ codebase. All three are handled in cmake/toolchains/morphos.cmake and cmake/toolchains/morphos-compat.h, and they are worth knowing before touching that file:

  • The ppcinline headers turn OS calls into function-like macros, and some of those names are ordinary C++ identifiers. bind is the fatal one: <unistd.h> pulls in proto/socket.h (zlib.h includes <unistd.h>, so this reaches almost every file) and the macro then rewrites std::bind inside libstdc++'s own <functional>. The build defines _NO_PPCINLINE, which keeps the prototypes and drops the macros, and force-includes morphos-compat.h for what that leaves undeclared (FD_ZERO expands to bzero).
  • proto/exec.h and proto/usergroup.h put struct Task in the global namespace, which is also the name of the engine's coroutine type - every unqualified Task<...> in the game then becomes ambiguous. Both are skipped by defining their include guards – FindTask(), which getpid() is a macro over, is declared by hand in morphos-compat.h with an opaque return type so that Task stays out of the global namespace.
  • Threads are off for the same reason, and this one cannot be worked around: MorphOS's <pthread.h> includes <exec/semaphores.h>, so merely using pthreads brings struct Task back. The port runs single-threaded, exactly like the classic Amiga - SDL2's own headers are clean, so nothing else is affected.
  • INT32_MIN and friends are still behind __STDC_LIMIT_MACROS here, the pre-C++11 rule, so the toolchain asks for them explicitly.
  • realpath(), fsync(), flock() and wcstoul() are declared by the C library and implemented by none of it – the first three are compiled out and wcstoul is provided in Sources/nCine/Backends/MorphOS/MorphOSLibcCompat.cpp, along with a definition of libstdc++'s __throw_bad_array_new_length (GCC 11 emits calls to it, the SDK's libstdc++ predates it).
  • TinyGL is reached through two globals in the program, TinyGLBase and __tglContext, because the SDK's gl* names are macros that read them (GLFoo(__tglContext, …), the library base supplying the call vector). sdl2.library cannot fill them in - it is a shared library and they live in the program - so the LegacyGL build does it: the link asks for the SDK's own TinyGL initializer by name (-u _CSTP_init_TinyGLBase, which nothing would otherwise pull out of libGL.a because SDL2's glue already declares the same global), and Sources/nCine/Backends/MorphOS/MorphOSTinyGl.cpp points __tglContext at the context SDL_GL_CreateContext() returned.
  • gethostname() and getpwuid() are not called here, and neither is libcurl linked. All three reach bsdsocket.library or usergroup.library, and a reference to one of those is enough for the loader to open it before the program starts - so a machine with no TCP/IP stack running would refuse to start the game, over a host name that only decorates a log line. The update check goes with them – online multiplayer is off here anyway.