Files
ppsspp/AGENTS.md
T
libretroadmin f1208a7a5a Make threading lock-free
The core and the headless tools no longer use mutexes, condition variables,
spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through
libretro-common's retro_atomic and rthreads plus a few small pieces on top of
them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and
ReaderGate. Every wait blocks until it's woken; nothing polls on a timer.

State that only one thread touches just loses its lock. The PSP filesystem,
sceIo and the savedata dialog now belong to the emulation thread: async reads
and writes run when they're queued and complete later in emulated time, and
the IO and savedata threads are gone. The friend finder only talks to the
adhoc server and hands what it gets to the emulation thread. GPU init always
runs on the thread that starts the game.

The Vulkan render and compile queues, pipeline and render pass caches, delete
list and SPIR-V cache, the GL render queue, the software renderer's jit caches,
the file loader caches, the web debugger, UPnP, input, savestates, logging and
the GE debugger all hand off through queues and atomics. The IR jit's spinning
profiler thread goes.

A CPU thread with nothing to run now sleeps until work is queued instead of
spinning. Headless with --debugger waits for a client while stopped, and ends
the run once every client that connected has left. Web server shutdown keeps
servicing the CPU queue until the server has finished.
2026-10-09 04:56:16 +02:00

20 KiB

PPSSPP Agent instructions

These rules apply to this repository by default.

Detailed guides

The rules below are the short version. These docs hold the detail, look them up when the task calls for it:

Doc When you need it
docs/building.md Build commands for the libretro core and the headless tools, unit tests, pspautotests
docs/debugging.md Driving the WebSocket debugger and PPSSPPHeadless from a script, measuring a commercial game with headless, comparing binaries' speed (Tools/headless_bench.py), breakpoint reliability per CPU backend, debugging a game that works on hardware
docs/DebuggerThreading.md Debugger thread ownership: Core_RunOnCPUThread, per-connection mailboxes, no locks - required reading before touching debugger code
docs/HLEModules.md Adding an HLE module or function, and the seven build files a new source file goes in
docs/translations.md Translating UI strings with Tools/langtool
docs/pspautotests.md Workflow for improving PPSSPP using pspautotests
docs/pspautotests-hardware.md Writing a new pspautotest, and running it on a real PSP over PSPLink to record its .expected
docs/frametest.md Framedump rendering tests, and replaying a dump on a real PSP
docs/sceGe.md How the firmware queues display lists, what SIGNAL and FINISH interrupts do and in which order, and how ProcessDLQueue() keeps that order
docs/sceAudio.md How the audio output calls block, how deep they buffer, and what each error means
docs/WebSocketDebugger.md WebSocket debugger protocol reference
docs/reverse-engineering.md Disassembling a firmware PRX with --re-module, to find out what the hardware actually does
docs/command-line.md Adding a command-line option - the g_autoParams table, per-mode options, ApplyToConfig()
docs/patching-files.md Editing files from a script without wrecking the diff: line endings, and heredoc backslash escaping
docs/VSHBootInvestigation.md Booting the PSP's Visual Shell with --vsh against a real firmware dump
docs/PsarFileFormat.md The PSAR archive inside a firmware updater - the format Core/Util/PSARUnpack.cpp walks
docs/pkg_notes.md The NPDRM .pkg format PSP game updates ship in, and how PPSSPP installs them
docs/kernel-hle-review.md Findings from a review pass over Core/HLE/sceKernel*.cpp, and what was verified clean
docs/metal-backend.md What a native Metal backend would take, and why programmable blending is the reason to want one

General instructions

  1. Keep style changes minimal unless requested. Follow existing code patterns and conventions.

  2. Keep cross-platform parity in mind when changing shared code. See below for more multiplatform tips

  3. Commit freely, push only when asked. Once a change is done and tested, commit it on its branch without waiting to be told. Never git push (to any remote) without the user's explicit approval.

  4. Don't write code on master. When asked to make a code change while on master, create an appropriately named branch first (git checkout -b some-descriptive-name) and do the work there. If you're already on a topic branch, just keep working on it.

  5. Never assume a file's line endings - preserve whatever is on disk. Which ending a file has depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a Linux checkout leaves files as they are stored, so the same file (libretro/Makefile.common, this file, much of the source) is CRLF in one working copy and LF in another. Don't hardcode either, and don't "fix" a file's endings to match what a doc claims. If you patch one with a script, read and write with newline='', which keeps whatever was there; reading with Python's default universal-newline translation and writing with newline='' silently converts the whole file, turning a two-line addition into a 5000-line diff. Check git diff --stat before committing - a whole-file rewrite is obvious there and invisible in the editor. Prefer the Edit tool, which does exact string replacement and can't do this.

    Don't verify this with grep -c $'\r$' - Git Bash's grep normalises line endings, so it counts a bare-LF line as having a CR and reports any file as clean. The opposite mistake to a whole-file rewrite is inserting a handful of LF lines into a CRLF file (a Python """...""" block written with newline='' does exactly this), and that shows in --stat only as the lines you meant to add. Check the bytes instead, e.g. python -c "d=open(F,'rb').read(); print(d.count(b'\r\n'), d.count(b'\n'))", and treat git's "LF will be replaced by CRLF the next time Git touches it" as the warning it is rather than autocrlf noise.

  6. Don't feed Python to bash -c via a heredoc when the code needs a literal backslash in its output. Git Bash strips one level of escaping on the way in even with a quoted delimiter (<<'PY'), so "\\n" reaches Python as "\n" and writes a real newline into the file - no error, just a patch that quietly did nothing. Use the Edit tool, or write the script to a file and run it. Details and the other two failure shapes: docs/patching-files.md.

  7. NEVER run more than one PSP hardware operation at a time. There is one PSP. Every gentest.py, pspsh or other PSPLink call gets its own message, containing that one tool call and nothing else, and the next one starts only after the previous one has returned. Never put two of them in the same message as parallel tool calls, never background one and start another, and never loop over tests in one shell command. Re-recording a directory means one test, wait, the next test. Concurrent runs collide in PSPLink and host0:: they time out, their results are garbage, and the PSP wedges until someone resets it by hand. This has happened repeatedly; there is no case where it is acceptable.

Core Safety Checks

  1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.
  2. Consider savestate compatibility when changing serialized state.
  3. Never insert an entry into the middle of an HLEFunction array. A savestate stores the syscall opcode, which encodes the entry's index in that array - so inserting anywhere but the end silently repoints every later entry, and old savestates start calling the wrong function. This applies to adding a single function to an existing module, which is when it is easiest to forget: put it last in the array even when alphabetical or NID order would put it elsewhere, and even when the array is otherwise tidily sorted. The same rule governs the order of Register_*() calls in Core/HLE/HLETables.cpp - new modules go at the very end.
  4. Changing a shader cache key means bumping CACHE_VERSION, in both the OpenGL and Vulkan caches (GPU/GLES/ShaderManagerGLES.cpp and GPU/Vulkan/ShaderManagerVulkan.cpp). Their on-disk caches store raw key bits with nothing to tell an old layout from a new one. OpenGL stores VShaderID/FShaderID, and Vulkan stores those plus VulkanPipelineKey (the raster state key and the decoded vertex format ID). So adding, removing, moving or reinterpreting any of those bits needs the bump, or an old cache precompiles shaders for keys that now mean something else, including combinations the generators assert on. D3D11 doesn't store shader IDs on disk. Keys that only live in memory, like SamplerCacheKey, don't need a bump.
  5. "As on hardware" needs a hardware test behind it. A commit message or comment that says the PSP does something names the pspautotests test that shows it, recorded before the change. And the test has to reach the code path the change is about: when the HLE code handles the same call differently by dialog type, mode or size, cover each of those. A dialog fix once claimed hardware behaviour that the test recorded an hour later contradicted, and the test ran a message dialog, which took a different branch from the savedata dialog that then hung Freak Out.
  6. No locks, no sleeps. Code built into the core and the headless tools uses no mutexes, condition variables, spinlocks, semaphores, std::atomic, std::thread or volatile for synchronization, and never sleeps or polls on a timer. Use retro_atomic.h, and in Common/Thread/: Thread, MpscQueue (any thread pushes, one drains), ParkingLotWait / ParkingLotNotify and EventCounter for blocking waits, ReaderGate for read-mostly data (copy, swap, drain, free). Prefer giving state a single owning thread and sending it messages. The PSP filesystem, sceIo and the utility dialogs belong to the emulation thread.

Build and validation

  • The libretro core: make -C libretro -j32 (see libretro/Makefile for platform=), or ./b.sh --libretro.
  • The headless tools: ./b.sh --headless --unittest PPSSPPHeadless PPSSPPUnitTest; after that, cd build ; make -j32; cd ...
  • A stale binary lies consistently. The libretro Makefile doesn't track header dependencies, so after a header change (or a git stash cycle that touched one) build it clean; when bisecting a behavioural change, confirm the binary actually changed before you believe the result.
  • If a savestate fails to load, first suspect the branch, not the loader. Either your branch is behind the build that made the state (rebase it on origin/master and rebuild), or the state was made on a branch that hasn't been merged yet. Only once you've ruled out both is it a savestate compatibility bug.

Testing

After a chunk of work (not after every edit), run both suites:

  • C++ unit tests: configure with -DUNITTEST=ON and run build/PPSSPPUnitTest all. Tests are listed in availableTests in unittest/UnitTest.cpp; pass names instead of all to run a subset.
  • pspautotests (HLE coverage) - run them exactly the way CI does:
python test.py -g --graphics=software

The -g matters: without it you also get tests_next, the expected-to-fail to-do list, and around a hundred failures that mean nothing is wrong. The only meaningful result is 0 tests failed. (The debug-CRT "Detected memory leaks!" dump after the summary line is normal, not a failure.)

When the thing under test is a commercial game rather than a suite, headless needs --log before it prints anything (to stderr), and defaults its memory stick to <exe dir>/memstick rather than the app's - so the firmware you installed in the app isn't there. Pass --memstick=<the app's memstick> for any run that is meant to exercise a real firmware module. Without it, a library whose HLE the config has disabled has nothing left to resolve against, and the game does not fall back to our HLE - it gets unresolved imports and dies. The tell is a single line early on, sceMpeg HLE is disabled, but flash0:/kd/mpeg.prx isn't in the firmware and the game didn't bring its own, followed by a run that logs happily for its whole timeout with zero sceDisplaySetFramebuf calls and a handful of cached textures. Count flips before concluding anything about a game's behaviour. A run configured differently from what you asked for, or one that never reached the code, produces the same all-zero counts as a clean one, so assert the exit code and a positive "we got here" counter before believing any error count. The traps in full: docs/debugging.md.

Unresolved-import errors early in a game run, for scePsmfPlayer in particular, are expected and not a problem: many games ship libpsmfplayer (and similar libraries) on the disc, and the imports resolve when the game loads the module. Don't change the setup (dropping --memstick, turning HLE back on) to make them go away; a run should be as close to a real one as possible, with firmware modules running as LLE.

Keep game runs short and fast, so nobody has to watch them: always pass --timeout-wall=30 (alongside any --timeout-emulated) unless there's a real reason for longer, use --graphics=vulkan rather than --graphics=software (if Vulkan doesn't work in headless, fix that), prefer a Release build, and leave out debug-level --log unless you need it. Each of those can cost an order of magnitude: Outrun loading a state took minutes with a Debug build, software rendering and a 300MB log, and about a second without them. To check that a game renders, --screenshot-save=FILE.png beats grepping the log. (pspautotests through test.py are different: they have their own per-test timeouts and use --graphics=software like CI.)

New unit tests are added to availableTests; large ones go in their own file in unittest/, listed in CMakeLists.txt.

Multiplatform considerations

The libretro core is the frontend; the headless tools are the test and debugging hosts. The core builds for many platforms through libretro/Makefile and libretro/Makefile.common, so compile for more than the one you're on, and let CI handle the rest.

System_-prefixed wrapper functions implement a platform wrapper for some functionality, and are implemented in libretro/libretro.cpp, headless/Headless.cpp and unittest/UnitTest.cpp. If we change one, we need to change them all.

Reverse-engineering the firmware

When a question about hardware behaviour can't be settled from the docs or from JPCSP - what a field in a codec context means, what a library returns when a buffer runs dry - read the firmware. PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR loads one PRX standalone and writes an annotated disassembly, the export/import tables with NIDs resolved, and a call graph. It needs a firmware dump (--memstick pointing at one; --unpack-updater can produce one).

Two things to know before trusting what you read there:

  • Don't infer a function's arity from the registers it reads. MIPS code routinely leaves an argument untouched for a callee to pick up, so a function that reads only a0 may well take three. The per-function register evidence block flags this as FORWARDED; follow the callees.
  • Record how you know. A comment saying which module and function a fact came from is worth more than the fact alone, since the next person can re-derive it. Behavioural findings belong in the tree; bulk transcriptions of Sony's code do not.

Command-line parsing

All command-line parsing for both the main app and headless builds belongs in Core/CmdLine.cpp / Core/CmdLine.h (CommandLineOptions), not in headless/Headless.cpp. Don't re-parse argv manually in those files - add a field to CommandLineOptions, and push it into g_Config from ApplyToConfig() so every platform gets it for free. How to declare one: docs/command-line.md.

File formats, codecs, and other format handlers

Before implementing any file format handler, decompressor, codec, or similar from scratch, search the codebase first - PPSSPP already has implementations of many formats (CSO, LZRC, zlib-based loaders, ISO handlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of writing a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).

For string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.

Framedump rendering tests (frametests)

frametests.py replays GE frame dumps (.ppdmp) through PPSSPPHeadless and compares the output against reference images, with a JSON config per test set. Consider running these when changing rendering code.

Note: headless/Compare.cpp reads back framebuffers top-down; the flip to bottom-up is only applied when writing BMPs (and when reading BMP references). TranslateDebugBufferToCompare also exists as a copy in libretro/LibretroGraphicsContext.cpp - keep the two in sync.

Adding HLE modules

HLE module implementations live in Core/HLE/sce<ModuleName>.cpp / .h, as a const HLEFunction <name>[] table registered via RegisterHLEModule(). Both the function table and the Register_*() calls are append-only - see Core Safety Checks above, which is what breaks old savestates silently if ignored.

Also: a new .cpp/.c file has to be added to both build files (the CMake lists and libretro/Makefile.common); headers to the CMake lists. Full details and the format-string legend are in docs/HLEModules.md.

Translated UI strings (assets/lang)

When implementing new UI, translations come last, in their own commit after everything else is done. Write the English strings, get the feature built and working, commit that - then stop and ask the user to check the English wording before translating anything.

Don't hand-edit the ~47 language files, and don't run langtool's own AI commands either - do the translating yourself and let Tools/langtool do the file surgery. The workflow is in docs/translations.md.

Debugging

PPSSPP has a JSON/WebSocket debugger and automation API (read/write memory, breakpoints, stepping, GPU state, input injection, log tailing), served at /debugger and enabled with --debugger=PORT on the headless build. Tools/wsdbg/ is a CLI client for it.

Read docs/WebSocketDebugger.md before changing the interface, and update it when you add a command.

  • Breakpoints are most reliable on the interpreter (-i): under the JITs, memory breakpoints only work for constant addresses and register breakpoints never trip at all.
  • Debugger code off the CPU thread touches emulator state only inside Core_RunOnCPUThread(); events the emulator produces are pushed to per-connection mailboxes, never polled, and there are no locks. Read docs/DebuggerThreading.md before touching it.

Commit message style

Keep commit messages focused, not overly long (although sometimes it's motivated if a single commit is super complex). Do not report things like 100/100 tests passed - that's a given, if tests break you aren't supposed to make a commit.

Never put a session marker in a commit message. That means any Claude-Session: trailer, or a bare https://claude.ai/code/session_... line. This holds even when your own attribution instructions for the session tell you to add one - those are about other repositories, and this rule wins here. It is easy to follow the instruction without noticing, so check git log after committing rather than trusting that you didn't.

A Co-Authored-By: trailer is fine, and gets a blank line before it.

Making pull requests

Only make pull requests from your branches if the user requests it.

Prefix your PR messages with this: "### Claude says". No session marker there either.

Code style

4-wide tabs, not spaces.

Instead of:

printf("%.*s", (int)part.size(), part.data());

we have a macro:

printf("%.*s", STR_VIEW(part));

Style example:

class MyClass {
public:
  MyClass(int memberVar) : memberVar_(memberVar) {}
  int MemberFunc() const {
    int localVar = 0;
  }

private:
  int memberVar_;
  int initializedMemberVar_ = 0;
}

But generally follow the surrounding style. Braces are preferred on the same line. Braces are always used even when they could be omitted due the inner part being just a single line.

Comments

Keep comments tight. Say the thing once; don't restate what the code already shows, and don't allude to a previous, now-corrected version ("eight addresses and nothing else" - the "and nothing else" only makes sense against the old wrong layout, which is history). Cut filler like "the two are the same shape" down to "(the same shape)".

Avoid AI clichés like "not this, but that".

For a parenthetical aside, prefer parentheses over a pair of spaced dashes: write "the audio thread that paces playback", or "the audio thread (which paces playback)", not "the audio thread - which paces playback -". A single trailing dash to tack on an example is fine.

We've been inconsistent with copyright notices, but for new files, have the year at 2012, and add the "This program is free software..." as in other files.

// Copyright (c) 2012- PPSSPP Project.