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.

ConsoleToolchain (CMake toolchain file)Rendering backendWindow backendBuild artifact
Sega DreamcastKallistiOS (kallistios.toolchain.cmake)PVR — fixed-functionDcjazz2.cdi (bootable disc image)
Nintendo WiidevkitPPC + libogc (Wii.cmake)GX — fixed-functionOgcboot.dol (in a staged sd/ tree)
Nintendo GameCubedevkitPPC + libogc (GameCube.cmake)GX — fixed-functionOgcJazz2.dol (in a staged sd/ tree)
PlayStation Portablepspdev (pspdev.cmake)GU — fixed-functionPspEBOOT.PBP (in a staged ms0/ tree)
PlayStation 2ps2dev (ps2dev.cmake)GS — fixed-functionPs2jazz2.iso (bootable disc image)
PlayStation 3ps3toolchain + PSL1GHT (cmake/toolchains/ps3dev.cmake)RSX (native libgcm, shaders)Ps3jazz2.pkg (and an unsigned jazz2.self)
PlayStation VitaVitaSDK (vita.toolchain.cmake)GXM (native sceGxm, the default), OpenGL via vitaGL (ES 2.0 profile) or SoftwareSDL2jazz2.vpk
Nintendo SwitchdevkitA64 (Switch.cmake)OpenGLSDL2jazz2.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_CACHE in "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, GU and GS backends (the Wii and the GameCube share GX) implement each effect as a short list of fixed-function hardware passes, transpiled from the very same .shader files 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:

ConsoleContent directory the game readsCache / 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.

FeatureDreamcastWiiGameCubePSPPS2PS3VitaSwitch
Threads (NCINE_WITH_THREADS)YesYesYesNoNoNoYesYes
Asynchronous tracing (DEATH_TRACE_ASYNC)NoNoNoNoNoNoNoNo
Audio backendAICA (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 oneOpenAL if the SDK has one
Sound effectsYesYesYesYesNoYesSDK-dependentSDK-dependent
Module music (libopenmpt)YesYesYesOff on purpose (see Limits and known issues)Off on purpose (see Limits and known issues)Yes (built from source, single-threaded)SDK-dependentSDK-dependent
Underwater low-pass filterNo — the driver leaves it offNo — no filter stage on the DSPNo — no filter stage on the DSPNo — no efx.h in pspdevNo — no audio backendNo — no filter stage in the mixerSDK-dependentYes
Local splitscreenYesYesYesNo — needs threadsNo — needs threadsNo — needs threadsYesYes
Online multiplayerNoNoNoNoNoNoYesYes
Post-processing and rescale filtersNo — fixed-functionNo — fixed-functionNo — fixed-functionNo — fixed-functionNo — fixed-functionYes — the RSX has shadersYesYes
External .shader filesNoNoNoNoNoNo — no runtime compilerYesYes
Converts the game data itselfNoNoNoNoNoNoYesYes

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::IAudioDevice — buffers, sources and the streaming queue — and nCine::AudioBuffer, nCine::IAudioPlayer and nCine::AudioStream contain no backend calls of their own, so a console only has to provide a device.

Each backend lives in "nCine/Audio/Backends/<backend>/", the way the rendering ones live under "nCine/Graphics/RHI/":

BackendWITH_*What it drives
ALWITH_OPENALThe system OpenAL on desktop, and the SDK's own on the PSP, Switch and Vita
ASNDWITH_ASNDlibogc's DSP mixer, the only sound API devkitPro ships for PowerPC. Comes with the toolchain, so there is nothing to install
AICAWITH_AICAThe Dreamcast sound processor through KallistiOS, using its wavetable channels for sound effects and its snd_stream driver for music
PS3WITH_PS3AUDIOPSL1GHT'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
noneThe 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.

ConsoleWhere the trace goes
Dreamcastdbgio — 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 / GameCubeA 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 Portablestdout 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 2The 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 3sysTtyWrite — 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 VitasceClibPrintf — picked up by the usual host-side console tools (psp2shell, VitaCompanion)
SwitchsvcOutputDebugString — 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-tool over 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 by std::bad_alloc and 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_stream driver, 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 .shader files.
  • 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 wiiuse and bte for 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_OPENMPT is 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 sceKernelCreateThread as 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_color pass expands into a modulated draw plus an additive silhouette, and why the MODULATE_X2/MODULATE_X4 presets 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 PSMCT16 buffer 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. audsrv is the intended path, until it exists the game runs silent and NCINE_WITH_OPENMPT is forced off (libopenmpt's to_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.config cannot 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_color pass expands into a modulated draw plus an additive silhouette (which the GS reaches through a white-RGB CLUT — see GsDevice::AcquireCoverageClutForOffset()) and the MODULATE_X2/MODULATE_X4 presets 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-elf compilers. Building them from source takes hours, the project's nightly releases ship a prebuilt ps3dev-linux-X64.tar.gz that 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, and cgcomp). 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: cgcomp is a thin front end over libCg.so and 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, so pthread_create does 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: ResizeCleanEdge needs 92 temporary registers where fp40 has 64, ResizeSabr needs a ninth TEXCOORD output, and ResizeMonochrome indexes an array the profile only allows indexing for texture coordinates. The other 58 program variants compile. The backend leaves RHI_CAP_HEAVY_RESCALE_SHADERS undefined, 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, because UpscaleRenderPass already 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_BATCHING undefined, so RenderResources::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::MaxBatchSize sizes the batched corner stream that supplies the instance index, and the backend publishes it as IRhiCapabilities::IntValues::MaxBatchSize so that both the shader compilation paths and RenderBatcher bound 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.aQuadCorner rather than aQuadCorner, 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_RHIWhat 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
OpenGLvitaGL, 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
SoftwareThe 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/".

  • GXM consumes 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.
  • OpenGL hands GLSL to glCompileShader(), 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. The GXM backend 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 GXM backend 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 with SCE_GXM_TEXTURE_ADDR_REPEAT on 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), so WITH_ONLINE_MULTIPLAYER is 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:

WorkflowContainer imageArtifact
dreamcast.ymlpcercuei/dreamcast-toolchain (compilers only — KOS, kos-ports and mkdcdisc are built in the job and cached)Jazz2.cdi + Jazz2.elf
wii.ymldevkitpro/devkitppcthe sd/ card tree with apps/Jazz2/boot.dol
gamecube.ymldevkitpro/devkitppcthe sd/ card tree with Jazz2/Jazz2.dol
psp.ymlpspdev/pspdevthe ms0/ memory-stick tree with EBOOT.PBP
ps2.ymlps2dev/ps2devJazz2.iso + Jazz2.elf (the bare ELF because PCSX2 boots one with -elf)
vita.ymlvitasdk/vitasdkJazz2.vpk
switch.ymldevkitpro/devkita64Jazz2.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. mkdcdisc was not on PATH when 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, an sbrk message, 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/fp40 profiles 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-gcc refuses 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 DeflateStream unresolved. The portlibs were not built — zlib is a hard requirement, see Toolchain.