namespace Death {
//###==##====#=====--==~--~=~- --- -- -  -  -   -

/** @page consoles Building the project for consoles
@brief Guide how to build, deploy and run Jazz² Resurrection on game consoles

@tableofcontents

@m_class{m-noindent}

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
@m_span{m-text m-dim} (the Wii and the GameCube share one) @m_endspan. 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 @ref building "Building the project".

Console | Toolchain @m_span{m-text m-dim} (CMake toolchain file) @m_endspan | Rendering backend | Window backend | Build artifact
------- | -------------------------------------------------------------- | ----------------- | -------------- | --------------
@ref consoles-dreamcast "Sega Dreamcast" | KallistiOS @m_span{m-text m-dim} (`kallistios.toolchain.cmake`) @m_endspan | `PVR` --- fixed-function | `Dc` | `jazz2.cdi` @m_span{m-text m-dim} (bootable disc image) @m_endspan
@ref consoles-wii "Nintendo Wii" | devkitPPC + libogc @m_span{m-text m-dim} (`Wii.cmake`) @m_endspan | `GX` --- fixed-function | `Ogc` | `boot.dol` @m_span{m-text m-dim} (in a staged `sd/` tree) @m_endspan
@ref consoles-gamecube "Nintendo GameCube" | devkitPPC + libogc @m_span{m-text m-dim} (`GameCube.cmake`) @m_endspan | `GX` --- fixed-function | `Ogc` | `Jazz2.dol` @m_span{m-text m-dim} (in a staged `sd/` tree) @m_endspan
@ref consoles-psp "PlayStation Portable" | pspdev @m_span{m-text m-dim} (`pspdev.cmake`) @m_endspan | `GU` --- fixed-function | `Psp` | `EBOOT.PBP` @m_span{m-text m-dim} (in a staged `ms0/` tree) @m_endspan
@ref consoles-ps2 "PlayStation 2" | ps2dev @m_span{m-text m-dim} (`ps2dev.cmake`) @m_endspan | `GS` --- fixed-function | `Ps2` | `jazz2.iso` @m_span{m-text m-dim} (bootable disc image) @m_endspan
@ref consoles-ps3 "PlayStation 3" | ps3toolchain + PSL1GHT @m_span{m-text m-dim} (`cmake/toolchains/ps3dev.cmake`) @m_endspan | `RSX` @m_span{m-text m-dim} (native libgcm, **shaders**) @m_endspan | `Ps3` | `jazz2.pkg` @m_span{m-text m-dim} (and an unsigned `jazz2.self`) @m_endspan
@ref consoles-vita "PlayStation Vita" | VitaSDK @m_span{m-text m-dim} (`vita.toolchain.cmake`) @m_endspan | `GXM` @m_span{m-text m-dim} (native sceGxm, the default) @m_endspan, `OpenGL` via **vitaGL** @m_span{m-text m-dim} (ES 2.0 profile) @m_endspan or `Software` | SDL2 | `jazz2.vpk`
@ref consoles-switch "Nintendo Switch" | devkitA64 @m_span{m-text m-dim} (`Switch.cmake`) @m_endspan | `OpenGL` | SDL2 | `jazz2.nro`

@m_class{m-noindent}

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 @m_span{m-text m-dim} (see @ref consoles-ps3) @m_endspan. 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.

@section consoles-common 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.** @ref shader-compiler "ShaderCompiler" and
    @ref asset-packer "AssetPacker" run on the build machine, so they are skipped for every
    cross-compiled target. Everything they produce is either committed to the repository
    @m_span{m-text m-dim} (the generated shader headers) @m_endspan or prepared by hand ahead of
    time @m_span{m-text m-dim} (the game content) @m_endspan.
-   **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
    @m_span{m-text m-dim} (`NCINE_HAS_WRITABLE_CACHE` in @cpp "Sources/Main.h" @ce) @m_endspan 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
    @m_span{m-text m-dim} (the Wii and the GameCube share `GX`) @m_endspan implement each effect as a
    short list of fixed-function hardware passes, transpiled from the very same `.shader` files by
    ShaderCompiler --- see @ref shader-compiler-fixed-function "console fixed-function blocks".
    Because those backends expose no shader capability, the post-processing chain
    @m_span{m-text m-dim} (and with it the rescale filters) @m_endspan does not exist there, the
    level viewport is aspect-fitted into the console's native output instead.

@subsection consoles-common-content Preparing the game content

The content tree is prepared with @ref asset-packer "AssetPacker", built as part of an ordinary
desktop build. All six data-prepared consoles consume the **same** tree, so it is built once:

@code{.sh}
# 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
@endcode

@m_class{m-noindent}

Point the console build at that directory with `NCINE_CONTENT_DIR`, the build stages it into the
package it creates, under the name @cpp "Content" @ce, at the place the console looks for it:

Console | Content directory the game reads | Cache / save data
------- | -------------------------------- | -----------------
Dreamcast | @cpp "/cd/Content/" @ce @m_span{m-text m-dim} (inside the disc image) @m_endspan | @cpp "/cd/Cache/" @ce --- read-only, so nothing is ever written
Wii | @cpp "sd:/apps/Jazz2/Content/" @ce | @cpp "sd:/apps/Jazz2/Cache/" @ce
GameCube | @cpp "carda:/Jazz2/Content/" @ce | @cpp "carda:/Jazz2/Cache/" @ce
PlayStation Portable | @cpp "ms0:/PSP/GAME/Jazz2/Content/" @ce | @cpp "ms0:/PSP/GAME/Jazz2/Cache/" @ce
PlayStation 2 | @cpp "cdfs:/Content/" @ce @m_span{m-text m-dim} (inside the disc image) @m_endspan | @cpp "cdfs:/Cache/" @ce --- read-only, and nothing is staged there
PlayStation 3 | @cpp "/app_home/Content/" @ce @m_span{m-text m-dim} (next to the `EBOOT.BIN`, see below) @m_endspan | @cpp "/dev_hdd0/game/JAZZ20000/USRDIR/Cache/" @ce --- writable
PS Vita | @cpp "app0:/Content/" @ce @m_span{m-text m-dim} (inside the VPK) @m_endspan | @cpp "ux0:/data/jazz2/Cache/" @ce
Switch | @cpp "romfs:/" @ce @m_span{m-text m-dim} (embedded in the `.nro`) @m_endspan | @cpp "sdmc:/Games/Jazz2/Cache/" @ce

@m_class{m-noindent}

@cpp "/app_home" @ce 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 @cpp "Prebaked.pak" @ce, where a cache the game converted itself keeps
@cpp "Source.pak" @ce. That package also holds the game's own @cpp "Animations" @ce and
@cpp "Metadata" @ce 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
@m_span{m-text m-dim} (the Wii, the PSP, the PS3, the Vita and the Switch) @m_endspan 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.

@m_class{m-block m-warning}

@par The tool and the game must come from the same sources
The converted data carries a cache format version, and a prepared tree is never rebuilt. A tree
produced by an older AssetPacker is not rejected with a message --- the game simply fails to load
what changed, which looks like missing text, missing tilesets or a level that will not start. After
pulling changes that touch the data formats, regenerate the tree with a freshly built tool.

@subsection consoles-common-features 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 @m_span{m-text m-dim} (`NCINE_WITH_THREADS`) @m_endspan | Yes | Yes | Yes | **No** | **No** | **No** | Yes | Yes
Asynchronous tracing @m_span{m-text m-dim} (`DEATH_TRACE_ASYNC`) @m_endspan | No | No | No | No | No | No | No | No
Audio backend | `AICA` @m_span{m-text m-dim} (KallistiOS sound driver) @m_endspan | `ASND` @m_span{m-text m-dim} (libogc DSP mixer) @m_endspan | `ASND` @m_span{m-text m-dim} (libogc DSP mixer) @m_endspan | `OpenAL` @m_span{m-text m-dim} (pspdev's OpenAL Soft) @m_endspan | **none yet** @m_span{m-text m-dim} (audsrv is unwritten) @m_endspan | `PS3` @m_span{m-text m-dim} (software mixer over libaudio) @m_endspan | `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 @m_span{m-text m-dim} (**libopenmpt**) @m_endspan | Yes | Yes | Yes | **Off on purpose** @m_span{m-text m-dim} (see @ref consoles-psp-limits) @m_endspan | **Off on purpose** @m_span{m-text m-dim} (see @ref consoles-ps2-limits) @m_endspan | Yes @m_span{m-text m-dim} (built from source, single-threaded) @m_endspan | 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

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

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

Backend | `WITH_*` | What it drives
------- | -------- | --------------
@ref nCine::ALAudioDevice "AL" | `WITH_OPENAL` | The system OpenAL on desktop, and the SDK's own on the PSP, Switch and Vita
@ref nCine::AsndAudioDevice "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
@ref nCine::AicaAudioDevice "AICA" | `WITH_AICA` | The Dreamcast sound processor through KallistiOS, using its wavetable channels for sound effects and its `snd_stream` driver for music
@ref nCine::Ps3AudioDevice "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 @cpp ON @ce everywhere and turns the whole subsystem
off when cleared.

@m_class{m-block m-info}

@par Why the Dreamcast does not use OpenAL
There is an OpenAL implementation for KallistiOS in kos-ports @m_span{m-text m-dim} (ALdc)
@m_endspan and it was tried first, but it is not usable as it stands: its startup sequence unlocks a
KallistiOS mutex nobody holds and panics the kernel, @cpp "alSourcei(AL_BUFFER, 0)" @ce queues an
internal null buffer instead of detaching, @cpp "alSourcef(AL_GAIN)" @ce is stored but never
applied so no volume setting has any effect, and its mixing thread sleeps ten milliseconds per live
source while holding the context lock. Talking to the AICA through KallistiOS directly is both less
code on our side and considerably better behaved.

@m_class{m-block m-info}

@par Why the PlayStation 3 mixes in software
The other consoles all have something to hand a sound to: the Dreamcast has 64 wavetable channels, the
Wii and GameCube have libogc's DSP mixer, the PSP has an OpenAL. lv2 gives a title one output ring and
nothing else --- no voices, no per-source volume or pitch register --- so resampling, panning, looping
and the streaming queues are all @ref nCine::Ps3AudioDevice on the PPE. Two consequences are worth
knowing when reading it: the ring is topped up from `updatePlayers()` once per frame rather than from
a callback @m_span{m-text m-dim} (PSL1GHT ships no threads, so there is no mixer thread to run one)
@m_endspan, which is why it is deliberately long enough to survive a late frame, and the mix is **clamped**
before it is written, because the hardware folds an out-of-range sample into the opposite
polarity, which sounds far worse than clipping.

@m_class{m-block m-info}

@par Sample byte order
The buffer formats of @ref nCine::IAudioDevice are **native-endian**, which is what the module
decoder produces. The WAVE and Vorbis readers therefore byte-swap 16-bit samples on a big-endian
target @m_span{m-text m-dim} (Vorbis for free, by asking `ov_read()` for big-endian packing)
@m_endspan --- without that, every `.wav` sound effect would come out as noise on the two PowerPC
consoles.

@subsection consoles-common-logging Getting a log out of a console

Every console writes the same @ref Asserts.h "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 @m_span{m-text m-dim} (visible on screen) @m_endspan, 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 @m_span{m-text m-dim} (memory-card slot B) @m_endspan, 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** @m_span{m-text m-dim} (`0x1000F180`) @m_endspan --- 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 @m_span{m-text m-dim} (`psp2shell`, VitaCompanion) @m_endspan
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

@m_class{m-noindent}

`DEATH_TRACE_LOG_PATH` additionally forces the trace into a file, which is worth using on the
platforms with writable storage @m_span{m-text m-dim} (not the Dreamcast, whose only medium is a
read-only disc) @m_endspan. 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.

- - -

@section consoles-dreamcast 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
@m_span{m-text m-dim} (@cpp "Sources/nCine/Graphics/RHI/PVR" @ce) @m_endspan on top of **KallistiOS**,
presented by the `Dc` window backend in 640x480 RGB565.

@subsection consoles-dreamcast-toolchain Toolchain

KallistiOS is built from source together with its `sh-elf` cross-compiler, there is no prebuilt
package.

@code{.sh}
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
@endcode

@m_class{m-noindent}

Edit @cpp "utils/kos-chain/Makefile.cfg" @ce first if the compilers should not be installed into the
default prefix, and @cpp "environ.sh" @ce afterwards so `KOS_BASE`, `KOS_CC_BASE` and `KOS_PORTS`
point at the tree that was just built.

@m_class{m-block m-warning}

@par environ.sh has to be sourced before CMake
The KOS toolchain file resolves the compiler, the linker script and the ports through those
environment variables and fails without them, so `source environ.sh` is part of every build --- and
of every terminal a build is started from. `$KOS_CMAKE_TOOLCHAIN` then points at the toolchain file,
and the `kos-cmake` wrapper in @cpp "utils/build_wrappers" @ce passes it automatically. Use
`kallistios.toolchain.cmake`, the `dreamcast.toolchain.cmake` next to it is a deprecated alias that
only prints a warning and includes it.

@m_class{m-noindent}

To get a bootable disc image, **mkdcdisc** has to be on `PATH` as well:

@code{.sh}
git clone https://gitlab.com/simulant/mkdcdisc.git
cd mkdcdisc
meson setup build && ninja -C build
sudo cp ./build/mkdcdisc /usr/local/bin/
@endcode

@subsection consoles-dreamcast-build Building

@code{.sh}
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/
@endcode

@m_class{m-noindent}

It leaves `NCINE_CONTENT_DIR` at its default, which is the repository's own
@cpp "Content" @ce --- 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 @cpp "build/dreamcast/jazz2.elf" @ce and, if `mkdcdisc` was found, the bootable
@cpp "build/dreamcast/jazz2.cdi" @ce with the content tree included as @cpp "/cd/Content" @ce.

@m_class{m-block m-danger}

@par Install mkdcdisc before configuring
The disc image step is added only when `find_program(mkdcdisc)` succeeds **at configure time**, and
the result is cached. Installing it afterwards changes nothing until the cache entry is dropped
@m_span{m-text m-dim} (re-run CMake with `-U MKDCDISC_EXECUTABLE`, or delete the build directory)
@m_endspan. A configuration without it prints `mkdcdisc not found, bootable CDI image will not be
created` and then builds only the ELF.

@subsection consoles-dreamcast-deploy Deploying and running

-   **Emulator** --- @cpp "jazz2.cdi" @ce 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 @cpp "jazz2.elf" @ce for a fast
    edit-run cycle, but the game reads its content from @cpp "/cd/Content/" @ce --- so a disc with
    the content still has to be present.

To read the log in **Flycast**, set `Debug.SerialConsoleEnabled = yes` in
@cpp "flycast/emu.cfg" @ce 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.

@subsection consoles-dreamcast-limits 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
    @m_span{m-text m-dim} (bursts are coarsened rather than truncated, so an explosion still looks
    like one) @m_endspan, 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
    @ref nCine::AicaAudioDevice. A sound effect is one AICA wavetable channel
    @m_span{m-text m-dim} (two for a stereo sample, since a channel is mono --- the sample is
    de-interleaved into one block per channel when it is uploaded) @m_endspan, 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 @m_span{m-text m-dim} (about 3 seconds of 22 kHz audio) @m_endspan.
    Rather than cutting one off, @ref 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 @m_span{m-text m-dim} (ARGB4444, or palettized with the hardware's palette
    banks) @m_endspan, the PVR has no hardware scissor @m_span{m-text m-dim} (clipping is
    geometric) @m_endspan, 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.

- - -

@section consoles-wii Nintendo Wii

A 729 MHz PowerPC @m_span{m-text m-dim} (Broadway) @m_endspan 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 @m_span{m-text m-dim} (@cpp "Sources/nCine/Graphics/RHI/GX" @ce)
@m_endspan on **libogc**, presented by the `Ogc` window backend, which adopts whatever video mode
the console prefers @m_span{m-text m-dim} (PAL 50 Hz or NTSC/EDTV 60 Hz, interlaced or
progressive) @m_endspan.

@subsection consoles-wii-toolchain Toolchain

@code{.sh}
# Install devkitPro's pacman, then the Wii group and zlib
sudo dkp-pacman -S --needed wii-dev ppc-zlib

export DEVKITPRO=/opt/devkitpro
@endcode

@m_class{m-noindent}

`ppc-zlib` is **not** part of the `wii-dev` group but is required for the compressed game content.
The toolchain file is @cpp "$DEVKITPRO/cmake/Wii.cmake" @ce.

@subsection consoles-wii-build Building

@code{.sh}
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/
@endcode

@m_class{m-noindent}

Beside @cpp "build/wii/jazz2.dol" @ce the build stages the SD card layout the Homebrew
Channel expects, so its contents can be copied to a card as they are:

@code{.shell-session}
build/wii/sd/apps/Jazz2/boot.dol
build/wii/sd/apps/Jazz2/Content/…
@endcode

@subsection consoles-wii-deploy Deploying and running

-   **Real hardware** --- copy the contents of @cpp "build/wii/sd/" @ce to the root of an SD card
    or USB storage device and launch *Jazz² Resurrection* from the **Homebrew Channel**. An
    @cpp "apps/Jazz2/meta.xml" @ce 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:

@code{.sh}
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
@endcode

@m_class{m-block m-warning}

@par The Wii build needs an SD card even in an emulator
Without one the game stops at *Cannot access the SD card* --- there is nowhere to read the content
from. Dolphin can also synchronise a host directory instead of an image
@m_span{m-text m-dim} (`WiiSDCardEnableFolderSync`) @m_endspan, which is more convenient while
iterating.

@m_class{m-noindent}

For the log, set a **USB Gecko** in memory-card slot B @m_span{m-text m-dim} (`SlotB = 7` in
`Dolphin.ini`) @m_endspan 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
@cpp "GFX.ini" @ce writes every displayed frame as a PNG.

@subsection consoles-wii-limits 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
    @cpp AsndAudioDevice::computeVolume() @ce.
-   ASND holds only two buffers per voice @m_span{m-text m-dim} (one playing, one queued) @m_endspan
    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
    @ref 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 @m_span{m-text m-dim} (including CI8 with a hardware
    TLUT for palettized ones) @m_endspan 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.

- - -

@section consoles-gamecube 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.

@subsection consoles-gamecube-toolchain Toolchain

@code{.sh}
sudo dkp-pacman -S --needed gamecube-dev ppc-zlib

export DEVKITPRO=/opt/devkitpro
@endcode

@m_class{m-noindent}

As on the Wii, `ppc-zlib` is not part of the console's package group. The toolchain file is
@cpp "$DEVKITPRO/cmake/GameCube.cmake" @ce.

@subsection consoles-gamecube-build Building

@code{.sh}
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/
@endcode

@m_class{m-noindent}

The staged layout differs from the Wii's, because the GameCube has no Homebrew Channel
and reads the card directly:

@code{.shell-session}
build/gamecube/sd/Jazz2/Jazz2.dol
build/gamecube/sd/Jazz2/Content/…
@endcode

@subsection consoles-gamecube-deploy Deploying and running

-   **Real hardware** --- copy the contents of @cpp "build/gamecube/sd/" @ce to an SD card in an **SD
    Gecko** @m_span{m-text m-dim} (memory-card slot A) @m_endspan or an **SD2SP2** adapter, and
    start @cpp "Jazz2/Jazz2.dol" @ce from **Swiss**. The game reads its content from
    @cpp "carda:/Jazz2/Content/" @ce, 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.

@subsection consoles-gamecube-limits Limits and known issues

Everything in @ref consoles-wii-limits 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
    @m_span{m-text m-dim} (the Wii build additionally links `wiiuse` and `bte` for Wii Remotes)
    @m_endspan.

- - -

@section consoles-psp PlayStation Portable

A 222 MHz @m_span{m-text m-dim} (raised to 333 MHz at startup) @m_endspan MIPS Allegrex with 32 MB
of memory @m_span{m-text m-dim} (24 MB usable, 64 MB on the 2000/3000 models) @m_endspan and a
Graphics Engine driven through `sceGu`. The port renders through the `GU` backend
@m_span{m-text m-dim} (@cpp "Sources/nCine/Graphics/RHI/GU" @ce) @m_endspan, presented by the `Psp`
window backend in the console's native 480x272.

@subsection consoles-psp-toolchain 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
@m_span{m-text m-dim} (which is what the CI workflow uses) @m_endspan:

@code{.sh}
# 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
@endcode

@m_class{m-noindent}

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 @cpp "$PSPDEV/psp/share/pspdev.cmake" @ce, 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.

@subsection consoles-psp-build Building

@code{.sh}
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/
@endcode

@m_class{m-noindent}

`Release` compiles with `-O2` for the same reasons as the other consoles --- the
Allegrex FPU is single-precision only.

The build produces @cpp "build/psp/jazz2.elf" @ce and stages the complete memory-stick layout under
@cpp "build/psp/ms0/" @ce:

@code{.shell-session}
build/psp/ms0/PSP/GAME/Jazz2/EBOOT.PBP
build/psp/ms0/PSP/GAME/Jazz2/Content/…
@endcode

@m_class{m-noindent}

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.

@subsection consoles-psp-deploy Deploying and running

-   **Real hardware** --- copy @cpp "build/psp/ms0/PSP/GAME/Jazz2" @ce 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.

@m_class{m-block m-warning}

@par PPSSPP boots from its own memory stick, not from the path passed to it
Handing PPSSPP a path outside its memory-stick directory can silently run a **stale** copy of the
game. Copy the staged tree into the emulator's memory stick every time --- for the flatpak that is
@cpp "~/.var/app/org.ppsspp.PPSSPP/config/ppsspp/PSP/GAME/Jazz2/" @ce, which the build directory is
not writable from. The emulator's `--log=FILE` option does not work either, capture its stdout
instead, where the game's trace appears prefixed with `I[PRINTF]: stdout:`.

@m_class{m-noindent}

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.

@subsection consoles-psp-limits 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
    @m_span{m-text m-dim} (every later load takes ~1.8 s) @m_endspan --- 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 @m_span{m-text m-dim} (including local splitscreen) @m_endspan
    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.

- - -

@section consoles-ps2 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 @m_span{m-text m-dim} (@cpp "Sources/nCine/Graphics/RHI/GS" @ce) @m_endspan,
presented by the `Ps2` window backend in NTSC 640x448.

@subsection consoles-ps2-toolchain 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:

@code{.docker}
FROM docker.io/ps2dev/ps2dev:latest
RUN apk add --no-cache cmake make git python3 bash xorriso
@endcode

@m_class{m-block m-warning}

@par $PS2DEV/ee/bin is not on PATH
Not even under a login shell, so CMake fails its compiler check with
"mips64r5900el-ps2-elf-g++ is not a full path and was not found". Export
`PATH=$PS2DEV/ee/bin:$PS2DEV/bin:$PS2SDK/bin:$PATH` before configuring. `xorriso` is what authors the
disc image in the packaging step, hence the `apk add` above.

@subsection consoles-ps2-build Building

@code{.sh}
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)
@endcode

@m_class{m-noindent}

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`
@m_span{m-text m-dim} (`BOOT2 = cdrom0:\JAZZ2.ELF;1`, `VMODE = NTSC`) @m_endspan, 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.

@subsection consoles-ps2-deploy 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:

@code{.sh}
pcsx2-qt -batch -nogui -earlyconsolelog -logfile run.log jazz2.iso
@endcode

@m_class{m-noindent}

A bare ELF has no connected stdout, so the trace goes to the Emotion Engine's **SIO transmit
register** @m_span{m-text m-dim} (`0x1000F180`) @m_endspan 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 @cpp MainApplication::Run() @ce emits, before the trace system
exists.

@subsection consoles-ps2-limits 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
    @m_span{m-text m-dim} (~856 KB measured) @m_endspan 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 @m_span{m-text m-dim} (libopenmpt's
    `to_chars(char*, char*, const double&)` is ambiguous against PS2SDK's newlib) @m_endspan.
-   **Threads are off**, so multiplayer @m_span{m-text m-dim} (including local splitscreen) @m_endspan
    is compiled out and asset loading is synchronous.
-   Two pipeline stages are still unimplemented: the weapon wheel's line strip
    @m_span{m-text m-dim} (`line_strip_mesh`) @m_endspan and the CPU-lightmap compositor
    @m_span{m-text m-dim} (`lighting_combine`, so a level renders unlit) @m_endspan.
-   `Jazz2.config` cannot be written --- a disc is read-only and the PS2 has no working directory, so
    preferences need a memory-card path @m_span{m-text m-dim} (`mc0:/`) @m_endspan.
-   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
    @m_span{m-text m-dim} (which the GS reaches through a white-RGB CLUT --- see
    @cpp GsDevice::AcquireCoverageClutForOffset() @ce) @m_endspan and the
    `MODULATE_X2`/`MODULATE_X4` presets are rejected for this target.

- - -

@section consoles-ps3 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
@m_span{m-text m-dim} (@cpp "Sources/nCine/Graphics/RHI/RSX" @ce) @m_endspan, 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.

@subsection consoles-ps3-toolchain 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 @m_span{m-text m-dim} (`fself`, `make_self_npdrm`, `sfo`,
    `pkg`, `sprxlinker`, and `cgcomp`) @m_endspan. 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
    @ref consoles-ps3-shaders.

@code{.sh}
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
@endcode

@m_class{m-block m-warning}

@par ppu-gcc reads $PSL1GHT itself
The compiler driver fails outright with "environment variable 'PSL1GHT' not defined" if it is unset,
so it has to be in the *environment* of every compile and not merely a CMake variable. The toolchain
file exports both it and `PS3DEV` for the configure step and CMake's own compiler probes.

@m_class{m-block m-info}

@par Step 9 is worth running even though it is slow
It builds the portlibs, of which **zlib** is a hard requirement --- without it
@ref Death::IO::Compression::DeflateStream "DeflateStream" has no implementation and the game's
compatibility layer does not link. It also brings **libogg** and **libvorbis**. It needs `libtool` on
the host, which is the one dependency ps3toolchain does not check for.

@subsection consoles-ps3-build Building

@code{.sh}
./BuildPs3.sh
@endcode

@m_class{m-noindent}

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:

@code{.sh}
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)
@endcode

@m_class{m-block m-info}

@par The toolchain file lives with the project
Every other console points `CMAKE_TOOLCHAIN_FILE` at a file inside its SDK. PSL1GHT is purely
Makefile-based @m_span{m-text m-dim} (`$PSL1GHT/ppu_rules`) @m_endspan and ships no CMake support at
all, so @cpp "cmake/toolchains/ps3dev.cmake" @ce is the project's own. It reproduces what those rules
set up: the cross compilers, the PSL1GHT include and library paths, and the machine flags the SDK
builds its own libraries with.

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
@m_span{m-text m-dim} (`JAZZ20000`) @m_endspan. 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 @m_span{m-text m-dim} (see @ref consoles-vita) @m_endspan. 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.

@subsection consoles-ps3-shaders 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:

@code{.sh}
PS3DEV=<prefix> LD_LIBRARY_PATH=<Cg toolkit>/usr/lib64 \
    ./build/Sources/Utilities/ShaderCompiler/ShaderCompiler --generate-all
@endcode

@m_class{m-noindent}

It is a mode of @ref shader-compiler "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 @cpp "skipped: RsxGeneratedShaders.h" @ce 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
@ref consoles-ps3-limits for which ones those currently are.

@subsection consoles-ps3-deploy Deploying and running

Install `jazz2.pkg` on a console with custom firmware, or run the staged tree directly:

@code{.sh}
rpcs3 --no-gui build/ps3/pkg/USRDIR/EBOOT.BIN
@endcode

@m_class{m-noindent}

RPCS3 needs the PlayStation 3 firmware installed once @m_span{m-text m-dim} (**File &rarr; Install
Firmware**, or `--installfw` with the `PS3UPDAT.PUP` Sony publishes) @m_endspan, 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.

@m_class{m-block m-warning}

@par Headless runs are not representative
`--headless` suppresses the window, and with it the flip timing the emulator's RSX thread paces
itself against. Its FIFO watchdog then fires after a while with "Dead FIFO commands queue state has
been detected" on a title that is behaving perfectly. It is still the convenient way to capture a
log, just do not read that particular message as a fault in the game. A host with no GPU
@m_span{m-text m-dim} (software Vulkan through **llvmpipe**) @m_endspan or one under memory pressure
provokes the same thing.

@subsection consoles-ps3-limits 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
    @m_span{m-text m-dim} (`sysThreadCreate`, `sys/mutex.h`, `sys/cond.h`) @m_endspan, which would be
    a new backend in @cpp "Thread.cpp" @ce rather than a configuration change. Multiplayer
    @m_span{m-text m-dim} (including local splitscreen) @m_endspan 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 @m_span{m-text m-dim} > @m_endspan Graphics @m_span{m-text m-dim} > @m_endspan
    Rescale Mode* and skips compiling them at all; a value stored by another backend needs no special
    handling, because @cpp UpscaleRenderPass @ce 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
    @cpp RenderResources::GetBatchedShader() @ce 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
    @m_span{m-text m-dim} (`Src check failed. Aborting`) @m_endspan 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 @m_span{m-text m-dim} (ShaderCompiler's `RsxMaxBatchSize`)
    @m_endspan, @cpp RsxDevice::MaxBatchSize @ce sizes the batched corner stream that supplies the
    instance index, and the backend publishes it as @cpp IRhiCapabilities::IntValues::MaxBatchSize @ce
    so that both the shader compilation paths and @cpp RenderBatcher @ce 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
    @m_span{m-text m-dim} (@cpp RsxDevice::FindVertexAttribute() @ce) @m_endspan. 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 @m_span{m-text m-dim} (no FIFO desync, no aborted
    vertex programs) @m_endspan --- but the port has not had the testing on real hardware that the
    older consoles have.

@m_class{m-block m-info}

@par What the toolchain's libstdc++ is missing
`powerpc64-ps3-elf` was configured with `_GLIBCXX_USE_C99_MATH_TR1` undefined, so `<cmath>` declares
none of the C99 maths functions in `std::` even though newlib provides them globally. Defining that
macro is not the fix --- it also drags in the `long double` variants this newlib does not have, and
`<cmath>` then fails to compile at all. @cpp "cmake/toolchains/ps3-libstdc++-c99.h" @ce pulls in the
names one by one and is **force-included into every translation unit** by the toolchain file, so that
third-party sources the build only fetches are covered too. It is a property of the compiler that was
installed rather than of anything in this project, which is why it lives with the toolchain.
@par
The same libstdc++ was also built without gthread support, so it has no `std::mutex` --- which is what
kept **libopenmpt**, and therefore the soundtrack, off this console for a while. The library has a
single-threaded mode that never includes `<mutex>`, but `mpt/base/detect_quirks.hpp` assigns
`MPT_PLATFORM_MULTITHREADED` unconditionally before clearing it again for a hardcoded list of
platforms @m_span{m-text m-dim} (DJGPP, Emscripten without pthreads) @m_endspan, so no `-D` can reach
it. @cpp "cmake/Findlibopenmpt.cmake" @ce adds a block of the same shape to the fetched copy and
selects it, which is why the PS3 sets `NCINE_COMPILE_OPENMPT` --- ps3dev packages no libopenmpt, and a
console has no dynamic loader for the runtime fallback the desktop platforms use.

- - -

@section consoles-vita 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` @m_span{m-text m-dim} (the default) @m_endspan | **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

@subsection consoles-vita-toolchain Toolchain

Install **VitaSDK** with `vdpm` @m_span{m-text m-dim} (see [vitasdk.org](https://vitasdk.org/)) @m_endspan
and export `VITASDK` environment variable, then use @cpp "$VITASDK/share/vita.toolchain.cmake" @ce.

@subsection consoles-vita-build Building

@code{.sh}
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/
@endcode

@m_class{m-noindent}

The native `GXM` backend is what this builds, add @cpp "-D NCINE_PREFERRED_RHI=OpenGL" @ce for the
vitaGL one instead. Either way the console needs @cpp "libshacccg.suprx" @ce, see
@ref consoles-vita-shaders. The result is @cpp "build/vita/jazz2.vpk" @ce.

@subsection consoles-vita-deploy Deploying and running

Install the VPK with **VitaShell** @m_span{m-text m-dim} (or over FTP) @m_endspan. It carries the
@cpp "Content" @ce directory with it, which the firmware unpacks into the application's own read-only
directory @m_span{m-text m-dim} (@cpp "ux0:/app/jazz20000/" @ce, mounted as @cpp "app0:" @ce)
@m_endspan, 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 @cpp "ux0:/data/jazz2/Source/" @ce and let it build
@cpp "ux0:/data/jazz2/Cache/" @ce --- or prepare the tree with AssetPacker anyway to skip the wait.
Both of those live on @cpp "ux0:" @ce because @cpp "app0:" @ce cannot be written to, reinstalling the
VPK replaces the content but leaves the cache, the converted source data and the settings alone.

@subsection consoles-vita-shaders 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 @cpp "libshacccg.suprx" @ce, part of the
console's own firmware. Extract it with
[VitaShaRK's instructions](https://github.com/Rinnegatamante/vitaShaRK) and place it in
@cpp "ur0:/data/" @ce.

-   `GXM` consumes GXP binaries, so it ships its shaders as **Cg source** @m_span{m-text m-dim} (the
    generated @cpp "CgGeneratedShaders.h" @ce, see @ref shader-compiler-cg) @m_endspan 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 @cpp glCompileShader() @ce, and vitaGL compiles it through the very same
    SceShaccCg. That is also what lets external @cpp ".shader" @ce files and the rescale filters work
    on this platform, unlike the fixed-function consoles above.

@subsection consoles-vita-limits Limits and known issues

-   The ES 2.0 profile has no uniform buffer objects and no @cpp gl_VertexID @ce, so its shaders come
    from the **ESSL 100** lowering that ShaderCompiler bakes into the same generated headers, see
    @ref shader-compiler-platforms. 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
    @m_span{m-text m-dim} (a power-of-two, tiled colour-surface texture whose control words read back
    with @cpp SCE_GXM_TEXTURE_ADDR_REPEAT @ce on both axes) @m_endspan 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 @m_span{m-text m-dim} (it needs
    @cpp "sys/ioctl.h" @ce, which the SDK does not provide) @m_endspan, 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.

- - -

@section consoles-switch Nintendo Switch

Also a shader platform, built with **devkitA64** and using **SDL2** and desktop OpenGL. Install the
`switch-dev` group and use @cpp "$DEVKITPRO/cmake/Switch.cmake" @ce:

@code{.sh}
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/
@endcode

@m_class{m-noindent}

`DEATH_TRACE_ASYNC` has to be off --- it crashes on startup on this platform. The build produces
@cpp "build/switch/jazz2.nro" @ce with the shipped game content **embedded in its RomFS**, so the
application is self-contained: copy the `.nro` to @cpp "sd:/switch/" @ce and start it from `hbmenu`.
Like the Vita, the Switch converts the original game data itself --- put an original installation
into @cpp "sdmc:/Games/Jazz2/Source/" @ce and the converted cache and save data appear next to it in
@cpp "sdmc:/Games/Jazz2/" @ce.

- - -

@section consoles-libretro 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**
@m_span{m-text m-dim} (@cpp "jazz2_libretro.so" @ce, sources in
@cpp "Sources/nCine/Backends/Libretro" @ce) @m_endspan that RetroArch drives, with no window backend
of its own. `NCINE_PREFERRED_RHI` accepts `Software` @m_span{m-text m-dim} (the CPU rasterizer's
framebuffer is handed to `retro_video_refresh`, which works on every frontend) @m_endspan or
`OpenGL` @m_span{m-text m-dim} (rendering into the frontend's FBO through `SET_HW_RENDER`, targeting
OpenGL|ES 3.0 as the common denominator of RetroArch's GPU platforms) @m_endspan.

@code{.sh}
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/
@endcode

- - -

@section consoles-ci Continuous integration

Every console is built on each push by its own workflow in @cpp ".github/workflows" @ce, 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` @m_span{m-text m-dim} (compilers only --- KOS, kos-ports and mkdcdisc are built in the job and cached) @m_endspan | `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` @m_span{m-text m-dim} (the bare ELF because PCSX2 boots one with `-elf`) @m_endspan
`vita.yml` | `vitasdk/vitasdk` | `Jazz2.vpk`
`switch.yml` | `devkitpro/devkita64` | `Jazz2.nro`

@m_class{m-noindent}

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, @ref consoles-ps3-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 @cpp "Content" @ce
@m_span{m-text m-dim} (they cannot ship converted original game data) @m_endspan, so their artifacts
boot into the main menu and need a content tree prepared as described in
@ref consoles-common-content to actually play a level.

- - -

@section consoles-troubleshooting 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
    @cpp "Prebaked.pak" @ce and no original files to convert, so this is what a missing or misplaced
    @cpp "Content" @ce directory looks like, check the path for the console in
    @ref consoles-common-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 @ref consoles-dreamcast-build.
-   **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 @ref consoles-dreamcast-limits.
-   **PPSSPP runs an old build.** It boots from its own configured memory stick, see
    @ref consoles-psp-deploy.
-   **CMake cannot find the compiler on the Dreamcast.** @cpp "environ.sh" @ce was not sourced in
    that shell, see @ref consoles-dreamcast-toolchain.
-   **A fully loaded sound is cut short on the Dreamcast.** An AICA channel addresses 65534 samples
    at most, see @ref consoles-dreamcast-limits.
-   **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
    @ref consoles-common-audio.
-   **"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
    @ref consoles-ps3-deploy. 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 @ref consoles-ps3-limits.
-   **`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
    @ref consoles-ps3-toolchain.
-   **The PlayStation 3 build does not link**, with `DeflateStream` unresolved. The portlibs were not
    built --- zlib is a hard requirement, see @ref consoles-ps3-toolchain.

*/

}
