mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-02 20:42:29 +01:00
The ffmpeg submodule held prebuilt libraries for every platform next to the sources (about 1.5 GB checked out), and nothing uses those any more. Only the needed sources are copied in, from the submodule's commit, along with every header and template they include. The path is new, so checking out commits on either side of this works. It's also cut down to what PPSSPP uses: the H.264, AAC, MP3, ATRAC3 and ATRAC3+ decoders, the H.264 parser, the demuxers as before, and the FFV1 encoder, AVI muxer and file protocol for AVI dumping. config.h takes its CONFIG_ values from FFmpeg 3.0.2's own configure run with just those enabled. The raw H.264 demuxer stays even though nothing opens raw H.264: probing PSMF data uses it to identify the video stream's codec, which otherwise comes out as none. On top of that: - The files nothing links (configure would build them, but they're never pulled out of the static library, going by the GCC link map) are left out: mostly libavutil's crypto and hashing and the alternative DCT/IDCT/FFT versions. - libswresample isn't built, now that nothing uses it. - The H.264 decoder only does 8-bit video, which is all the PSP has. The SPS parser rejects any other bit depth, and the DSP files no longer build their 9- to 16-bit variants, which the linker kept because the init functions pick one by bit depth at run time. That's 160 files to compile instead of 277, and 391 of FFmpeg's vendored, about 5.8 MB. The README in ext/ffmpeg has what was done, the PPSSPP patches in the copy, how to add a component, and the tradeoffs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
279 lines
18 KiB
Markdown
279 lines
18 KiB
Markdown
# 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](../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.:
|
|
|
|
```powershell
|
|
$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=<platform> /m
|
|
```
|
|
|
|
(swap `/t:UnitTest` for `/t:PPSSPPWindows` or another project name as needed; drop it entirely to build the whole solution).
|
|
|
|
On an ARM64 host, use `$installPath\MSBuild\Current\Bin\arm64\MSBuild.exe` instead. The MSBuild in
|
|
`Bin\` itself runs emulated and picks the 32-bit x86-hosted compiler (`bin\HostX86\arm64\CL.exe`),
|
|
which can't always map the precompiled headers into its address space. Builds then fail
|
|
intermittently, every file of a project at line 1, with `C3859: Failed to create virtual memory for
|
|
PCH` and `C1076: compiler limit: internal heap limit reached`. That looks like running out of
|
|
memory, but lowering `/m` or `CL_MPCount` doesn't help, and `/p:PreferredToolArchitecture=arm64`
|
|
has no effect. The build log's `CL.exe` path shows which host compiler ran.
|
|
|
|
`<platform>` is `ARM64` or `x64` - whichever the machine actually is, so look it up rather than
|
|
picking a default. The output directory follows it (`Windows\<platform>\<configuration>\`), which
|
|
makes building one and running the other an easy mistake. It is easiest to make on Windows-on-ARM,
|
|
where an x64 build runs anyway under emulation: everything appears to work, but it is slower than
|
|
the native build and any performance measurement from it describes the emulator rather than the
|
|
code. `platform.machine()` in Python reports the host; `$PROCESSOR_ARCHITECTURE` reports the shell,
|
|
which is `AMD64` in an emulated shell even on an ARM64 machine.
|
|
|
|
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 `UnitTest` project (unittest/UnitTests.vcxproj), then run `Windows/<platform>/Debug/UnitTest.exe all` (`<platform>` being `ARM64` or `x64`, whichever you built)
|
|
- Linux/Mac: configure with `-DUNITTEST=ON`, then run `build/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](debugging.md)) 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.
|
|
|
|
## FFmpeg
|
|
|
|
Every build compiles FFmpeg (3.0.2) itself, from the copy vendored into `ext/ffmpeg`. There's no
|
|
configure step: `ext/ffmpeg-build` has a hand-kept `config.h` and `sources.mk`, the one list of
|
|
files that every build system uses. After changing the list, run `ext/ffmpeg-build/update_vcxproj.py`
|
|
to copy it into the VS projects. How it works, how to add a codec and why it's done this way are in
|
|
`ext/ffmpeg/README.md`. CMake still links the system's FFmpeg with `-DUSE_SYSTEM_FFMPEG=ON`, or one
|
|
in `FFMPEG_DIR`.
|
|
|
|
## UWP build
|
|
|
|
UWP has its own solution, `UWP\PPSSPP_UWP.sln`. Build the app with `/t:PPSSPP_UWP` (or a single
|
|
library such as `/t:CoreUWP`), same MSBuild setup as above:
|
|
|
|
```powershell
|
|
& $msbuild "UWP\PPSSPP_UWP.sln" /t:PPSSPP_UWP /p:Configuration=Debug /p:Platform=<platform> /m
|
|
```
|
|
|
|
## Android assets
|
|
|
|
The APK's `assets/` directory is the repo-root `assets/` directory, wired up by
|
|
`assets.directories.add("../assets")` in `android/build.gradle.kts`. There's no copy step, so a file dropped
|
|
in `assets/` ships as-is. `ndk-build` doesn't read assets at all, and nothing populates `android/assets/` -
|
|
if you have one, it's a leftover from the old Ant/Eclipse build and is ignored.
|
|
|
|
## 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 the `NDK` env var to `android/ab.sh` (POSIX).
|
|
It should match the `ndkVersion` in `android/build.gradle.kts`. The scripts just 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_headless` executable ends up in `android/libs/<abi>/`.
|
|
|
|
## tvOS
|
|
|
|
The tvOS app is the iOS app with the iPhone-only parts compiled out, checked with
|
|
`PPSSPP_PLATFORM(TVOS)` (tvOS also gets `PPSSPP_PLATFORM(IOS)`). The CMake side is the iOS toolchain
|
|
with `-DIOS_PLATFORM=TVOS`, which sets `TVOS`.
|
|
|
|
- `./b-tvos.sh` builds an unsigned app into `build-tvos-xcode/Release-appletvos/`.
|
|
`DEVELOPMENT_TEAM=<team> DEVICE=<udid> ./b-tvos.sh` signs it, registers the Apple TV with the team
|
|
if needed, installs it and launches it with the console attached.
|
|
- To pair an Apple TV, put it on Settings > Remotes and Devices > Remote App and Devices. It doesn't
|
|
show in `xcrun devicectl list devices` until paired; find it with
|
|
`dns-sd -B _remotepairing-manual-pairing._tcp` and `xcrun devicectl device info details --device <name>`,
|
|
then `xcrun devicectl manage pair --device <identifier>` and enter the code it shows.
|
|
- `./b.sh --tvos --libretro` (and `./b.sh --ios --libretro`) build the libretro cores. tvOS has no
|
|
`sigaltstack`, among other things, so this is the build that catches it.
|
|
- There's no tvOS simulator build. FFmpeg having no tvOS simulator libraries was what stopped it,
|
|
which no longer applies now that it's built from source, but nobody has tried since.
|
|
- tvOS apps have no persistent storage except about 500 KB of `NSUserDefaults`. Anything else may be
|
|
purged, so the memory stick is in Caches for now.
|
|
|
|
## 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`).
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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. A new file has to be listed in three build files, not two:
|
|
`CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its `.filters`), and `android/jni/Android.mk`, which
|
|
builds a unit test executable of its own. The Android one is the easiest to forget, since missing it builds
|
|
fine everywhere you are likely to try it and only fails on Android CI.
|
|
|
|
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.
|
|
|
|
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`):
|
|
|
|
```bash
|
|
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.
|
|
|
|
### 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):
|
|
|
|
```bash
|
|
./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.
|
|
|
|
The 32-bit ARM JIT works the same way with `./b.sh --armhf` (into `build-armhf/`) and `qemu-arm -L
|
|
/usr/arm-linux-gnueabihf`. The cross packages are `gcc-14-arm-linux-gnueabihf g++-14-arm-linux-gnueabihf`, and
|
|
`sudo cmake/scripts/setup-cross.sh armhf` adds the GL and EGL stubs the link needs. Without root, `apt-get download`
|
|
the packages, `dpkg-deb -x` them into one directory, and point `CROSS_ARMHF` at it: the toolchain file then takes
|
|
the sysroot from `$CROSS_ARMHF/usr/arm-linux-gnueabihf`. The binutils need `LD_LIBRARY_PATH` to include
|
|
`$CROSS_ARMHF/usr/lib/x86_64-linux-gnu`, glibc's `libc.so` linker script names absolute paths that have to be
|
|
rewritten to the new root, and the stubs are copied in by hand. For a debugger, run `qemu-arm -g PORT` and attach
|
|
`gdb-multiarch` to it.
|
|
|
|
`hash/hash` dies with a bus error under qemu-arm on every CPU core, so it's not a JIT problem, but it takes the rest
|
|
of its batch down with it: pass `--known-failures=armhf`.
|
|
|
|
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/LoongArch64` and their emitters) have no architecture guards, so the host compiler can check
|
|
them: `clang++ -fsyntax-only` per file, with the `-I`/`-D`/`-std`/`-isystem` flags of any Core file from
|
|
`compile_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 `$FLAGS` variable, 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 of `ppsspp_config.h` in `#ifndef PPSSPP_TEST_NO_SIMD`, configure a separate
|
|
`build-nosimd` with `-DHEADLESS=ON` and `-DPPSSPP_TEST_NO_SIMD` in `CMAKE_C_FLAGS` and `CMAKE_CXX_FLAGS`,
|
|
and build `PPSSPPHeadless` there. Delete it and revert `ppsspp_config.h` afterwards, since `test.py` keeps
|
|
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:
|
|
|
|
```bash
|
|
cd build ; make -j32; cd ..
|
|
```
|