Building the project for consoles
Guide how to build, deploy and run Jazz² Resurrection on game consoles.
Beside the desktop and mobile platforms the game runs on eight consoles. Each of them is cross-compiled with its own SDK, most of them have a bespoke window/input backend instead of SDL2 or GLFW, and five of them have no programmable shaders at all — so they are driven by one of four rendering backends written for their fixed-function graphics hardware (the Wii and the GameCube share one). This page covers the whole path for each one: installing the toolchain, configuring the build, packaging the result, copying it to the device and getting a log back out of it. For everything that is not console-specific see Building the project.
| Console | Toolchain (CMake toolchain file) | Rendering backend | Window backend | Build artifact |
|---|---|---|---|---|
| Sega Dreamcast | KallistiOS (kallistios.toolchain.cmake) | PVR — fixed-function | Dc | jazz2.cdi (bootable disc image) |
| Nintendo Wii | devkitPPC + libogc (Wii.cmake) | GX — fixed-function | Ogc | boot.dol (in a staged sd/ tree) |
| Nintendo GameCube | devkitPPC + libogc (GameCube.cmake) | GX — fixed-function | Ogc | Jazz2.dol (in a staged sd/ tree) |
| PlayStation Portable | pspdev (pspdev.cmake) | GU — fixed-function | Psp | EBOOT.PBP (in a staged ms0/ tree) |
| PlayStation 2 | ps2dev (ps2dev.cmake) | GS — fixed-function | Ps2 | jazz2.iso (bootable disc image) |
| PlayStation 3 | ps3toolchain + PSL1GHT (cmake/toolchains/ps3dev.cmake) | RSX (native libgcm, shaders) | Ps3 | jazz2.pkg (and an unsigned jazz2.self) |
| PlayStation Vita | VitaSDK (vita.toolchain.cmake) | GXM (native sceGxm, the default), OpenGL via vitaGL (ES 2.0 profile) or Software | SDL2 | jazz2.vpk |
| Nintendo Switch | devkitA64 (Switch.cmake) | OpenGL | SDL2 | jazz2.nro |
The rendering backend is not a choice on the first six — NCINE_PREFERRED_RHI is pinned to the one backend the console has and any other value is a configure error. The PlayStation 3 is the exception among them: its pin is to RSX, which unlike the four fixed-function backends is a full shader backend, so it keeps the post-processing chain and differs from a desktop build mainly in when* its shaders are compiled (see PlayStation 3). The PS Vita and the Switch are ordinary shader platforms that happen to be consoles, so most of what follows applies to them only in the deployment part.
What every console build shares
Three properties separate a console build from a desktop one, and they explain most of the extra steps below.
- The host tools are not built. ShaderCompiler and AssetPacker run on the build machine, so they are skipped for every cross-compiled target. Everything they produce is either committed to the repository (the generated shader headers) or prepared by hand ahead of time (the game content).
- The game data is converted in advance for all of them, into a content tree prepared by AssetPacker. The Dreamcast and the PlayStation 2 play from a disc and the GameCube has nowhere to put a converted installation, so those three have no first-run conversion compiled in at all (
NCINE_HAS_WRITABLE_CACHEin"Sources/Main.h") and mark their content as verified without looking at it, a build without a prepared tree reaches the main menu and finds no episode to play. The Wii, the PSP and the PlayStation 3 do have the conversion — their content sits on a writable SD card, memory stick or hard disk — but they recognize a prepared tree and skip it, and so do the Vita and the Switch, which otherwise convert on first run like a desktop build. - Five of them have no shaders. The
PVR,GX,GUandGSbackends (the Wii and the GameCube shareGX) implement each effect as a short list of fixed-function hardware passes, transpiled from the very same.shaderfiles by ShaderCompiler — see console fixed-function blocks. Because those backends expose no shader capability, the post-processing chain (and with it the rescale filters) does not exist there, the level viewport is aspect-fitted into the console's native output instead.
Preparing the game content
The content tree is prepared with AssetPacker, built as part of an ordinary desktop build. All six data-prepared consoles consume the same tree, so it is built once:
# Build the tool first (part of the regular desktop build) cmake -B build -D CMAKE_BUILD_TYPE=Release make -j $(nproc) -C build # Convert an installed copy of the original game into a console content tree ./build/Sources/Utilities/AssetPacker/AssetPacker <path to the game installation> ./build/ConsoleContent --target=console
Point the console build at that directory with NCINE_CONTENT_DIR, the build stages it into the package it creates, under the name "Content", at the place the console looks for it:
| Console | Content directory the game reads | Cache / save data |
|---|---|---|
| Dreamcast | "/cd/Content/" (inside the disc image) | "/cd/Cache/" — read-only, so nothing is ever written |
| Wii | "sd:/apps/Jazz2/Content/" | "sd:/apps/Jazz2/Cache/" |
| GameCube | "carda:/Jazz2/Content/" | "carda:/Jazz2/Cache/" |
| PlayStation Portable | "ms0:/PSP/GAME/Jazz2/Content/" | "ms0:/PSP/GAME/Jazz2/Cache/" |
| PlayStation 2 | "cdfs:/Content/" (inside the disc image) | "cdfs:/Cache/" — read-only, and nothing is staged there |
| PlayStation 3 | "/app_home/Content/" (next to the EBOOT.BIN, see below) | "/dev_hdd0/game/JAZZ20000/USRDIR/Cache/" — writable |
| PS Vita | "app0:/Content/" (inside the VPK) | "ux0:/data/jazz2/Cache/" |
| Switch | "romfs:/" (embedded in the .nro) | "sdmc:/Games/Jazz2/Cache/" |
"/app_home" on the PlayStation 3 is the alias the loader maps the running executable's own directory to, which is USRDIR rather than the package root. Going through it rather than through the title's real path means the same build runs both as an installed package and straight out of the staged package directory, which is how RPCS3 boots the build tree — and the application id can change without any path following it.
What marks the tree as prepared is the package the tool writes its sprites and sounds into: a prepared tree gets "Prebaked.pak", where a cache the game converted itself keeps "Source.pak". That package also holds the game's own "Animations" and "Metadata" directories, so a few hundred small files are one file to open on media where that is the expensive part, the translations, levels, tilesets, music and cinematics sit next to it as ordinary files. Finding that file is how a platform that could convert (the Wii, the PSP, the PS3, the Vita and the Switch) knows not to — it skips the conversion, leaves the Cache directory alone and never looks for the original game files, which are not deployed with such a tree anyway. To convert on the device instead, leave the prepared tree out and put an original installation into the console's Source directory.
What is available on which console
Everything below is decided at configure time from the platform, not from a build parameter. The values are what a default Release configuration of each console produces.
| Feature | Dreamcast | Wii | GameCube | PSP | PS2 | PS3 | Vita | Switch |
|---|---|---|---|---|---|---|---|---|
Threads (NCINE_WITH_THREADS) | Yes | Yes | Yes | No | No | No | Yes | Yes |
Asynchronous tracing (DEATH_TRACE_ASYNC) | No | No | No | No | No | No | No | No |
| Audio backend | AICA (KallistiOS sound driver) | ASND (libogc DSP mixer) | ASND (libogc DSP mixer) | OpenAL (pspdev's OpenAL Soft) | none yet (audsrv is unwritten) | PS3 (software mixer over libaudio) | OpenAL if the SDK has one | OpenAL if the SDK has one |
| Sound effects | Yes | Yes | Yes | Yes | No | Yes | SDK-dependent | SDK-dependent |
| Module music (libopenmpt) | Yes | Yes | Yes | Off on purpose (see Limits and known issues) | Off on purpose (see Limits and known issues) | Yes (built from source, single-threaded) | SDK-dependent | SDK-dependent |
| Underwater low-pass filter | No — the driver leaves it off | No — no filter stage on the DSP | No — no filter stage on the DSP | No — no efx.h in pspdev | No — no audio backend | No — no filter stage in the mixer | SDK-dependent | Yes |
| Local splitscreen | Yes | Yes | Yes | No — needs threads | No — needs threads | No — needs threads | Yes | Yes |
| Online multiplayer | No | No | No | No | No | No | Yes | Yes |
| Post-processing and rescale filters | No — fixed-function | No — fixed-function | No — fixed-function | No — fixed-function | No — fixed-function | Yes — the RSX has shaders | Yes | Yes |
External .shader files | No | No | No | No | No | No — no runtime compiler | Yes | Yes |
| Converts the game data itself | No | No | No | No | No | No | Yes | Yes |
Audio backends
Sound is structured the way rendering is: WITH_AUDIO says the audio subsystem is compiled in at all, and exactly one audio backend is compiled with it, chosen at configure time from the platform. The backend implements nCine::
Each backend lives in "nCine/Audio/Backends/<backend>/", the way the rendering ones live under "nCine/Graphics/RHI/":
| Backend | WITH_* | What it drives |
|---|---|---|
| AL | WITH_OPENAL | The system OpenAL on desktop, and the SDK's own on the PSP, Switch and Vita |
| ASND | WITH_ASND | libogc's DSP mixer, the only sound API devkitPro ships for PowerPC. Comes with the toolchain, so there is nothing to install |
| AICA | WITH_AICA | The Dreamcast sound processor through KallistiOS, using its wavetable channels for sound effects and its snd_stream driver for music |
| PS3 | WITH_PS3AUDIO | PSL1GHT's libaudio. The console offers no mixer at all — only a ring of 256-sample blocks of interleaved floats that the hardware scans out — so this backend is the mixer, on the PPE |
| none | — | The silent fallback when no backend is available, or when the game is started with audio turned off |
Nothing has to be installed by hand for any of them — each console's own SDK provides what its backend needs. NCINE_WITH_AUDIO is left at ON everywhere and turns the whole subsystem off when cleared.
Getting a log out of a console
Every console writes the same trace output the desktop build does, but each one needs a different receiver. This is the single most useful thing to set up before debugging anything.
| Console | Where the trace goes |
|---|---|
| Dreamcast | dbgio — the framebuffer console during startup (visible on screen), then the SCIF serial port once the renderer takes over. Emulators print it to their own log, dc-tool shows it in its console |
| Wii / GameCube | A USB Gecko on EXI channel 1 (memory-card slot B), probed once at startup, without the adapter nothing is written and the game runs normally |
| PlayStation Portable | stdout through sceIoWrite on fd 1 — PPSSPP prints it into its log, psplink into its console, plus the debug screen on the display until the GU session takes the framebuffer over |
| PlayStation 2 | The Emotion Engine's SIO transmit register (0x1000F180) — a bare ELF has no connected stdout. PCSX2 captures it as EE console output with EnableEEConsole on, and a serial cable picks it up on hardware |
| PlayStation 3 | sysTtyWrite — PSL1GHT routes fd 1 and 2 straight to that lv2 syscall, so an ordinary write reaches the console TTY on hardware and RPCS3's log in the emulator. No cable and no on-screen fallback are needed |
| PS Vita | sceClibPrintf — picked up by the usual host-side console tools (psp2shell, VitaCompanion) |
| Switch | svcOutputDebugString — picked up by a debug logger on the host, writing the trace to a file on the SD card is forced on this platform as well |
DEATH_TRACE_LOG_PATH additionally forces the trace into a file, which is worth using on the platforms with writable storage (not the Dreamcast, whose only medium is a read-only disc). Verbose I/O lines use the deferred trace level and are only flushed when an error follows them, so a sparse log is normal and does not mean files are not being opened.
Sega Dreamcast
The oldest and by far the tightest target: a 200 MHz SH-4 with 16 MB of main memory, 8 MB of video memory and a PowerVR2 (CLX2) that has no programmable shading, no hardware scissor and reads textures in 16-bit formats only. The port renders through the PVR backend ("Sources/nCine/Graphics/RHI/PVR") on top of KallistiOS, presented by the Dc window backend in 640x480 RGB565.
Toolchain
KallistiOS is built from source together with its sh-elf cross-compiler, there is no prebuilt package.
git clone https://github.com/KallistiOS/KallistiOS.git kos cd kos # Build the sh-elf (SH-4) cross-compiler make -C utils/kos-chain # The sample configuration matches the paths the toolchain just used cp doc/environ.sh.sample environ.sh source environ.sh make -j $(nproc) # zlib is required for the compressed game content and comes from kos-ports git clone https://github.com/KallistiOS/kos-ports.git ../kos-ports make -C ../kos-ports/zlib install
Edit "utils/kos-chain/Makefile.cfg" first if the compilers should not be installed into the default prefix, and "environ.sh" afterwards so KOS_BASE, KOS_CC_BASE and KOS_PORTS point at the tree that was just built.
To get a bootable disc image, mkdcdisc has to be on PATH as well:
git clone https://gitlab.com/simulant/mkdcdisc.git cd mkdcdisc meson setup build && ninja -C build sudo cp ./build/mkdcdisc /usr/local/bin/
Building
source <path to KOS>/environ.sh cmake -B ./build/dreamcast/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=${KOS_BASE}/utils/cmake/kallistios.toolchain.cmake \ -D NCINE_CONTENT_DIR=$PWD/build/ConsoleContent make -j $(nproc) -C ./build/dreamcast/
It leaves NCINE_CONTENT_DIR at its default, which is the repository's own "Content" — enough to reach the main menu, but pass the parameter on the first configuration of the build directory to get a playable disc. Link-time optimization is force-disabled for this platform because it makes the sh-elf GCC 15.2 abort, and Release compiles with -O2 rather than -Ofast — fast-math reordering is untested on the SH-4 and code size matters here.
The build produces "build/dreamcast/jazz2.elf" and, if mkdcdisc was found, the bootable "build/dreamcast/jazz2.cdi" with the content tree included as "/cd/Content".
Deploying and running
- Emulator —
"jazz2.cdi"boots directly in Flycast, lxdream or redream, no further preparation. - Real hardware — burn the CDI image with a tool that understands the Dreamcast's multi-session layout, or serve it from an optical-drive emulator such as GDEMU/MODE.
dc-toolover serial or a Broadband Adapter uploads"jazz2.elf"for a fast edit-run cycle, but the game reads its content from"/cd/Content/"— so a disc with the content still has to be present.
To read the log in Flycast, set Debug.SerialConsoleEnabled = yes in "flycast/emu.cfg" while the emulator is not running — it rewrites that file on exit and can silently revert the setting, which looks exactly like a game that died before main(). Startup messages go to the on-screen framebuffer console, not to serial, so a crash during initialization has to be read off the screen, the guest's serial output is block-buffered, so a crash also loses everything written since the last flush.
Limits and known issues
- Main memory is the binding constraint. The heap window between the loaded ELF and the top of RAM is roughly 13.8 MB. Exhaustion is not a clean error — it appears in the log as
Out of memory. Requested sbrk_base …followed bystd::bad_allocand an abort, which the emulator reports only as a CPU exception. Video memory has its own message,Out of PVR memory allocating …, and the render-to-texture variant of it names the target size. - The engine keeps several console-only budgets for this reason: live debris particles are capped (bursts are coarsened rather than truncated, so an explosion still looks like one), the outgoing level's assets are released before the incoming level loads, and the host copies of PVR textures are kept RLE-compressed.
- Audio costs almost nothing in main memory here, which is worth knowing on a console whose heap is the binding constraint: the AICA has its own 2 MB of sound RAM and every fully loaded sound effect lives there, not on the 13.8 MB heap. What main memory does hold is the module decoder and the three 16 KB buffers a stream is refilled through.
- The two kinds of player use two different parts of the sound processor, see nCine::
AicaAudioDevice. A sound effect is one AICA wavetable channel (two for a stereo sample, since a channel is mono — the sample is de-interleaved into one block per channel when it is uploaded), and whether it has finished is read back from the hardware rather than predicted from a timer. Music goes through KallistiOS's snd_streamdriver, of which there are only four. - A channel addresses at most 65534 samples from wherever it is pointed at, which a long sound effect can exceed (about 3 seconds of 22 kHz audio). Rather than cutting one off, nCine::
AicaAudioDevice:: uploadBuffer halves its sample rate until it fits and says so in the log — the hardware resamples every channel anyway, so the sound still plays to its end at the right pitch and only loses some high end. A sound long enough to need this more than once is really a stream. - Panning and distance attenuation are computed on the SH4 and folded into the channel volume and pan, because the hardware has no notion of a listener. Each channel does have a low-pass filter, but the KallistiOS driver switches it off and exposes no command to reach it, so nothing is muffled underwater.
- Module music is rendered at 22050 Hz rather than at the output rate: the AICA resamples every channel in hardware for free, so a higher rate would only cost the SH4 more time in the module mixer — the one place this console has no headroom to spare.
- A stream is resampled at the rate it was started with and the driver cannot change it afterwards, so pitch changes apply to sound effects only.
- Textures are 16-bit (ARGB4444, or palettized with the hardware's palette banks), the PVR has no hardware scissor (clipping is geometric), and there is no post-processing tier, so the rescale filters and the lightmap's shader path are unavailable — the lightmap is combined on the CPU instead.
- Online multiplayer is disabled, local splitscreen works.
Nintendo Wii
A 729 MHz PowerPC (Broadway) with 24 MB of fast MEM1 and 64 MB of MEM2, and the Hollywood GPU — a fixed-function design with a 16-stage TEV combiner. The port renders through the GX backend ("Sources/nCine/Graphics/RHI/GX") on libogc, presented by the Ogc window backend, which adopts whatever video mode the console prefers (PAL 50 Hz or NTSC/EDTV 60 Hz, interlaced or progressive).
Toolchain
# Install devkitPro's pacman, then the Wii group and zlib sudo dkp-pacman -S --needed wii-dev ppc-zlib export DEVKITPRO=/opt/devkitpro
ppc-zlib is not part of the wii-dev group but is required for the compressed game content. The toolchain file is "$DEVKITPRO/cmake/Wii.cmake".
Building
cmake -B ./build/wii/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=${DEVKITPRO}/cmake/Wii.cmake \ -D NCINE_CONTENT_DIR=$PWD/build/ConsoleContent make -j $(nproc) -C ./build/wii/
Beside "build/wii/jazz2.dol" the build stages the SD card layout the Homebrew Channel expects, so its contents can be copied to a card as they are:
build/wii/sd/apps/Jazz2/boot.dol build/wii/sd/apps/Jazz2/Content/…
Deploying and running
- Real hardware — copy the contents of
"build/wii/sd/"to the root of an SD card or USB storage device and launch Jazz² Resurrection from the Homebrew Channel. An"apps/Jazz2/meta.xml"is optional, without it the entry is listed under its directory name. - Dolphin — build a FAT32 image from the same staging tree and enable the virtual SD card:
truncate -s 512M sd.raw && mkfs.vfat -F 32 sd.raw mcopy -i sd.raw -s ./build/wii/sd/apps ::/ # Dolphin.ini: [Core] WiiSDCard = True, WiiSDCardPath = <path to sd.raw> flatpak run org.DolphinEmu.dolphin-emu -e ./build/wii/jazz2.dol -b
For the log, set a USB Gecko in memory-card slot B (SlotB = 7 in Dolphin.ini) and read the TCP socket Dolphin opens for it on port 55020. The adapter is probed once at startup, so a build without one behaves normally and simply writes nothing. Frame dumps are the practical way to check rendering: DumpXFBTarget = True in "GFX.ini" writes every displayed frame as a PNG.
Limits and known issues
- Sound goes through libogc's ASND, not OpenAL — devkitPro ships no OpenAL for PowerPC. The DSP mixes up to 16 voices at 48 kHz and resamples each one, so the voice count rather than CPU time is the limit, that is also the source count the engine advertises, well below the 64 it uses on desktop. Panning and distance attenuation are computed on the CPU and folded into the per-voice volume, because ASND has no notion of a listener — see
AsndAudioDevice::computeVolume(). - ASND holds only two buffers per voice (one playing, one queued) where a streaming player expects an OpenAL-style queue, so the queue itself is kept in the backend and fed into the voice one buffer at a time from nCine::
AsndAudioDevice:: updatePlayers. - Sample data is read by the DSP straight out of main memory by DMA, so every buffer is 32-byte aligned and padded. Unlike the Dreamcast, there is no separate sound RAM — a level's sound effects come out of the same heap as everything else, which is worth remembering on the GameCube in particular.
- There is no filter stage on the DSP, so nothing is muffled underwater.
- Online multiplayer is disabled, local splitscreen works.
- No post-processing tier, so no rescale filters, the lightmap is combined on the CPU.
- Textures are converted to the GX formats (including CI8 with a hardware TLUT for palettized ones) and the effect passes are expressed as TEV stages with swap tables and a KONST colour, all generated from the
.shaderfiles. - Palette data is byte-swapped for the big-endian CPU on load — worth knowing when comparing a palette dump with the desktop build.
Nintendo GameCube
The Wii's predecessor and the same code path: a 486 MHz Gekko and the Flipper GPU, driven by the same GX backend and Ogc window backend. The one difference that matters in practice is memory — 24 MB of main RAM and no MEM2, which makes it the tighter of the two PowerPC targets — and that its storage is an SD card in a memory-card slot.
Toolchain
sudo dkp-pacman -S --needed gamecube-dev ppc-zlib export DEVKITPRO=/opt/devkitpro
As on the Wii, ppc-zlib is not part of the console's package group. The toolchain file is "$DEVKITPRO/cmake/GameCube.cmake".
Building
cmake -B ./build/gamecube/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=${DEVKITPRO}/cmake/GameCube.cmake \ -D NCINE_CONTENT_DIR=$PWD/build/ConsoleContent make -j $(nproc) -C ./build/gamecube/
The staged layout differs from the Wii's, because the GameCube has no Homebrew Channel and reads the card directly:
build/gamecube/sd/Jazz2/Jazz2.dol build/gamecube/sd/Jazz2/Content/…
Deploying and running
- Real hardware — copy the contents of
"build/gamecube/sd/"to an SD card in an SD Gecko (memory-card slot A) or an SD2SP2 adapter, and start"Jazz2/Jazz2.dol"from Swiss. The game reads its content from"carda:/Jazz2/Content/", which is slot A by definition. - Dolphin — attach an SD adapter to slot A and point it at a FAT32 image built exactly like the Wii one above. Keep slot B free for the USB Gecko if the log is needed, the game writes its trace to EXI channel 1, which is slot B.
Limits and known issues
Everything in Limits and known issues applies, with two additions:
- Memory is much tighter than on the Wii — 24 MB total, with no second pool to fall back on. The console budgets described for the Dreamcast are active here as well, and audio is one more claim on that pool here: the ASND backend keeps every decoded sound effect in main memory for the DSP to read by DMA.
- There is no Bluetooth, so only GameCube controllers are read (the Wii build additionally links
wiiuseandbtefor Wii Remotes).
PlayStation Portable
A 222 MHz (raised to 333 MHz at startup) MIPS Allegrex with 32 MB of memory (24 MB usable, 64 MB on the 2000/3000 models) and a Graphics Engine driven through sceGu. The port renders through the GU backend ("Sources/nCine/Graphics/RHI/GU"), presented by the Psp window backend in the console's native 480x272.
Toolchain
pspdev ships prebuilt for Linux, macOS and Windows, can be built from source with its build-all.sh, and is also published as the pspdev/pspdev container image (which is what the CI workflow uses):
# Prebuilt SDK (see https://github.com/pspdev/pspdev/releases) tar -xJf pspdev-<platform>.tar.xz -C ~/sdk export PSPDEV=~/sdk/pspdev export PATH=$PSPDEV/bin:$PATH
Everything the game needs is in the SDK itself — zlib, libvorbis and an OpenAL Soft whose output backend drives sceAudio. Further ports are installed with psp-pacman. The CMake toolchain file is "$PSPDEV/psp/share/pspdev.cmake", the psp-cmake wrapper passes it automatically, and $PSPDEV/bin must stay on PATH because the packaging step invokes psp-fixup-imports, mksfoex and pack-pbp from there.
Building
export PSPDEV=~/sdk/pspdev export PATH=$PSPDEV/bin:$PATH cmake -B ./build/psp/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake \ -D NCINE_CONTENT_DIR=$PWD/build/ConsoleContent make -j $(nproc) -C ./build/psp/
Release compiles with -O2 for the same reasons as the other consoles — the Allegrex FPU is single-precision only.
The build produces "build/psp/jazz2.elf" and stages the complete memory-stick layout under "build/psp/ms0/":
build/psp/ms0/PSP/GAME/Jazz2/EBOOT.PBP build/psp/ms0/PSP/GAME/Jazz2/Content/…
The EBOOT.PBP is packed by create_pbp_file() from the stripped ELF with MEMSIZE=1 in its SFO, which asks the firmware for the extra memory of the 2000/3000 models, a PSP-1000 keeps its 24 MB.
Deploying and running
- Real hardware — copy
"build/psp/ms0/PSP/GAME/Jazz2"onto the memory stick at exactly that path. Requires custom firmware or another way to start unsigned homebrew. - PPSSPP — copy the same directory into the emulator's configured memory stick, then start it from the game list.
The application raises the CPU to 333 MHz itself and installs its HOME-button callback before anything else can fail, so the console always has a way out even if initialization goes wrong.
Limits and known issues
- Module music is disabled. The game's soundtrack is tracker modules, and while libopenmpt does cross-compile, play correctly and cost only ~0.2 FPS in steady state, the first module a process loads costs a fixed ~29 seconds inside the library on this CPU (every later load takes ~1.8 s) — most likely because the Allegrex has no double-precision unit. There is nowhere to hide a half-minute freeze on a handheld, so
NCINE_WITH_OPENMPTis forced off, sound effects work normally. - Threads are off, so multiplayer (including local splitscreen) is compiled out and asset loading is synchronous. pspdev does ship a pthreads implementation, but the engine's threading layer also wants thread names, priorities and affinity, which do not map onto
sceKernelCreateThreadas they stand. - The underwater and pause low-pass filters are absent, because pspdev's OpenAL has no
efx.h. - Textures are limited to 512 pixels per axis, so larger atlases are split into pages with per-primitive page selection, palettized textures use the hardware CLUT, of which only one is resident at a time, so the palette is part of the batch key.
- The GE has no post-texture additive term and no combiner output scale, which is why an
offset_colorpass expands into a modulated draw plus an additive silhouette, and why theMODULATE_X2/MODULATE_X4presets are rejected for this target.
PlayStation 2
A 294 MHz MIPS R5900 "Emotion Engine" with 32 MB of main memory and a Graphics Synthesizer holding 4 MB of local memory that is at once the frame buffer, the texture cache and the palette store. The port renders through the GS backend ("Sources/nCine/Graphics/RHI/GS"), presented by the Ps2 window backend in NTSC 640x448.
Toolchain
ps2dev is published as the ps2dev/ps2dev container image, which is the easiest way to get it and what the build recipe below assumes. The image ships no CMake, so derive one that has it:
FROM docker.io/ps2dev/ps2dev:latest RUN apk add --no-cache cmake make git python3 bash xorriso
Building
cmake -S . -B build/ps2 -DCMAKE_TOOLCHAIN_FILE=$PS2SDK/ps2dev.cmake \ -DCMAKE_BUILD_TYPE=Release -DNCINE_STRIP_BINARIES=ON \ -DNCINE_CONTENT_DIR=<path to the console content tree> cmake --build build/ps2 -j$(nproc)
The toolchain file defines PLATFORM_PS2, from which NCINE_PREFERRED_RHI is pinned to GS. The build produces a stripped jazz2.elf and stages a bootable jazz2.iso around it: SYSTEM.CNF (BOOT2 = cdrom0:\JAZZ2.ELF;1, VMODE = NTSC), the ELF as JAZZ2.ELF, CDFS.IRX from the SDK, and the content tree as Content/, whose Source.pak is renamed to Prebaked.pak on the way in so the game recognizes it as a prepared tree. Nothing is staged into Cache/: a prepared tree needs none, and the disc is read-only anyway.
Deploying and running
Burn jazz2.iso to a disc, or load it in PCSX2. A headless run needs the emulator's setup wizard marked complete and a BIOS filename set in its INI, EnableEEConsole turned on to see any output at all, and a renderer the host GPU can actually create:
pcsx2-qt -batch -nogui -earlyconsolelog -logfile run.log jazz2.iso
A bare ELF has no connected stdout, so the trace goes to the Emotion Engine's SIO transmit register (0x1000F180) instead of printf. PCSX2 captures that as EE console output and a serial cable picks it up on real hardware. The same channel carries the early messages the CDVD bring-up in MainApplication::Run() emits, before the trace system exists.
Limits and known issues
- Local memory is the binding constraint. 4 MB holds the display buffer, the render-target reserve and the palette slab before any texture: a 640x448
PSMCT16buffer plus the reserve and the CLUT slab leave about 2816 KB of texture cache. A frame's working set fits comfortably (~856 KB measured) but a whole level's sheets do not, so textures stream in and out on transitions and a busy level still reports allocation failures. The allocator hands out whole 8 KB pages, because within a page the texel-to-block swizzle depends on the pixel storage mode and two buffers of different modes sharing a page would corrupt each other. - There is no audio backend yet.
audsrvis the intended path, until it exists the game runs silent andNCINE_WITH_OPENMPTis forced off (libopenmpt'sto_chars(char*, char*, const double&)is ambiguous against PS2SDK's newlib). - Threads are off, so multiplayer (including local splitscreen) is compiled out and asset loading is synchronous.
- Two pipeline stages are still unimplemented: the weapon wheel's line strip (
line_strip_mesh) and the CPU-lightmap compositor (lighting_combine, so a level renders unlit). Jazz2.configcannot be written — a disc is read-only and the PS2 has no working directory, so preferences need a memory-card path (mc0:/).- The GS has no post-texture additive term for a colour and no combiner output scale, so an
offset_colorpass expands into a modulated draw plus an additive silhouette (which the GS reaches through a white-RGB CLUT — seeGsDevice::AcquireCoverageClutForOffset()) and theMODULATE_X2/MODULATE_X4presets are rejected for this target.
PlayStation 3
A 3.2 GHz Cell "PPE" with 256 MB of XDR main memory, and an RSX — an NVIDIA NV47 — with 256 MB of GDDR3 of its own. The port renders through the RSX backend ("Sources/nCine/Graphics/RHI/RSX"), presented by the Ps3 window backend at whatever mode the attached display accepts.
This is the odd one out among the console ports. The RSX is a fully programmable part, so despite being a console backend it belongs with GXM and the desktop backends rather than with the fixed-function PVR/GX/GU/GS tier: it advertises shaders and off-screen render targets, runs the whole bloom / lighting / combine / rescale chain, and consumes none of the transpiled fixed_function effect tables. What separates it from the PS Vita — which wants the same Cg — is only when the shaders are compiled. The Vita has SceShaccCg on the console, the PS3 has no runtime shader compiler at all, so the same Cg is compiled to NV40 microcode offline and embedded in the executable, the arrangement the Vulkan backend uses for its SPIR-V.
Toolchain
Two pieces are needed, plus one that is not part of either.
- ps3toolchain provides the
powerpc64-ps3-elfcompilers. Building them from source takes hours, the project's nightly releases ship a prebuiltps3dev-linux-X64.tar.gzthat only has to be unpacked. - PSL1GHT is the SDK proper — librsx, libgcm_sys, libio, libaudio, libsysutil — and the host tools the packaging step runs (
fself,make_self_npdrm,sfo,pkg,sprxlinker, andcgcomp). It is built from source by step 8 of ps3toolchain, which takes a few minutes. - NVIDIA's Cg Toolkit is needed only to regenerate the shader table, not to build the game:
cgcompis a thin front end overlibCg.soand does nothing without it. See Shaders are compiled offline.
export PS3DEV=<prefix> # e.g. ~/sdk/ps3dev export PSL1GHT=$PS3DEV/psl1ght export PATH=$PS3DEV/bin:$PS3DEV/ppu/bin:$PS3DEV/spu/bin:$PATH # Prebuilt compilers curl -L -o ps3dev.tar.gz https://github.com/ps3dev/ps3toolchain/releases/download/<nightly>/ps3dev-linux-X64.tar.gz tar xzf ps3dev.tar.gz -C $(dirname $PS3DEV) # PSL1GHT and the host tools (step 8), then the portlibs (step 9) git clone --depth 1 https://github.com/ps3dev/ps3toolchain cd ps3toolchain && ./toolchain.sh 8 9
Building
./BuildPs3.sh
The script exports the environment described above, validates that the compiler and PSL1GHT are where it expects, and configures with the project's own toolchain file. Doing it by hand is the same three lines:
cmake -S . -B build/ps3 -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/toolchains/ps3dev.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DNCINE_CONTENT_DIR=<path to the console content tree> cmake --build build/ps3 -j$(nproc)
The toolchain file defines PLATFORM_PS3, from which NCINE_PREFERRED_RHI is pinned to RSX. The build produces jazz2.elf, then stages a package tree around a stripped copy of it: sprxlinker rewrites the PRX import stubs, make_self_npdrm wraps the result as the EBOOT.BIN a package boots from, fself produces an unsigned jazz2.self that RPCS3 and a CFW console will also run straight from disk, and sfo writes the PARAM.SFO carrying the title and the application id (JAZZ20000). The content tree is copied to USRDIR/Content with its Source.pak renamed to Prebaked.pak, and pkg wraps the whole tree into jazz2.pkg.
The application id is not written down anywhere — it is derived from the application name, padded with zeroes to the nine characters the firmware demands and uppercased, exactly as the PS Vita title id is derived (see PlayStation Vita). The content id, PARAM.SFO and the writable path the game uses at runtime all follow from that one value, so they cannot drift apart. Configure with -DPS3_APPID=<id> to override it, a value that is not four letters followed by five digits fails configuration rather than producing a package the firmware would reject.
Shaders are compiled offline
There is no shader compiler on the console, so the microcode is generated ahead of time and committed, exactly as the other generated shader headers are:
PS3DEV=<prefix> LD_LIBRARY_PATH=<Cg toolkit>/usr/lib64 \ ./build/Sources/Utilities/ShaderCompiler/ShaderCompiler --generate-all
It is a mode of ShaderCompiler like every other generated artifact — --emit-rsx, which --generate-all runs when it can find cgcomp. The tool emits the very same Cg it produces for the PS Vita, because the two consoles want the same language, and only caps BATCH_SIZE to what the RSX's constant registers hold before compiling each stage with the vp40/fp40 profiles. cgcomp is found through $PS3DEV/bin or PATH, or named with --cgcomp, it needs NVIDIA's Cg Toolkit on the library path, which is what LD_LIBRARY_PATH above supplies.
On a machine without cgcomp, --generate-all reports "skipped: RsxGeneratedShaders.h" and leaves the committed header alone rather than emptying it — so a regeneration elsewhere cannot silently strip the console of its shaders. Commit the result, the console build never invokes cgcomp, so neither the PS3 toolchain nor the Cg Toolkit is a build dependency.
A stage the profiles reject is reported and left out of the table. The backend then fails that one program at load time with a message naming the variant, rather than mis-rendering silently, see Limits and known issues for which ones those currently are.
Deploying and running
Install jazz2.pkg on a console with custom firmware, or run the staged tree directly:
rpcs3 --no-gui build/ps3/pkg/USRDIR/EBOOT.BIN
RPCS3 needs the PlayStation 3 firmware installed once (File → Install Firmware, or --installfw with the PS3UPDAT.PUP Sony publishes), without it the emulator stops at "Missing Firmware" before the title starts. Booting the EBOOT.BIN in place is what makes /app_home resolve to USRDIR, so the staged content is found with no install step.
Limits and known issues
- Threads are off. Unlike the PSP and the PS2, which at least ship a pthreads shim, PSL1GHT has none: the newlib installs a
<pthread.h>that declares symbols nothing implements, sopthread_createdoes not even link. Threading here means lv2's own PPU threads (sysThreadCreate,sys/mutex.h,sys/cond.h), which would be a new backend in"Thread.cpp"rather than a configuration change. Multiplayer (including local splitscreen) is therefore compiled out and asset loading is synchronous. - Three rescale filters have no microcode:
ResizeCleanEdgeneeds 92 temporary registers wherefp40has 64,ResizeSabrneeds a ninthTEXCOORDoutput, andResizeMonochromeindexes an array the profile only allows indexing for texture coordinates. The other 58 program variants compile. The backend leavesRHI_CAP_HEAVY_RESCALE_SHADERSundefined, which drops those three modes from *Options > Graphics > Rescale Mode* and skips compiling them at all; a value stored by another backend needs no special handling, becauseUpscaleRenderPassalready falls back to the plain sprite shader when a mode resolves to no program, which is exactly the pixel-perfect mode. - Sprite batching is turned off. The backend leaves
RHI_CAP_BATCHINGundefined, soRenderResources::GetBatchedShader()reports no batched counterpart — which is how the batcher is told a run cannot be combined — and every sprite is drawn individually. That one switch also skips linking the batched programs, keeping a dozen of them out of memory. The cause is the microcode cgcomp emits for the instance indexing: a batched vertex program reaches the instance array through the address register, and the emulator's decompiler demands a register type in all three source slots of every instruction, abandoning the whole program when one is zero (Src check failed. Aborting) before the position output is ever written — so a batched draw renders nothing while unbatched ones are correct. Whether real hardware would accept it is untested. Patching the emitted microcode to fill those slots was tried and reverted: it silenced the complaint but turned real constant fetches into temporary reads, so a type of zero is not the "unused slot" that attempt assumed. - The batch would be 32 sprites when it is re-enabled, against 585 on a platform with uniform buffers, because a batched instance array reaches the vertex program through its constant registers rather than a buffer. Three places have to agree on that number and none can derive it from the others: the microcode bakes it in offline (ShaderCompiler's
RsxMaxBatchSize),RsxDevice::MaxBatchSizesizes the batched corner stream that supplies the instance index, and the backend publishes it asIRhiCapabilities::IntValues::MaxBatchSizeso that both the shader compilation paths andRenderBatcherbound themselves by it. Left to derive itself from the uniform block budget the engine picks 65, which overruns both the corner stream and the instance array; the backend says so at load time rather than clamping quietly. - Stage attributes carry the Cg entry point's struct qualifier. cgcomp records a generated stage's inputs as
_input.aQuadCornerrather thanaQuadCorner, because the emitter passes them in a struct, while the hand-written present shader's are unqualified.rsxVertexProgramGetAttrib()only matches whole names, so the backend matches the qualifier as a suffix instead (RsxDevice::FindVertexAttribute()). Asking for the bare name silently finds nothing, the stream is never bound, and every sprite reads whatever the previous draw left in that register — which renders an entirely black frame with no error anywhere. - The three resize filters and batching aside, nothing else is knowingly unimplemented. Under the emulator the game runs at 60 FPS with tilemaps, sprites, HUD, menus and the textured backgrounds all correct, and the command queue stays healthy (no FIFO desync, no aborted vertex programs) — but the port has not had the testing on real hardware that the older consoles have.
PlayStation Vita
PlayStation Vita is a shader platform, and the only console here with a choice of two hardware backends. Its GPU is a PowerVR SGX543MP4+ driven by sceGxm, the console's own graphics API, the window and input come from SDL2, so the build differs from a desktop one mostly in its packaging.
NCINE_PREFERRED_RHI | What it drives |
|---|---|
GXM (the default) | sceGxm directly. A full-pipeline backend — unlike the fixed-function consoles above, the SGX is a unified-shader part, so the whole bloom / lighting / combine chain runs. What it removes compared to the OpenGL path on the same console is the translation layer between the engine and sceGxm, nothing else |
OpenGL | vitaGL, an OpenGL|ES 2.0 implementation layered on that very same sceGxm. Force-enables the OpenGL|ES path and the strict ES 2.0 profile |
Software | The CPU rasterizer, for comparison and bring-up |
Toolchain
Install VitaSDK with vdpm (see vitasdk.org) and export VITASDK environment variable, then use "$VITASDK/share/vita.toolchain.cmake".
Building
export VITASDK=/usr/local/vitasdk cmake -B ./build/vita/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake make -j $(nproc) -C ./build/vita/
The native GXM backend is what this builds, add "-D NCINE_PREFERRED_RHI=OpenGL" for the vitaGL one instead. Either way the console needs "libshacccg.suprx", see The console needs a firmware module. The result is "build/vita/jazz2.vpk".
Deploying and running
Install the VPK with VitaShell (or over FTP). It carries the "Content" directory with it, which the firmware unpacks into the application's own read-only directory ("ux0:/app/jazz20000/", mounted as "app0:"), so nothing else has to be copied for the game to start.
Unlike the six consoles above, the Vita converts the original game data itself on first run, so copy an original installation to "ux0:/data/jazz2/Source/" and let it build "ux0:/data/jazz2/Cache/" — or prepare the tree with AssetPacker anyway to skip the wait. Both of those live on "ux0:" because "app0:" cannot be written to, reinstalling the VPK replaces the content but leaves the cache, the converted source data and the settings alone.
The console needs a firmware module
Shaders are compiled on the console, whichever backend is selected, because the VitaSDK ships no offline shader compiler for the platform: the only one is "libshacccg.suprx", part of the console's own firmware. Extract it with VitaShaRK's instructions and place it in "ur0:/data/".
GXMconsumes GXP binaries, so it ships its shaders as Cg source (the generated"CgGeneratedShaders.h", see The Cg dialect (PS Vita and PlayStation 3)) and compiles them through vitaShaRK when a program links. Without the module the backend refuses to start and says exactly that in the log.OpenGLhands GLSL toglCompileShader(), and vitaGL compiles it through the very same SceShaccCg. That is also what lets external".shader"files and the rescale filters work on this platform, unlike the fixed-function consoles above.
Limits and known issues
- The ES 2.0 profile has no uniform buffer objects and no
gl_VertexID, so its shaders come from the ESSL 100 lowering that ShaderCompiler bakes into the same generated headers, see Platform notes. TheGXMbackend has the same missing vertex-ID input and reuses the very same rewrite, so both read the quad corner from a vertex attribute instead. - Under Vita3K the
GXMbackend draws a one-pixel seam where a repeating texture wraps — most visibly along the scrolling textured background. The wrap is set up correctly for the hardware (a power-of-two, tiled colour-surface texture whose control words read back withSCE_GXM_TEXTURE_ADDR_REPEATon both axes) and the emulator translates sceGxm onto desktop OpenGL, where such a texture repeats without any restriction, so the join is expected to be absent on a console. No workaround is applied for it: emulating one in the shared shader would cost every other backend and could not blend across the join anyway. - Online multiplayer is unavailable: ENet has no Vita arm (it needs
"sys/ioctl.h", which the SDK does not provide), soWITH_ONLINE_MULTIPLAYERis forced off. Local (split-screen) multiplayer is unaffected. - Asynchronous tracing is disabled, threads are available.
- Sound depends on the SDK providing an OpenAL implementation — without one, audio is compiled out exactly as on the fixed-function consoles.
Nintendo Switch
Also a shader platform, built with devkitA64 and using SDL2 and desktop OpenGL. Install the switch-dev group and use "$DEVKITPRO/cmake/Switch.cmake":
sudo dkp-pacman -S --needed switch-dev cmake -B ./build/switch/ -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_TOOLCHAIN_FILE=${DEVKITPRO}/cmake/Switch.cmake \ -D DEATH_TRACE_ASYNC=OFF make -j $(nproc) -C ./build/switch/
DEATH_TRACE_ASYNC has to be off — it crashes on startup on this platform. The build produces "build/switch/jazz2.nro" with the shipped game content embedded in its RomFS, so the application is self-contained: copy the .nro to "sd:/switch/" and start it from hbmenu. Like the Vita, the Switch converts the original game data itself — put an original installation into "sdmc:/Games/Jazz2/Source/" and the converted cache and save data appear next to it in "sdmc:/Games/Jazz2/".
Libretro core
Not a console target in itself, but the other way to reach a console-like device using libretro interface: with NCINE_BUILD_LIBRETRO=ON the game builds as a libretro core ("jazz2_libretro.so", sources in "Sources/nCine/Backends/Libretro") that RetroArch drives, with no window backend of its own. NCINE_PREFERRED_RHI accepts Software (the CPU rasterizer's framebuffer is handed to retro_video_refresh, which works on every frontend) or OpenGL (rendering into the frontend's FBO through SET_HW_RENDER, targeting OpenGL|ES 3.0 as the common denominator of RetroArch's GPU platforms).
cmake -B ./build/libretro/ -D CMAKE_BUILD_TYPE=Release \ -D NCINE_BUILD_LIBRETRO=ON -D NCINE_PREFERRED_RHI=Software make -j $(nproc) -C ./build/libretro/
Continuous integration
Every console is built on each push by its own workflow in ".github/workflows", all of them inside the SDK's container image, and each uploads a ready-to-deploy package as an artifact:
| Workflow | Container image | Artifact |
|---|---|---|
dreamcast.yml | pcercuei/dreamcast-toolchain (compilers only — KOS, kos-ports and mkdcdisc are built in the job and cached) | Jazz2.cdi + Jazz2.elf |
wii.yml | devkitpro/devkitppc | the sd/ card tree with apps/Jazz2/boot.dol |
gamecube.yml | devkitpro/devkitppc | the sd/ card tree with Jazz2/Jazz2.dol |
psp.yml | pspdev/pspdev | the ms0/ memory-stick tree with EBOOT.PBP |
ps2.yml | ps2dev/ps2dev | Jazz2.iso + Jazz2.elf (the bare ELF because PCSX2 boots one with -elf) |
vita.yml | vitasdk/vitasdk | Jazz2.vpk |
switch.yml | devkitpro/devkita64 | Jazz2.nro |
The PlayStation 3 has no workflow yet. Unlike every other console here it has no official SDK container image to build inside, and the prebuilt compilers plus a PSL1GHT built from source are a larger setup step than the others need, Toolchain describes doing it by hand.
The KallistiOS, kos-ports and mkdcdisc revisions in dreamcast.yml are pinned to specific commits and their build output is cached against those commits: an unpinned clone of KOS master can otherwise break the workflow — or silently change the runtime — without any change in this repository. The workflows build against the repository's own "Content" (they cannot ship converted original game data), so their artifacts boot into the main menu and need a content tree prepared as described in Preparing the game content to actually play a level.
Troubleshooting
- The game starts, but no episode can be played. The content tree is missing. The Dreamcast, the GameCube and the PlayStation 2 mark their content as verified without checking it, and the others find no
"Prebaked.pak"and no original files to convert, so this is what a missing or misplaced"Content"directory looks like, check the path for the console in Preparing the game content. - Text is missing, tilesets look wrong, or a level refuses to load. Almost always a stale content tree — regenerate it with an AssetPacker built from the current sources. On the Wii and GameCube also make sure the card really was rewritten, an emulator's SD image and a physical card both keep an old copy convincingly.
- No CDI image after a Dreamcast build.
mkdcdiscwas not onPATHwhen CMake configured the build directory, see Building. - An empty log. Each console needs its receiver enabled — the serial console in Flycast's configuration, a USB Gecko in slot B on the PowerPC consoles, PPSSPP's stdout. On the Dreamcast and the PSP the startup messages go to the screen rather than to the log, which is exactly the window in which a crash would otherwise be invisible.
std::bad_alloc, ansbrkmessage, or a CPU exception with no explanation on the Dreamcast or GameCube is main memory running out,Out of PVR memory allocating …on the Dreamcast is video memory, see Limits and known issues.- PPSSPP runs an old build. It boots from its own configured memory stick, see Deploying and running.
- CMake cannot find the compiler on the Dreamcast.
"environ.sh"was not sourced in that shell, see Toolchain. - A fully loaded sound is cut short on the Dreamcast. An AICA channel addresses 65534 samples at most, see Limits and known issues.
- Sound effects are noise on the Wii or GameCube. The buffer formats are native-endian, so a reader that emits little-endian samples on a big-endian console produces exactly this, see Audio backends.
- "Dead FIFO commands queue state" in RPCS3. Most often the emulator rather than the game — a headless run, a host with no GPU or one that is swapping all provoke it, see Deploying and running. When it is genuine it usually means memory the GPU is still reading was reused, or the command ring wrapped without being flushed.
- A shader reports "has no compiled RSX microcode" on the PlayStation 3. That variant was rejected by the
vp40/fp40profiles when the table was generated, which is a build-time fact and not something that can change on the console, see Limits and known issues. ppu-gccrefuses to run, with "environment variable 'PSL1GHT' not defined". It reads that variable itself, so it has to be exported in the shell that runs the build, see Toolchain.- The PlayStation 3 build does not link, with
DeflateStreamunresolved. The portlibs were not built — zlib is a hard requirement, see Toolchain.