The libretro core is the frontend, and the headless tools are the test and debugging hosts. The Windows, UWP, Android, iOS, macOS and SDL apps, the UI framework and imgui go, along with what only they used: the Win32 debugger hooks (g_frameMutex, the shutdown lock), JNI thread attachment, the Android text drawer, RAIntegration, Discord, recent files, game installs, tilt input, the audio resamplers, the control mapper, memstick moving and the remote ISO and upload server modes. The web server keeps the debugger. The MSVC, Gradle and ndk-build projects go too.
9.9 KiB
Building and testing PPSSPP
Agent-oriented notes on the various build systems and test suites. This is the long version of the "Build and validation" / "Testing" sections in AGENTS.md.
Build and Validation
The supported targets are the libretro core and the headless tools (PPSSPPHeadless and the unit
tests). For the core, make -C libretro -j32 (platform= picks the target, see libretro/Makefile)
or ./b.sh --libretro; for the headless tools, ./b.sh --headless --unittest PPSSPPHeadless PPSSPPUnitTest.
In addition to the pspautotests runner (test.py), there is a separate binary with C++ unit tests in the /unittest subdirectory. After substantial changes (at the end of a chunk of work, not necessarily after every edit), run these too:
- Configure with
-DUNITTEST=ON, then runbuild/PPSSPPUnitTest all
This runs all tests in availableTests in unittest/UnitTest.cpp. You can run one or more
specific tests by passing their names instead of all (space-separated, e.g. PPSSPPUnitTest CmdLine Path Utf8); no arguments lists the available tests.
After a header change, or a git stash/git stash pop that touches a header, build the libretro core
clean: its Makefile doesn't track header dependencies, so adding or removing a class member produces a
binary where translation units disagree on the object layout. That shows up as crashes that look like
a catastrophic code bug and are not one.
More generally, when you are bisecting a behavioural change, confirm the binary actually changed before you believe the result - check the executable's mtime, or have the code you just added log something you can grep for. A stale binary is indistinguishable from a real regression, and it lies consistently, so a bisect on top of one produces a confident, entirely fictional answer.
Known environment-specific issue: in at least one sandboxed dev environment, the Jit test
(unittest/JitHarness.cpp) hangs indefinitely specifically during the CPUCore::JIT_IR
phase - confirmed unrelated to source changes (reproduces identically on unmodified checkouts)
and not a memory-access fault (Memory::HandleFault is never entered). Root cause wasn't
pinned down further (would need a native debugger attached to the hung process, not available
in that environment) but is very likely specific to that sandbox rather than a real PPSSPP
bug, since CI runs PPSSPPUnitTest all on every commit across multiple
platforms without apparent issue. If all/Jit hangs in your environment, run every other
test by name instead (skip Jit) to still get real coverage.
libretro core build (Windows)
Canonical instructions are in libretro/README_WINDOWS.txt - read that first, this is a summary plus
agent-specific gotchas. The libretro core (ppsspp_libretro.dll) is built with a real make - it uses cl.exe/link.exe as the compiler/linker (via
platform=windows_msvc2019_desktop_x64), but orchestrated through GNU Make running inside an MSYS2
shell (a plain MSYS2 install, not "Git Bash" - typically at C:\msys64, needs pacman -S make).
cd libretro
make DEBUG=1 platform=windows_msvc2019_desktop_x64 -j32
(drop DEBUG=1 for a release build; -j count doesn't need to match logical CPUs exactly). To test the
result, copy ppsspp_libretro.* into wherever the local RetroArch install reads cores from (e.g. its
cores/ directory) and load it from within RetroArch.
An agent can drive this non-interactively by invoking C:\msys64\usr\bin\bash.exe -lc "..." directly
as a subprocess (the -l login-shell flag matters - it's what sets up MSYS2's own PATH, make,
cygpath, etc. correctly). In a sandboxed/agentic invocation (as opposed to a normal interactive MSYS2
terminal a human opens), two Windows environment variables the Makefile's VS-detection logic depends on
may not be inherited by the spawned process - COMSPEC (breaks the cmd //c "bash VSWhere.sh ..." call
used to locate Visual Studio) and ProgramFiles(x86) (which VSWhere.sh itself needs to find
vswhere.exe). If VS auto-detection fails this way, skip it by overriding VsInstallRoot directly on
the make command line (GNU Make command-line variables take precedence over the Makefile's own :=
assignment of the same name):
make VsInstallRoot="/c/Program Files/Microsoft Visual Studio/<year>/<edition>" DEBUG=1 platform=windows_msvc2019_desktop_x64 -j32
(path in MSYS2/cygpath POSIX form, not a raw Windows path; find the real value via vswhere -latest -property installationPath if unsure of <year>/<edition>). This is a real full compile+link - prefer
it over trying to syntax-check libretro-specific files with a standalone cl.exe /Zs invocation, which
can miss real bugs (e.g. an include-order issue that leaves a platform macro like
VK_USE_PLATFORM_WIN32_KHR undefined before vulkan.h's first, include-guarded inclusion, since a
narrower manual include-path/define set used for a syntax-only check may not reproduce the actual build
step's ordering).
Headless and unittest builds
We have additional PPSSPPHeadless and unit test builds (/headless and /unittest), that have their own separate main functions (and also stub out most of the System_ functions as needed). Take these into account when making cross platform changes.
New unit tests are added by listing them in availableTests in unittest.cpp. If they are large, put them in
separate files in the unittest subdirectory, listed in CMakeLists.txt.
A unit test is often the first thing to call a given function from outside its own .cpp: MSVC links an
inline function defined in a .cpp anyway, clang correctly does not, so a test can link on one and not the
other, with an undefined symbol pointing at a header line. Fix it by dropping the bogus inline from the
definition, not by avoiding the call.
The compilers also disagree about floating point contraction, which matters for any test asserting that a JIT
is bit-identical to its C++ reference. Clang folds a * b + c into a single fused multiply-add by default;
MSVC never does, under /fp:precise, in Debug or Release. So on arm64, where the JITs emit FMLA, a
reference written as a * b + c matches on Mac, Linux and Android and is off by one ULP on Windows on ARM.
Don't leave it to the compiler: write fmaf(a, b, c) when the fused result is wanted (MSVC compiles it to a
single fmadd), and a * b + c when it isn't. PrescaleUV in GPU/Common/VertexDecoderCommon.cpp picks per
architecture, matching what each JIT does. x86 doesn't have the problem, since the SSE2 baseline has no FMA
instruction to contract into.
pspautotests are a large set of tests of the PSP OS's API surface, and thus tests our HLE implementation.
To check for regressions, run them exactly the way CI does (see .github/workflows/build.yml):
python test.py -g --graphics=software
The -g matters. test.py keeps two lists: tests_good (the regression set - these pass and must keep
passing, ~314 of them) and tests_next (work-in-progress tests that are expected to fail, i.e. the to-do list).
-g runs only tests_good; with no flag you get tests_next + tests_good and around a hundred failures that mean
nothing is wrong. Don't go hunting those, and don't report them as regressions - the only meaningful result from
-g is 0 tests failed. (-b runs only tests_next; -m prefix-filters whichever list is selected.)
See docs/pspautotests.md for a workflow for running pspautotests and improving PPSSPP with the results.
LoongArch64 and RISC-V JITs under qemu
CI tests these JITs by cross-building headless and running pspautotests under qemu-user, and you can do the
same on Linux (packages: gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu qemu-user, or the riscv64
equivalents):
./b.sh --loongarch64 PPSSPPHeadless # builds into build-loongarch64/
mkdir -p build-qemu
printf '#!/bin/bash\nexec qemu-loongarch64 -L /usr/loongarch64-linux-gnu "$(dirname "$0")/../build-loongarch64/PPSSPPHeadless" "$@"\n' > build-qemu/PPSSPPHeadless
chmod +x build-qemu/PPSSPPHeadless
python3 test.py -g --graphics=software --cpu=jit-ir --timeout=60 --known-failures=loongarch64
test.py runs the most recently modified build*/PPSSPPHeadless, so touch the shim after rebuilding the real
binary, or it runs your native build instead. The full suite takes a couple of minutes.
qemu reports LSX and LASX as present, so CI only exercises the LoongArch vector paths. The scalar fallbacks never run here, so passing tells you nothing about them.
Without a cross toolchain (on macOS, say) two things still help:
- A syntax check of the other JITs. The RISC-V and LoongArch backends (
Core/MIPS/RiscV,Core/MIPS/LoongArch64and their emitters) have no architecture guards, so the host compiler can check them:clang++ -fsyntax-onlyper file, with the-I/-D/-std/-isystemflags of any Core file fromcompile_commands.json, plus-I<repo> -I<repo>/ext -I<repo>/Common(the database's include paths are relative to the build directory). Check that a copy with a deliberate syntax error does report errors: zsh doesn't word-split a$FLAGSvariable, which once made a broken check look clean. This proves the code compiles, nothing about its output. - The portable software-renderer paths. riscv64 and loongarch64 run the software renderer's non-SSE,
non-NEON C++ paths, so their test failures reproduce natively: wrap the two NEON defines in the
__aarch64__block ofppsspp_config.hin#ifndef PPSSPP_TEST_NO_SIMD, configure a separatebuild-nosimdwith-DHEADLESS=ONand-DPPSSPP_TEST_NO_SIMDinCMAKE_C_FLAGSandCMAKE_CXX_FLAGS, and buildPPSSPPHeadlessthere. Delete it and revertppsspp_config.hafterwards, sincetest.pykeeps picking the newest binary. A failure that's the same on both architectures points at the portable path, not the JITs.
Quick rebuild on Linux
You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut:
cd build ; make -j32; cd ..