LLVM/clang cross-toolchain layout, wrappers and build scripts for PS2 EE and IOP
  • Shell 83.4%
  • C 15.6%
  • Dockerfile 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tech 6daa288f64
All checks were successful
Build / build-native-linux (push) Successful in 2m8s
Release / build-native-linux (push) Successful in 3m47s
Release / package-linux (push) Successful in 1m45s
Build / build-windows-cross (push) Successful in 2m10s
Release / build-windows-cross (push) Successful in 1m40s
Release / package-windows (push) Successful in 2m16s
Build the EE with the mips64r5900el-ps2-elf triple
PS2-194. The EE wrapper config and the libc++, emutls and compiler-rt builds use the PS2 triple, which implies the R5900 and n32 so lld can link -flto bitcode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 22:29:54 +00:00
.forgejo/workflows release.yml: build on the llvm-linux runner, as build.yml does 2026-10-11 10:30:57 -05:00
docker Make build.sh, the step scripts and the helper scripts executable 2026-10-04 13:51:14 -05:00
lib fetch_source: show why rev-parse failed on an existing clone 2026-10-05 13:45:02 -05:00
scripts Build the EE with the mips64r5900el-ps2-elf triple 2026-10-15 22:29:54 +00:00
src/iopmini Add llvm-toolchain: README, libc++ build script, eh stubs, wrappers 2026-10-03 09:09:36 -05:00
steps Build the EE with the mips64r5900el-ps2-elf triple 2026-10-15 22:29:54 +00:00
wrappers Build the EE with the mips64r5900el-ps2-elf triple 2026-10-15 22:29:54 +00:00
.gitignore Add the Linux build pipeline and CI workflows 2026-10-03 10:29:08 -05:00
build.sh Make build.sh, the step scripts and the helper scripts executable 2026-10-04 13:51:14 -05:00
config.sh Add the Linux build pipeline and CI workflows 2026-10-03 10:29:08 -05:00
README.md Support thread_local on the EE with emulated TLS 2026-10-11 15:46:18 +00:00

llvm-toolchain

⚠️ Work in progress. This is part of the experimental LLVM-based PS2 toolchain. It changes often, has gaps, and is not ready for production use. ⚠️

Build scripts and CI for the LLVM-based PS2 toolchain (EE and IOP): clang and lld from the ps2/llvm fork (r5900-ee), LLVM binutils replacements, compiler-rt builtins, newlib from ps2/newlib-llvm, pthread-embedded, libc++, libc++abi and libatomic.

Building

PS2DEV=/opt/ps2dev-llvm ./build.sh          # everything
PS2DEV=/opt/ps2dev-llvm ./build.sh 3 4      # only steps 3 and 4

Steps (steps/): 1 LLVM, 2 per-target wrappers, 3 compiler-rt, 4 newlib, 5 newlib-nano, 6 pthread-embedded, 7 libc++/libc++abi/libatomic. newlib-nano installs libc_nano.a, libm_nano.a and libg_nano.a beside the full libraries; link with -lc_nano -lm_nano to use them. Source pins are in config.sh. Set PS2_CROSS_HOST=x86_64-w64-mingw32 to build the Windows-hosted compiler from Linux (steps 1 and 2 only; the target libraries come from a native build).

The install is relocatable: llvm/bin holds the compiler and tools, ee/bin and iop/bin hold the target-prefixed wrappers (mips64r5900el-ps2-elf-clang, -ar, ...) with their .cfg files, and ee/mips64r5900el-ps2-elf, iop/mipsel-none-elf are the sysroots.

scripts/sanity-check.sh <install-dir> compiles small EE and IOP programs against the install.

Local Linux run

docker/run-local.sh <out-dir> [steps] runs the build in the CI image with sources and build trees in a Docker volume and the install bind-mounted to <out-dir>. It needs LLVM_SRC_DIR and NEWLIB_SRC_DIR pointing at local clones of ps2/llvm and ps2/newlib-llvm. The container is limited to 12 GB and LLVM_COMPILE_JOBS (default 6): more parallel clang compiles exhaust the Docker VM.

CI

.forgejo/workflows/build.yml builds and sanity-checks on every push. release.yml runs on v* tags: native Linux build, mingw cross build for Windows, then publishes both zips to the release.


Self-contained LLVM/clang cross-toolchain for PS2 homebrew (EE and IOP), built from the LLVM fork at llvm/llvm-project (patch: llvm/patches/r5900-vu0-vux-phase2-*.patch). Experimental, parallel to the GCC-based toolchain/ - nothing here depends on toolchain/ staying in place, and nothing in toolchain/ depends on this. No GCC component is used anywhere in this toolchain or in anything built with it - verified end to end, see "Zero GCC, verified" below.

Layout

llvm-fa360c3b8dbf/    a real, versioned install (named after the LLVM fork's commit) - not a
                       live reference into the dev build tree. `bin/clang.exe`,
                       `lib/clang/24/include/` (resource headers). Produced by
                       `ninja install-clang install-clang-resource-headers` from
                       `llvm/build`, copied here. Re-run that and copy over again to pick up a
                       rebuilt compiler - see "Updating the compiler" below.

ee/
  bin/
    mips64r5900el-ps2-elf-clang.bat   wrapper: execs ../../llvm-fa360c3b8dbf/bin/clang.exe
                                       --config=<the .cfg below>
    mips64r5900el-ps2-elf-clang.cfg   default flags (target triple, ABI, -fno-pic, header/lib
                                       search paths, ...)
    mips64r5900el-ps2-elf-{ar,ld,objcopy,ranlib,nm,strip,objdump,...}.exe
                                       real GNU binutils (no llvm-ar/lld built yet - see gaps)
  mips64r5900el-ps2-elf/
    include/    newlib headers (clang-built, `newlib/build-clang-msys2-v2` install output)
    lib/        libc.a, libm.a, libg.a, libpthread.a, libclang_rt.builtins-mips64el.a (ALL
                clang-built - no libgcc.a here, see "Zero GCC" below), crt0.o (newlib's generic
                PS2 startup - most PS2 homebrew uses its own crt0 from sdk-core instead, kept
                for completeness)

iop/
  bin/
    mipsel-none-elf-clang.bat   wrapper: execs ../../llvm-fa360c3b8dbf/bin/clang.exe
                                --config=<the .cfg below>
    mipsel-none-elf-clang.cfg   default flags
    mipsel-none-elf-{ar,ld,objcopy,ranlib,nm,strip,objdump,...}.exe
  mipsel-none-elf/
    lib/        crt0.o (unused by real IRX modules - see note), crti/crtbegin/crtend/crtn.o
                (GCC's real default IOP link convention - the objects themselves are GCC's,
                see gaps below), libclang_rt.builtins-mipsel.a (clang-built), ldscripts/

No IOP include/ - the IOP target has no C library at all (matches the existing GCC IOP toolchain); sdk-core provides IOP module headers itself, per package, same as it always has.

Using it

Point CC at the relevant bin/*-clang.bat and treat it like a normal cross-compiler - it's a thin wrapper (a couple lines, %~dp0-relative) that runs the versioned clang.exe with its target's .cfg explicitly (--config=...), so mips64r5900el-ps2-elf-clang.bat foo.c -c just works without extra flags. Verified working invoked from PowerShell, Git-Bash, and the portable MSYS2 bash bundled with toolchain/ (real builds should use that MSYS2, not this machine's global make - see feedback_llvm_toolchain_gotchas memory for why). Link with the bin/*-ld.exe next to it and the libs under <target>/lib/. See scratch/llvm-makefile-test/Makefile for a complete, working example.

Why a wrapper script + a real versioned install, not a hardlink per target (two earlier iterations did that): a hardlinked binary copy is live - it drifts the moment llvm/build/bin/ clang.exe is rebuilt with no version attached to what's actually in there, and doesn't survive being zipped/copied elsewhere (a hardlink becomes an independent full copy on transfer). Real toolchains (Android NDK's target-triple wrapper scripts, the LLVM project's own releases) do a deliberate install step into an immutable, versioned directory instead, and point every wrapper at that fixed path. .bat wrappers still avoid duplicating the compiler per target (one binary, several thin scripts), same reasoning as before - just backed by a real install now.

C++: the install carries only clang.exe (the clang++, clang-cl, clang-cpp and clang-dxc names were identical 100 MB copies and are gone). Compile C++ by passing --driver-mode=g++ to the wrapper, for example mips64r5900el-ps2-elf-clang.bat --driver-mode=g++ -std=c++17 -c foo.cpp.

Updating the compiler: cd llvm/build && ninja install-clang install-clang-resource-headers installs to llvm/install/ by default; copy bin/clang.exe and lib/clang/ from there into a new (or, during active same-session iteration, the same) llvm-fa360c3b8dbf/-style directory here, then repoint the .bat/.cfg files if the directory name changed. A proper "cut a new version" step (new directory per real change, wrappers bumped explicitly) is the natural next refinement once this stops being continuous same-session iteration.

Real PS2 programs (both EE homebrew and IOP IRX modules) don't use the generic crt0/crti+ crtbegin+crtend+crtn startup convention above in practice - EE homebrew boots via EELOAD into sdk-core's own startup code, and IOP modules load via loadcore's IRX import/export table convention, not a normal ELF _start. Those objects are here for toolchain completeness (so a bare clang foo.c -o foo works, same as it does with the GCC toolchain) - see scratch/llvm-spinningcube/, scratch/llvm-iop-rpc/, and scratch/llvm-makefile-test/ for how real sdk-core-based programs are actually built and linked.

Zero GCC, verified

scratch/llvm-makefile-test/ is a real Makefile-driven project built entirely against this toolchain - not just main.c, but the TTY diagnostic path too (tty.c/deci2.c compiled fresh from sdk-core source, plus a hand-extracted single-syscall stub and a minimal DIntr/EIntr, not prebuilt GCC .o files or the full kernel.S/glue.c - see that Makefile's header comment for exactly why). Confirmed:

  • readelf -p .comment on the built ELF: clang version 24.0.0git (...) - nothing else.
  • nm on the built ELF: no gcc-named symbols anywhere.
  • mips64r5900el-ps2-elf-ld.exe --version: real GNU Binutils - a separate project from GCC, not a GCC collect2 wrapper.
  • Ran clean in PCSX2: real libc (strcpy/strlen/memcmp) and real libm (sqrtf) both verified working at runtime, not just linking cleanly.

The one library still built by GCC's own crt objects is IOP's crti/crtbegin/crtend/crtn.o (the objects are GCC's; nothing links against a GCC-built library there since IOP has no libc at all) - real IOP modules don't use that startup path anyway (see above), so it's untested whether a real IRX build needs anything from it.

Known gaps

  • No llvm-ar/llvm-ranlib/lld built - GNU binutils are vendored in instead. Fine for now, ELF tooling is toolchain-agnostic (a separate project from GCC either way).
  • newlib-nano and a real IOP IRX module build/boot test (as opposed to the EE-side verification above) are not done - this is still the experimental, parallel-track toolchain, not the default.
  • ps2build --llvm (on its own llvm branch) now drives a real, full ps2workspace.yaml build against this toolchain - see ps2build/PLAN.md for where that stands. Not merged to ps2build's default branch and not installed by ps2build update.

IOP CPU (fixed 2026-09-20): -mcpu=r3000a, not generic mips1

The IOP's real CPU (R3000A-based, also the original PlayStation's CPU) is now a named LLVM target CPU, r3000a, instead of the generic mips1 bucket used before. The IOP .cfg uses it in place of -mcpu=mips1.

Motivation: existing sdk-core/newlib IOP asm (sysclib's hand-optimized memset.S/memcpy.S/etc) uses cache/pref directly - real MIPS III/32-lineage instructions per the real GNU opcode table, not something GNU leniency invented; real GNU as -march=mips1 rejects them too. r3000a adds a narrow FeaturePS2Cache feature that accepts just those two mnemonics, without pulling in real FeatureMips3_32's actual MIPS III/32 codegen implications - the compiler's own code-generation baseline for ordinary C stays exactly as conservative as plain mips1. FeatureR5900 gets the same feature, which also closes the R5900 cache gap this file used to list under "Known gaps" - same root cause (INSN_MIPS3_32_NOT_32R6_64R6, upstream's CACHE/PREF gate, pulls in more than just mnemonic acceptance), same fix. Both byte-diff verified against real GNU as.

Gotcha: clang's driver validates -mcpu= against its own hardcoded CPU list (clang/lib/Basic/Targets/Mips.cpp's ValidCPUNames), separate from the LLVM backend's own TableGen-derived CPU list that llvm-mc already accepts new names from directly. Adding a backend CPU needs both lists updated, not just the backend - llvm-mc -mcpu=<name> working is not proof clang -mcpu=<name> will.

R5900 MMI instruction set (fixed 2026-09-20)

The gap flagged in earlier revisions of this section - upstream LLVM missing the full R5900 MMI (paired/packed 128-bit SIMD) instruction set - is closed. ~100 instructions added to MipsVU0InstrInfo.td: the full MMI group (pand/pnor/pcpyld/pcpyud/psubb/pxor/...), the pipeline-1 HI/LO instructions (mult1/div1/madd1/mfhi1/mflo1/mthi1/mtlo1/...), the SA-register instructions (mfsa/mtsa/mtsab/mtsah), and movn/movz (standard MIPS IV conditional move, never enabled for this subtarget before). Every encoding was mechanically derived from real GNU binutils' own opcode table (toolchain/build/ee/binutils-gdb/opcodes/mips-opc.c) rather than hand-transcribed from the EE manual, then spot-checked byte-identical against real mips64r5900el-ps2-elf-as across every distinct operand shape in the set (3-reg, 2-reg, 1-reg, shift-immediate, 16-bit-immediate). Confirmed against the real motivating case: sdk-core's kernel.S (the file that originally exposed this gap - its hand-optimized memcpy/memset/strlen/strncpy use MMI directly) now assembles those functions with zero MMI-related errors.

Two small, deliberate scope choices, not bugs: div1/divu1 accept div1 $rs, $rt here instead of gas's div1 $0, $rs, $rt (gas requires a literal $0 placeholder with no corresponding bit in the encoding; verified byte-identical machine code either way, just a different accepted syntax); mult1/multu1/madd1/maddu1 implement only the simpler 2-operand form that writes HI1/LO1 only, not the 3-operand form that also writes a GPR (mips-opc.c has both).

scratch/llvm-mmi-test/ is a real runtime correctness test, not just an assembly-level check: 22 individual PASS/FAIL cases covering every instruction family added (bitwise AND/OR/NOR/XOR, byte-lane add/sub with wraparound, signed saturation, conditional move both branches, the pipeline-1 multiply/HI1/LO1, the SA register, a shift-immediate, and the two real 128-bit-aware ops pcpyld/pcpyud via lq/sq), run on real PCSX2 EE emulation - 22/22 pass. Two of the expected values (pcpyud's hi/lo half-mapping direction, plzcw's leading-zero count) weren't safe to hand-derive from memory, so they were taken from PCSX2's own first run instead of guessed

  • see the comment in main.c for detail; plzcw matched the hand-derived value, pcpyud did not (initial guess had the halves backwards, corrected once).

Building libc++, libc++abi and libatomic

scripts/build-libcxx.sh configures the runtimes build from the r5900-ee LLVM fork (LLVM_SRC, default ../llvm/llvm-project), builds libc++, libc++abi and libunwind with C++ exceptions on (LIBCXX_EXCEPTIONS=OFF for a build without), builds scripts/atomic.c into libatomic.a, and installs all three into ee/mips64r5900el-ps2-elf. Set INSTALL_ROOT to install somewhere else. Link with -lc++ -lc++abi -lunwind -latomic. Programs that use exceptions also need the .eh_frame rules in the SDK's EE linkfile.

thread_local uses emulated TLS: the EE driver config passes -femulated-tls (the R5900 has no rdhwr, so native TLS cannot fetch a thread pointer). steps/08-emutls.sh adds compiler-rt's emutls.o to libclang_rt.builtins-mips64el.a, and libc++abi is built with __cxa_thread_atexit for thread-exit destructors.

The sysroot's newlib comes from ps2/newlib-llvm, branch ee-v4.6.0-llvm: the ps2/newlib ee-v4.6.0 branch plus one commit enabling the POSIX threads profile for __ps2sdk__.