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
To verify that things build on Linux/Mac, use ./b.sh --debug. For Windows, use the Visual Studio solution in the Windows subdirectory
(Windows/PPSSPP.sln) - always build through it, even if a stray CMake-generated build/ directory exists at the repo root (e.g.
left over from WSL/MSYS2 experimentation); that directory is not the supported Windows build path and may not have a working
compiler toolchain wired up.
An agent can drive the VS solution non-interactively with MSBuild.exe instead of opening the devenv GUI. Locate it via
vswhere.exe (same tool/gotchas as described in the libretro section below) and build a specific project with /t:, e.g.:
$installPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath
$msbuild = "$installPath\MSBuild\Current\Bin\MSBuild.exe"
& $msbuild "Windows\PPSSPP.sln" /t:UnitTest /p:Configuration=Debug /p:Platform=x64 /m
(swap /t:UnitTest for /t:PPSSPPWindows or another project name as needed; drop it entirely to build the whole solution).
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:
- Windows: build the
UnitTestproject (unittest/UnitTests.vcxproj), then runWindows/x64/Debug/UnitTest.exe all - Linux/Mac: 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. UnitTest.exe CmdLine Path Utf8); no arguments lists the available tests.
After a git stash/git stash pop that touches a header, do a /t:Rebuild rather than trusting
the incremental build. Stashing rewrites files with timestamps that can leave objects looking newer
than the header they were compiled against, so adding or removing a class member produces a binary
where translation units disagree on the object layout. That shows up as UnitTest.exe segfaulting
before it prints anything, for every test including ones unrelated to the change - which looks like
a catastrophic code bug and is not one. If a build starts crashing inexplicably right after a stash
cycle, rebuild before investigating anything else.
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. This cost
several hours once: a link that silently failed left a stale PPSSPPHeadless.exe in place, every
subsequent "revert this and retest" step reported the same failure, and a change was blamed that a
later clean rebuild proved innocent. The LNK1168: cannot open ... for writing case (a still-running
instance holding the exe, see Debugging) is the most common way to get there, which
is why killing leftover processes before building is worth doing unconditionally rather than only
when a build complains.
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 the equivalent of UnitTest.exe 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.
Legacy Android build (android/jni)
There is a legacy Android build using the raw NDK build system (android/jni/Android.mk + ndk-build), separate from the
gradle build in android/. It's hooked up on CI (see .github/workflows/build.yml, the android matrix entries) and is
useful for quick test builds (it can build ppsspp_headless and the unit tests for Android). You do not need to build it
by default, but if you want to test-build it locally:
- The NDK path is hardcoded in
android/ab.cmd(Windows) or passed via theNDKenv var toandroid/ab.sh(POSIX). It should match thendkVersioninandroid/build.gradle.kts. The scripts copy assets first, then run ndk-build with a core count derived from the machine (nproc / %NUMBER_OF_PROCESSORS%). - Example (POSIX):
cd android && NDK=/path/to/ndk ./ab.sh APP_ABI=arm64-v8a HEADLESS=1 - The
ppsspp_headlessexecutable ends up inandroid/libs/<abi>/.
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, not the
Visual Studio solution, even on Windows - 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. Remember to update both CMakeLists.txt and the visual studio project.
A unit test is often the first thing to call a given function from outside its own .cpp, which makes the
ppsspp_unittest target in the legacy Android build (android/jni/Android.mk, see above) the strictest check
we have: MSVC links an inline function defined in a .cpp anyway, clang correctly does not. So a test can
build and pass on Windows and fail to link only on Android CI, with an undefined symbol pointing at a header
line. Fix it by dropping the bogus inline from the definition, not by avoiding the call.
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.)
Note the runner prints a debug-CRT "Detected memory leaks!" dump after the summary line on Windows debug builds.
That's normal and not a test failure - read the N tests passed, N tests failed line, which comes before it.
See docs/pspautotests.md for a workflow for running pspautotests and improving PPSSPP with the results.
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 ..