Files
ppsspp/headless
Henrik RydgårdandClaude Opus 5.5 9ddfcca670 Firmware install: Fewer and parallel file operations into the staging directory
Through Android's storage framework, every exists check, create and open is a round trip to the
storage provider, and writing each file took four of them (a check of its directory, a check of
the file, the create, the open). That made a 6.61 install take around 30 s on a phone, against a
fraction of a second of decrypting and inflating.

The install unpacks into a staging directory it has just cleared, so nothing in it can exist yet.
With the new outputDirIsEmpty option, each directory is created once and each file is created and
opened without checking for it first (File::WriteDataToNewFile), on the IO threads while decoding
goes on. A name that turns up twice still goes the careful way, overwriting the first. The swap
into place at the end is unchanged.

Headless --unpack-updater uses the same path when the output directory doesn't exist yet. Its
output is identical either way. With SIMULATE_SLOW_IO, unpacking the fonts went from 17 s to 7.5 s,
most of what's left being the fixed cost of opening the updater.

The UI's install already runs on a thread pool IO thread, so writes queued on the same pool
could stall behind other long IO tasks, and the pool drops queued tasks unrun at teardown,
which would have left the install waiting forever on its in-flight count. Eight threads owned by
the writer (attached for JNI) avoid both, and headless no longer has to start the pool for it.

Also compare file names case-insensitively when looking for repeats, since the storage usually
is case-insensitive and a second create there makes a renamed copy.

Checked with ThreadSanitizer, and on a read-only destination (every file fails, no hang).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:06:47 +02:00
..

PPSSPPHeadless

Non-interactive, headless build of PPSSPP. It boots a PSP executable, PRX, or GE frame dump (.ppdmp) without a GUI, forwards what the program prints to the console, optionally captures and compares text output or screenshots, and exits.

Primarily intended for:

  • Automated regression testing (via pspautotests)
  • Replaying GE frame dumps (.ppdmp) to compare rendering output across GPU backends or settings
  • CI pipelines that need to verify emulation correctness

Building

Windows (Visual Studio)

The PPSSPPHeadless project is part of the Windows/PPSSPP.sln solution. Build the PPSSPPHeadless target in your preferred configuration.

Example:

msbuild Windows\PPSSPP.sln /p:Configuration=Debug /p:Platform=x64 /t:PPSSPPHeadless

Linux / macOS (CMake)

cmake -DHEADLESS=ON -B build-headless
cmake --build build-headless

Usage

PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options]

Options

Argument Description
file.elf / file.prx / file.ppdmp Executable or frame dump to run. Directories followed by ... recurse for .prx files.
@file Read list of test filenames from a text file (@- for stdin).
-m, --mount <file.cso> Mount an ISO/CSO on umd1:.
-r, --root <path> Mount a path on host0: (ELF/PRX files must be under this).
-l, --log Full emulator log output, on top of the program's own output (see Program Output below).
-o, --odslog Write log to OutputDebugString (Windows only).
--graphics=<backend> GPU backend: software, gles, directx11, vulkan.
--screenshot=<file> Compare the rendered output against a reference screenshot.
--screenshot-save=<file> Save the rendered output to a file (PNG if the path ends in .png, BMP otherwise).
--screenshot-dir=<dir> Save each test's output as <dir>/<test path>.png, so one run can render many dumps.
--screenshot-diff=<file> When comparing screenshots, save a visual comparison image to this file (always, regardless of pass/fail).
--screenshot-keep-alpha Preserve the alpha channel when saving PNG screenshots (default: alpha is forced to 255, since games often use it for non-visual purposes).
--max-mse=<number> Maximum allowed Mean Squared Error for screenshot comparison (default: 0 = exact).
--compare / -c Compare test output with .expected text file and/or screenshot (see below).
--timeout=<seconds> Abort test if it takes longer than this.
-v, --verbose Print full pass/fail details.
--bench Run multiple times and report average speed.
-i Use interpreter CPU core.
--ir Use IR interpreter CPU core.
-j Use JIT CPU core (default).
--debugger=<port> Enable WebSocket debugger and break at start.
--state=<file> Load a save state before running.
--old-atrac Use the old Atrac3+ audio decoder.
--ignore <file> Skip the specified test file.
--help / -h Show usage information.

Program Output

Running a homebrew with no options at all prints what it prints, and nothing else. Two separate channels feed the console, both on by default:

  • The emulated program's stdout and stderr - sceIoWrite() to fd 1 and 2 (which is where a PSPSDK printf() ends up), and writes to a tty device. These go to our stdout and stderr respectively, byte for byte, with no prefix or sanitization.
  • The emulator: devctl channel - sceIoDevctl("emulator:", SEND_OUTPUT, ...), a PPSSPP extension. This goes to stdout. pspautotests uses this channel exclusively, and it is what --compare compares against the .expected file.

--compare and --bench turn both off, so the only thing a test run prints is the comparison result. Emulator log output is separate again, and stays off unless you pass -l.

GPU Backends

The --graphics option selects the rendering backend:

Backend Description
software Software rasterizer (most deterministic, recommended for tests)
gles OpenGL ES (desktop OpenGL on non-Windows)
directx11 Direct3D 11 (Windows only)
vulkan Vulkan

The software backend produces deterministic output across runs and is the default. Hardware backends may produce slightly different pixels due to precision differences in shaders and rasterization.

Frame Dump Replay (.ppdmp)

GE frame dumps are recordings of a single frame's GE graphics commands. When a .ppdmp file is passed:

  1. The file is identified by the PPSSPPGE magic header.
  2. The GE commands are replayed through the selected GPU backend.
  3. The framebuffer is captured automatically (512×272 stride, 480×272 visible).
  4. A screenshot is sent for comparison/saving via --compare, --screenshot, or --screenshot-save.

For batch rendering tests over sets of frame dumps, see docs/frametest.md (the frametests.py runner).

Example: Generate a reference screenshot

# BMP:
PPSSPPHeadless.exe --graphics=software --screenshot-save=reference.bmp frame.ppdmp
# PNG (lossless, recommended for storing references):
PPSSPPHeadless.exe --graphics=software --screenshot-save=reference.png frame.ppdmp

The BMP output is 512×272 with 32-bit BGRA pixel data. The file size is always 557,110 bytes (54-byte header + 512 × 272 × 4 bytes). PNG output is 512×272 RGBA.

Example: Compare against a reference

# With --compare, auto-derives the screenshot path:
#   frame.ppdmp  →  looks for  frame.png  (next to the .ppdmp)
PPSSPPHeadless.exe --graphics=software --compare frame.ppdmp

# Explicit reference file (saves a visual comparison to diff.png):
PPSSPPHeadless.exe --graphics=software --screenshot=reference.bmp --screenshot-diff=diff.png --max-mse=0.5 frame.ppdmp

When the MSE exceeds --max-mse, the following files are saved in the working directory:

File Contents
__testfailure.bmp The actual rendered output (512×272 BMP).
__testcompare.png Visual comparison: left column = actual, right column = reference on top, diff map on bottom.

Screenshot Comparison Details

  • Resolution: Always 480×272 display captured from a 512-pixel-wide framebuffer.
  • Format: MSE (Mean Squared Error) calculated per-pixel across R, G, B channels (alpha ignored).
  • MSE formula: MSE = sum((actual - reference)^2) / (width × height × 3)
  • Reference formats: BMP (32-bit BGRA) or PNG (auto-detected by header).
  • --compare auto-path: For .ppdmp files, it replaces the extension with .png. For .prx/.elf files, it appends .expected.bmp.
  • Failure output: Off by default in GitHub Actions CI; controllable via SetWriteFailureScreenshot().

Text Output Comparison

The --compare flag also compares emulated debug output (printf / sceIoWrite to the emulator channel) against a .expected text file:

  • For .prx files: same path with .expected extension.
  • The comparison is line-based with a diff algorithm that highlights mismatches and insertions/deletions, although the support for the latter is limited, it's not a full-on diff.

If only a screenshot reference exists and no .expected file, the test passes as long as there is no unexpected text output.

Batch Testing

Tests can be listed in a text file or piped via stdin:

# List file
PPSSPPHeadless.exe --compare --timeout=5 @testlist.txt

# Stdin
echo "test1.prx test2.prx test3.ppdmp" | PPSSPPHeadless.exe --compare --timeout=5 @-

# Recursive directory scan (append /...)
PPSSPPHeadless.exe --compare tests/cpu/...

Exit code is 0 if all tests pass, 1 if any fail.

CI Integration

GitHub Actions is auto-detected via the GITHUB_ACTIONS environment variable and will print error annotations inline.

Example workflow step:

- name: Headless tests
  run: |
    PPSSPPHeadless.exe --graphics=software --compare --timeout=10 @testlist.txt

Configuration

The following configuration is hardcoded for headless mode (config file is never saved):

(For actual up-to-date hardcoded config, see the headless.cpp file).

Setting Value Notes
Internal resolution 1× (480×272) Fixed.
Hardware transform Enabled
Vertex decoder JIT Enabled
Software renderer JIT Enabled
Ignore bad mem access Enabled
Firmware version 6.60
PSP model Slim