{"owner":"hrydgard","repo":"ppsspp","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":7354,"categories":["root-instruction","mcp-config","marketplace","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"ppsspp":{"command":"npx","args":["-y","@modelcontextprotocol/server-ppsspp"]}}},"found":["AGENTS.md",".cursor/mcp.json",".claude-plugin/marketplace.json",".claude-plugin/plugin.json"],"skills":{"AGENTS.md":"# PPSSPP Agent instructions\n\nThese rules apply to this repository by default.\n\nIgnore the folder ai_instructions in the root directory, it's old stuff from contributors.\n\n## Detailed guides\n\nThe rules below are the short version. These docs hold the detail, look them up when the task calls\nfor it:\n\n| Doc | When you need it |\n|---|---|\n| [docs/building.md](docs/building.md) | Build commands for every target (VS/MSBuild, CMake, UWP, legacy Android NDK, libretro), unit tests, pspautotests |\n| [docs/android-device-testing.md](docs/android-device-testing.md) | Running a build on a real Android device or a Quest over adb: installing the build you think you are, forcing activity recreates, measuring retained activities, VR |\n| [docs/debugging.md](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`), finding a game's hot JIT blocks (`--jit-profile`), breakpoint reliability per CPU backend, debugging a game that works on hardware, finding and hashing a game function (like its own `memcpy`) for `ReplaceTables` when hardware backends miss CPU writes to VRAM |\n| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code |\n| [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in |\n| [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool |\n| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests, and running the tests that need the real firmware modules (`--disable-hle`, a firmware memstick) |\n| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |\n| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests, and replaying a dump on a real PSP |\n| [docs/sceGe.md](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 |\n| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |\n| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |\n| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does |\n| [docs/command-line.md](docs/command-line.md) | Adding a command-line option - the `g_autoParams` table, per-mode options, `ApplyToConfig()` |\n| [docs/patching-files.md](docs/patching-files.md) | Editing files from a script without wrecking the diff: line endings, and heredoc backslash escaping |\n| [docs/VSHBootInvestigation.md](docs/VSHBootInvestigation.md) | Booting the PSP's Visual Shell with `--vsh` against a real firmware dump |\n| [docs/POPS.md](docs/POPS.md) | PS1 EBOOTs: how the firmware's POPS manager and emulator are booted, and what's patched in them |\n| [docs/PsarFileFormat.md](docs/PsarFileFormat.md) | The PSAR archive inside a firmware updater - the format `Core/Util/PSARUnpack.cpp` walks |\n| [docs/pkg_notes.md](docs/pkg_notes.md) | The NPDRM `.pkg` format PSP game updates ship in, and how PPSSPP installs them |\n| [docs/cpu-equivalence.md](docs/cpu-equivalence.md) | Making the CPU cores agree bit for bit on every host: the `CPUEquivalence` fuzzer, what it found, and the differences still open (several waiting on a hardware test) |\n| [docs/dnas-patches.md](docs/dnas-patches.md) | Game patches installed on a real PSP (Killzone: Liberation's DLC): installing one over PSPLink, and the Fuse-ID-keyed PGD and type 3 PRX that `sceKernelLoadModuleDNAS` decrypts |\n| [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean |\n| [docs/shader-compilation.md](docs/shader-compilation.md) | Shader/pipeline compile stutter: where the time goes on MoltenVK (Metal's MSL compile, not glslang), the caches involved, how game-specific shaders are, and how to measure a cold start |\n| [docs/metal-backend.md](docs/metal-backend.md) | The Metal backend: shader translation, the render manager (recording, replay, pass merging), programmable blending, how it was tested against Vulkan, and what isn't done |\n| [docs/async-savestates.md](docs/async-savestates.md) | How savestate compression and file I/O run on a worker thread: the save/load/rewind pipelines, ordering, memory rules, what stays synchronous, and testing slots with the `game.state.*` debugger commands |\n\n## General instructions\n\n1. Keep style changes minimal unless requested. Follow existing code patterns and conventions.\n2. Keep cross-platform parity in mind when changing shared code. See below for more multiplatform tips\n3. **Commit freely, push only when asked.** Once a change is done and tested, commit it on its\n   branch without waiting to be told. Never `git push` (to any remote) without the user's explicit\n   approval. The exception is a useful new or re-recorded test in `pspautotests`, which goes\n   straight to its `master` without asking (see [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md#committing)).\n4. **Don't write code on `master`.** When asked to make a code change while on `master`, create an\n   appropriately named branch first (`git checkout -b some-descriptive-name`) and do the work there.\n   If you're already on a topic branch, just keep working on it.\n5. **Never assume a file's line endings - preserve whatever is on disk.** Which ending a file has\n   depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a\n   Linux checkout leaves files as they are stored, so the same file (`.vcxproj`, `.vcxproj.filters`,\n   `android/jni/Android.mk`, `libretro/Makefile.common`, this file, much of the source) is CRLF in one\n   working copy and LF in another. Don't hardcode either, and don't \"fix\" a file's endings to match\n   what a doc claims. If you patch one with a script, read *and* write with `newline=''`, which keeps\n   whatever was there; reading with Python's default universal-newline translation and writing with\n   `newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff.\n   Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in\n   the editor. Prefer the Edit tool, which does exact string replacement and can't do this.\n\n   **Don't verify this with `grep -c $'\\r$'`** - Git Bash's grep normalises line endings, so it counts\n   a bare-LF line as having a CR and reports any file as clean. The opposite mistake to a whole-file\n   rewrite is inserting a handful of LF lines into a CRLF file (a Python `\"\"\"...\"\"\"` block written with\n   `newline=''` does exactly this), and that shows in `--stat` only as the lines you meant to add. Check\n   the bytes instead, e.g. `python -c \"d=open(F,'rb').read(); print(d.count(b'\\r\\n'), d.count(b'\\n'))\"`,\n   and treat git's \"LF will be replaced by CRLF the next time Git touches it\" as the warning it is\n   rather than autocrlf noise.\n6. **Don't feed Python to `bash -c` via a heredoc when the code needs a literal backslash in its\n   *output*.** Git Bash strips one level of escaping on the way in even with a quoted delimiter\n   (`<<'PY'`), so `\"\\\\n\"` reaches Python as `\"\\n\"` and writes a real newline into the file - no error,\n   just a patch that quietly did nothing. Use the Edit tool, or write the script to a file and run it.\n   Details and the other two failure shapes: [docs/patching-files.md](docs/patching-files.md).\n\n7. **NEVER run more than one PSP hardware operation at a time.** There is one PSP. Every\n   `gentest.py`, `pspsh` or other PSPLink call gets its own message, containing that one tool call\n   and nothing else, and the next one starts only after the previous one has returned. Never put two\n   of them in the same message as parallel tool calls, never background one and start another, and\n   never loop over tests in one shell command. Re-recording a directory means one test, wait,\n   the next test. Concurrent runs collide in PSPLink and `host0:`: they time out, their results are\n   garbage, and the PSP wedges until someone resets it by hand. This has happened repeatedly; there\n   is no case where it is acceptable.\n8. **Update submodules when you start working.** Run `git submodule update --init --recursive`\n   before anything else, and again after switching or rebasing branches, so that builds and tests\n   use the submodule commits the branch records.\n\n## Core Safety Checks\n\n1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.\n2. Consider savestate compatibility when changing serialized state.\n3. **Never insert an entry into the middle of an `HLEFunction` array.** A savestate stores the\n   syscall opcode, which encodes the entry's *index* in that array - so inserting anywhere but the\n   end silently repoints every later entry, and old savestates start calling the wrong function.\n   This applies to adding a *single* function to an *existing* module, which is when it is easiest\n   to forget: put it last in the array even when alphabetical or NID order would put it elsewhere,\n   and even when the array is otherwise tidily sorted. The same rule governs the order of\n   `Register_*()` calls in `Core/HLE/HLETables.cpp` - new modules go at the very end.\n4. **Changing a shader cache key means bumping `CACHE_VERSION`, in both the OpenGL and Vulkan\n   caches** (`GPU/GLES/ShaderManagerGLES.cpp` and `GPU/Vulkan/ShaderManagerVulkan.cpp`). Their\n   on-disk caches store raw key bits with nothing to tell an old layout from a new one. OpenGL\n   stores `VShaderID`/`FShaderID`, and Vulkan stores those plus `VulkanPipelineKey` (the raster\n   state key and the decoded vertex format ID). So adding, removing, moving or reinterpreting any\n   of those bits needs the bump, or an old cache precompiles shaders for keys that now mean\n   something else, including combinations the generators assert on. D3D11 doesn't store shader IDs\n   on disk. Keys that only live in memory, like `SamplerCacheKey`, don't need a bump.\n5. **\"As on hardware\" needs a hardware test behind it.** A commit message or comment that says the\n   PSP does something names the pspautotests test that shows it, recorded before the change. And the\n   test has to reach the code path the change is about: when the HLE code handles the same call\n   differently by dialog type, mode or size, cover each of those. A dialog fix once claimed\n   hardware behaviour that the test recorded an hour later contradicted, and the test ran a message\n   dialog, which took a different branch from the savedata dialog that then hung Freak Out.\n\n## Build and validation\n\n- Linux/Mac: `./b.sh --debug` for a full configure+build; after that, `cd build ; make -j32; cd ..`.\n- Windows: always build through `Windows/PPSSPP.sln`, even if a stray CMake-generated `build/` directory\n  exists at the repo root. Drive it with `MSBuild.exe` (found via `vswhere.exe`) rather than the GUI:\n\n```powershell\n$installPath = & \"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -property installationPath\n$msbuild = \"$installPath\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n& $msbuild \"Windows\\PPSSPP.sln\" /t:UnitTest /p:Configuration=Debug /p:Platform=<platform> /m\n```\n\n- **`<platform>` is whatever the machine is - look it up, don't assume.** It is `ARM64` or `x64`, and\n  the build lands in `Windows\\<platform>\\<configuration>\\` to match, so building one and running the\n  other is easy to do without noticing. On Windows-on-ARM an x64 build runs anyway, under emulation,\n  which is what makes it easy to miss: it works, but it is slower than the native build, it is not\n  the code ARM users get, and any benchmark from it measures the emulator. Get the host from\n  `python -c \"import platform; print(platform.machine())\"`, not `$PROCESSOR_ARCHITECTURE`, which\n  describes the *shell* and says `AMD64` from an emulated one. The binaries say which they are too -\n  `UnitTest.exe` prints an `ABI:` line at startup.\n- Kill leftover `PPSSPPHeadless.exe`/`PPSSPP*.exe` instances before building - one holding the exe makes\n  the link fail with `LNK1168`, which looks like a build problem and isn't.\n- **A stale binary lies consistently.** After a `git stash` cycle that touched a header, do a\n  `/t:Rebuild`; when bisecting a behavioural change, confirm the binary actually changed before you\n  believe the result.\n- **If a savestate fails to load, first suspect the branch, not the loader.** Either your branch is\n  behind the build that made the state (rebase it on `origin/master` and rebuild), or the state was\n  made on a branch that hasn't been merged yet. Only once you've ruled out both is it a savestate\n  compatibility bug.\n\nUWP, the legacy Android NDK build and the libretro core have their own build systems.\n\n## Testing\n\nAfter a chunk of work (not after every edit), run both suites:\n\n- C++ unit tests: build the `UnitTest` project and run `Windows/<platform>/Debug/UnitTest.exe all`\n  (Linux/Mac: configure with `-DUNITTEST=ON`, run `build/PPSSPPUnitTest all`). Tests are listed in\n  `availableTests` in `unittest/UnitTest.cpp`; pass names instead of `all` to run a subset.\n- pspautotests (HLE coverage) - run them **exactly the way CI does**:\n\n```bash\npython test.py -g --graphics=software\n```\n\n  **The `-g` matters**: without it you also get `tests_next`, the expected-to-fail to-do list, and\n  around a hundred failures that mean nothing is wrong. The only meaningful result is `0 tests failed`.\n  (The debug-CRT \"Detected memory leaks!\" dump after the summary line is normal, not a failure.)\n\nWhen the thing under test is a commercial game rather than a suite, headless needs `--log` before it prints\nanything (to stderr). On Windows it defaults its memory stick to `<exe dir>/memstick` rather than the app's\n(on Mac and Linux it uses the app's default), so the firmware you installed in the app may not be there.\n`test.py` and `frametests.py` pass `<exe dir>/memstick` everywhere. Pass `--memstick=<the app's memstick>` for any run that is\nmeant to exercise a real firmware module. Without it, a library whose HLE the config has disabled has\nnothing left to resolve against, and the game does not fall back to our HLE - it gets unresolved imports and\ndies. The tell is a single line early on, `sceMpeg HLE is disabled, but flash0:/kd/mpeg.prx isn't in the\nfirmware and the game didn't bring its own`, followed by a run that logs happily for its whole timeout with\nzero `sceDisplaySetFramebuf` calls and a handful of cached textures. Count flips before concluding anything\nabout a game's behaviour. A run configured\ndifferently from what you asked for, or one that never reached the code, produces the same all-zero counts as\na clean one, so assert the exit code and a positive \"we got here\" counter before believing any error count.\nThe traps in full: [docs/debugging.md](docs/debugging.md).\n\nUnresolved-import errors early in a game run, for `scePsmfPlayer` in particular, are expected and not a\nproblem: many games ship libpsmfplayer (and similar libraries) on the disc, and the imports resolve when the\ngame loads the module. Don't change the setup (dropping `--memstick`, turning HLE back on) to make them go\naway; a run should be as close to a real one as possible, with firmware modules running as LLE.\n\nKeep game runs short and fast, so nobody has to watch them: always pass `--timeout-wall=30` (alongside any\n`--timeout-emulated`) unless there's a real reason for longer, use `--graphics=vulkan` rather than\n`--graphics=software` (if Vulkan doesn't work in headless, fix that), prefer a Release build, and leave out\ndebug-level `--log` unless you need it. Each of those can cost an order of magnitude: Outrun loading a state\ntook minutes with a Debug build, software rendering and a 300MB log, and about a second without them. To\ncheck that a game renders, `--screenshot-save=FILE.png` beats grepping the log. (pspautotests through\n`test.py` are different: they have their own per-test timeouts and use `--graphics=software` like CI.)\n\nTesting on a real Android device has its own traps, the worst of which is silent: `adb install -r`\nof a shorter branch over a longer one fails with `INSTALL_FAILED_VERSION_DOWNGRADE`, because\n`versionCode` is the commit count. The device keeps running the old build and every measurement after\nthat describes the wrong binary. Use `adb install -r -d`, don't swallow the install output, and check\n`adb shell dumpsys package org.ppsspp.ppsspp | grep versionName` afterwards. See\n[docs/android-device-testing.md](docs/android-device-testing.md).\n\nNew unit tests are added to `availableTests`; large ones go in their own file in `unittest/`, which has\nto be listed in **three** build files, not two: `CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its\n`.filters`), and `android/jni/Android.mk`, which builds a unit test executable of its own. Miss the last\none and it builds everywhere you can easily try it, and fails on Android CI.\n\n## Multiplatform considerations\n\nThe emulator has multiple platform-specific entry points. Some of these will be merged or removed in the future, but are all\nstill there. To verify that a change works, technically we need to compile for all these systems, but in practice we'll\njust compile locally and test the platform we are currently on, and let CI handle the cross platform considerations.\n\nSystem_-prefixed wrapper functions implement kind of a platform wrapper for some functionality, and are implemented in\nthe following list of files for each system. If we change one, we need to change them all.\n\nWindows/main.cpp\nios/main.cpp\nSDL/SDLMain.cpp\nUWP/PPSSPP_UWPMain.cpp\nandroid/jni/app-android.cpp\nlibretro/libretro.cpp\n\n## Reverse-engineering the firmware\n\nWhen a question about hardware behaviour can't be settled from the docs or from JPCSP - what a field in\na codec context means, what a library returns when a buffer runs dry - read the firmware.\n`PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR` loads one PRX standalone and writes an\nannotated disassembly, the export/import tables with NIDs resolved, and a call graph. It needs a\nfirmware dump (`--memstick` pointing at one; `--unpack-updater` can produce one).\n\nTwo things to know before trusting what you read there:\n\n- **Don't infer a function's arity from the registers it reads.** MIPS code routinely leaves an\n  argument untouched for a callee to pick up, so a function that reads only `a0` may well take\n  three. The per-function register evidence block flags this as `FORWARDED`; follow the callees.\n- **Record how you know.** A comment saying which module and function a fact came from is worth\n  more than the fact alone, since the next person can re-derive it. Behavioural findings belong\n  in the tree; bulk transcriptions of Sony's code do not.\n\n## Command-line parsing\n\nAll command-line parsing for both the main app and headless builds belongs in `Core/CmdLine.cpp` /\n`Core/CmdLine.h` (`CommandLineOptions`), not in the platform entry points (`Windows/main.cpp`,\n`headless/Headless.cpp`, `UI/NativeApp.cpp`, etc.). Don't re-parse `argv` manually in those files - add\na field to `CommandLineOptions`, and push it into `g_Config` from `ApplyToConfig()` so every platform\ngets it for free. How to declare one: [docs/command-line.md](docs/command-line.md).\n\n## File formats, codecs, and other format handlers\n\nBefore implementing any file format handler, decompressor, codec, or similar from scratch, search the\ncodebase first - PPSSPP already has implementations of many formats (CSO, LZRC, zlib-based loaders, ISO\nhandlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of\nwriting a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).\n\nFor string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.\n\n## Framedump rendering tests (frametests)\n\n`frametests.py` replays GE frame dumps (`.ppdmp`) through PPSSPPHeadless and compares the output against\nreference images, with a JSON config per test set. Consider running these when changing rendering code.\n\nNote: `headless/Compare.cpp` reads back framebuffers top-down; the flip to bottom-up is only applied when\nwriting BMPs (and when reading BMP references). `TranslateDebugBufferToCompare` also exists as a copy in\n`libretro/LibretroGraphicsContext.cpp` - keep the two in sync.\n\n## Adding HLE modules\n\nHLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h`, as a `const HLEFunction\n<name>[]` table registered via `RegisterHLEModule()`. Both the function table and the `Register_*()`\ncalls are append-only - see Core Safety Checks above, which is what breaks old savestates silently\nif ignored.\n\nAlso: a new `.cpp`/`.c` file has to be added to **seven** build files (CMake, Core.vcxproj + filters,\nthe two UWP projects, `android/jni/Android.mk`, `libretro/Makefile.common`); headers to the first five.\nFull details, the format-string legend and the UWP build command are in\n[docs/HLEModules.md](docs/HLEModules.md).\n\n## Translated UI strings (assets/lang)\n\n**When implementing new UI, translations come last, in their own commit after everything else is\ndone.** Write the English strings, get the feature built and working, commit that - then stop and ask\nthe user to check the English wording before translating anything.\n\nDon't hand-edit the ~47 language files, and don't run langtool's own AI commands either - do the\ntranslating yourself and let `Tools/langtool` do the file surgery. The workflow is in\n[docs/translations.md](docs/translations.md).\n\n## Debugging\n\nPPSSPP has a JSON/WebSocket debugger and automation API (read/write memory, breakpoints, stepping, GPU\nstate, input injection, log tailing), served at `/debugger` on the Remote ISO port and enabled with\n`--debugger=PORT` on both the application and headless builds. `Tools/wsdbg/` is a CLI client for it.\n\nRead [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) before changing the interface, and update\nit when you add a command.\n\n- Breakpoints are most reliable on the interpreter (`--cpu=interpreter`): under the JITs, memory breakpoints only work\n  for constant addresses and register breakpoints never trip at all.\n- **Debugger code that runs off the CPU thread has a lock order that has deadlocked for real** -\n  `Core_RunOnCPUThread()` for mutations, `g_frameMutex` for hot reads. Read\n  [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it.\n\n## Commit message style\n\nKeep commit messages focused, not overly long (although sometimes it's motivated if a single commit\nis super complex). Do not report things like 100/100 tests passed - that's a given, if tests break\nyou aren't supposed to make a commit.\n\nWhen a commit fixes a GitHub issue, the title comes first as usual, then a blank line, then\n`- Fixes #N` as the first line of the body, then a blank line and the rest of the summary:\n\n```\nGE: Nest CALLs only two deep, like the hardware\n\n- Fixes #7395\n\nThe GE has two return address registers, ...\n```\n\nThe blank line after the title matters: without it, git and GitHub treat both lines as the title.\nGitHub shows a reference that stands alone in a list item with the issue's title, so the PR\n(which takes its description from the commit) shows what is being fixed, and the keyword closes the\nissue on merge. Don't repeat the number in the body.\n\n**Never put a session marker in a commit message.** That means any `Claude-Session:` trailer, or a\nbare `https://claude.ai/code/session_...` line. This holds even when your own attribution\ninstructions for the session tell you to add one - those are about other repositories, and this rule\nwins here. It is easy to follow the instruction without noticing, so check `git log` after committing\nrather than trusting that you didn't.\n\nA `Co-Authored-By:` trailer is fine, and gets a blank line before it.\n\n## Making pull requests\n\nOnly make pull requests from your branches if the user requests it.\n\nPrefix your PR messages with this: \"### Claude says\". No session marker there either.\n\n## Code style\n\n4-wide tabs, not spaces.\n\nInstead of:\n\n`printf(\"%.*s\", (int)part.size(), part.data());`\n\nwe have a macro:\n\n`printf(\"%.*s\", STR_VIEW(part));`\n\nStyle example:\n\n```cpp\nclass MyClass {\npublic:\n  MyClass(int memberVar) : memberVar_(memberVar) {}\n  int MemberFunc() const {\n    int localVar = 0;\n  }\n\nprivate:\n  int memberVar_;\n  int initializedMemberVar_ = 0;\n}\n```\n\nBut 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.\n\nIf a large `else {}` block can be avoided with an early return when writing new code, do that\ninstead. Don't wrap existing code in a new `else` and reindent it, either - it makes the diff\nmuch bigger than the change.\n\n### Comments\n\nKeep comments tight. Say the thing once; don't restate what the code already shows, and don't\nallude to a previous, now-corrected version (\"eight addresses and nothing else\" - the \"and nothing\nelse\" only makes sense against the old wrong layout, which is history). Cut filler like \"the two\nare the same shape\" down to \"(the same shape)\".\n\nAvoid AI clichés like \"not this, but that\".\n\nFor a parenthetical aside, prefer parentheses over a pair of spaced dashes: write \"the audio thread\nthat paces playback\", or \"the audio thread (which paces playback)\", not \"the audio thread - which\npaces playback -\". A single trailing dash to tack on an example is fine.\n\nWe'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.\n\n`// Copyright (c) 2012- PPSSPP Project.`\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/plugin.json)"},"files":{"AGENTS.md":"# PPSSPP Agent instructions\n\nThese rules apply to this repository by default.\n\nIgnore the folder ai_instructions in the root directory, it's old stuff from contributors.\n\n## Detailed guides\n\nThe rules below are the short version. These docs hold the detail, look them up when the task calls\nfor it:\n\n| Doc | When you need it |\n|---|---|\n| [docs/building.md](docs/building.md) | Build commands for every target (VS/MSBuild, CMake, UWP, legacy Android NDK, libretro), unit tests, pspautotests |\n| [docs/android-device-testing.md](docs/android-device-testing.md) | Running a build on a real Android device or a Quest over adb: installing the build you think you are, forcing activity recreates, measuring retained activities, VR |\n| [docs/debugging.md](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`), finding a game's hot JIT blocks (`--jit-profile`), breakpoint reliability per CPU backend, debugging a game that works on hardware, finding and hashing a game function (like its own `memcpy`) for `ReplaceTables` when hardware backends miss CPU writes to VRAM |\n| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code |\n| [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in |\n| [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool |\n| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests, and running the tests that need the real firmware modules (`--disable-hle`, a firmware memstick) |\n| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |\n| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests, and replaying a dump on a real PSP |\n| [docs/sceGe.md](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 |\n| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |\n| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |\n| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does |\n| [docs/command-line.md](docs/command-line.md) | Adding a command-line option - the `g_autoParams` table, per-mode options, `ApplyToConfig()` |\n| [docs/patching-files.md](docs/patching-files.md) | Editing files from a script without wrecking the diff: line endings, and heredoc backslash escaping |\n| [docs/VSHBootInvestigation.md](docs/VSHBootInvestigation.md) | Booting the PSP's Visual Shell with `--vsh` against a real firmware dump |\n| [docs/POPS.md](docs/POPS.md) | PS1 EBOOTs: how the firmware's POPS manager and emulator are booted, and what's patched in them |\n| [docs/PsarFileFormat.md](docs/PsarFileFormat.md) | The PSAR archive inside a firmware updater - the format `Core/Util/PSARUnpack.cpp` walks |\n| [docs/pkg_notes.md](docs/pkg_notes.md) | The NPDRM `.pkg` format PSP game updates ship in, and how PPSSPP installs them |\n| [docs/cpu-equivalence.md](docs/cpu-equivalence.md) | Making the CPU cores agree bit for bit on every host: the `CPUEquivalence` fuzzer, what it found, and the differences still open (several waiting on a hardware test) |\n| [docs/dnas-patches.md](docs/dnas-patches.md) | Game patches installed on a real PSP (Killzone: Liberation's DLC): installing one over PSPLink, and the Fuse-ID-keyed PGD and type 3 PRX that `sceKernelLoadModuleDNAS` decrypts |\n| [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean |\n| [docs/shader-compilation.md](docs/shader-compilation.md) | Shader/pipeline compile stutter: where the time goes on MoltenVK (Metal's MSL compile, not glslang), the caches involved, how game-specific shaders are, and how to measure a cold start |\n| [docs/metal-backend.md](docs/metal-backend.md) | The Metal backend: shader translation, the render manager (recording, replay, pass merging), programmable blending, how it was tested against Vulkan, and what isn't done |\n| [docs/async-savestates.md](docs/async-savestates.md) | How savestate compression and file I/O run on a worker thread: the save/load/rewind pipelines, ordering, memory rules, what stays synchronous, and testing slots with the `game.state.*` debugger commands |\n\n## General instructions\n\n1. Keep style changes minimal unless requested. Follow existing code patterns and conventions.\n2. Keep cross-platform parity in mind when changing shared code. See below for more multiplatform tips\n3. **Commit freely, push only when asked.** Once a change is done and tested, commit it on its\n   branch without waiting to be told. Never `git push` (to any remote) without the user's explicit\n   approval. The exception is a useful new or re-recorded test in `pspautotests`, which goes\n   straight to its `master` without asking (see [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md#committing)).\n4. **Don't write code on `master`.** When asked to make a code change while on `master`, create an\n   appropriately named branch first (`git checkout -b some-descriptive-name`) and do the work there.\n   If you're already on a topic branch, just keep working on it.\n5. **Never assume a file's line endings - preserve whatever is on disk.** Which ending a file has\n   depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a\n   Linux checkout leaves files as they are stored, so the same file (`.vcxproj`, `.vcxproj.filters`,\n   `android/jni/Android.mk`, `libretro/Makefile.common`, this file, much of the source) is CRLF in one\n   working copy and LF in another. Don't hardcode either, and don't \"fix\" a file's endings to match\n   what a doc claims. If you patch one with a script, read *and* write with `newline=''`, which keeps\n   whatever was there; reading with Python's default universal-newline translation and writing with\n   `newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff.\n   Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in\n   the editor. Prefer the Edit tool, which does exact string replacement and can't do this.\n\n   **Don't verify this with `grep -c $'\\r$'`** - Git Bash's grep normalises line endings, so it counts\n   a bare-LF line as having a CR and reports any file as clean. The opposite mistake to a whole-file\n   rewrite is inserting a handful of LF lines into a CRLF file (a Python `\"\"\"...\"\"\"` block written with\n   `newline=''` does exactly this), and that shows in `--stat` only as the lines you meant to add. Check\n   the bytes instead, e.g. `python -c \"d=open(F,'rb').read(); print(d.count(b'\\r\\n'), d.count(b'\\n'))\"`,\n   and treat git's \"LF will be replaced by CRLF the next time Git touches it\" as the warning it is\n   rather than autocrlf noise.\n6. **Don't feed Python to `bash -c` via a heredoc when the code needs a literal backslash in its\n   *output*.** Git Bash strips one level of escaping on the way in even with a quoted delimiter\n   (`<<'PY'`), so `\"\\\\n\"` reaches Python as `\"\\n\"` and writes a real newline into the file - no error,\n   just a patch that quietly did nothing. Use the Edit tool, or write the script to a file and run it.\n   Details and the other two failure shapes: [docs/patching-files.md](docs/patching-files.md).\n\n7. **NEVER run more than one PSP hardware operation at a time.** There is one PSP. Every\n   `gentest.py`, `pspsh` or other PSPLink call gets its own message, containing that one tool call\n   and nothing else, and the next one starts only after the previous one has returned. Never put two\n   of them in the same message as parallel tool calls, never background one and start another, and\n   never loop over tests in one shell command. Re-recording a directory means one test, wait,\n   the next test. Concurrent runs collide in PSPLink and `host0:`: they time out, their results are\n   garbage, and the PSP wedges until someone resets it by hand. This has happened repeatedly; there\n   is no case where it is acceptable.\n8. **Update submodules when you start working.** Run `git submodule update --init --recursive`\n   before anything else, and again after switching or rebasing branches, so that builds and tests\n   use the submodule commits the branch records.\n\n## Core Safety Checks\n\n1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.\n2. Consider savestate compatibility when changing serialized state.\n3. **Never insert an entry into the middle of an `HLEFunction` array.** A savestate stores the\n   syscall opcode, which encodes the entry's *index* in that array - so inserting anywhere but the\n   end silently repoints every later entry, and old savestates start calling the wrong function.\n   This applies to adding a *single* function to an *existing* module, which is when it is easiest\n   to forget: put it last in the array even when alphabetical or NID order would put it elsewhere,\n   and even when the array is otherwise tidily sorted. The same rule governs the order of\n   `Register_*()` calls in `Core/HLE/HLETables.cpp` - new modules go at the very end.\n4. **Changing a shader cache key means bumping `CACHE_VERSION`, in both the OpenGL and Vulkan\n   caches** (`GPU/GLES/ShaderManagerGLES.cpp` and `GPU/Vulkan/ShaderManagerVulkan.cpp`). Their\n   on-disk caches store raw key bits with nothing to tell an old layout from a new one. OpenGL\n   stores `VShaderID`/`FShaderID`, and Vulkan stores those plus `VulkanPipelineKey` (the raster\n   state key and the decoded vertex format ID). So adding, removing, moving or reinterpreting any\n   of those bits needs the bump, or an old cache precompiles shaders for keys that now mean\n   something else, including combinations the generators assert on. D3D11 doesn't store shader IDs\n   on disk. Keys that only live in memory, like `SamplerCacheKey`, don't need a bump.\n5. **\"As on hardware\" needs a hardware test behind it.** A commit message or comment that says the\n   PSP does something names the pspautotests test that shows it, recorded before the change. And the\n   test has to reach the code path the change is about: when the HLE code handles the same call\n   differently by dialog type, mode or size, cover each of those. A dialog fix once claimed\n   hardware behaviour that the test recorded an hour later contradicted, and the test ran a message\n   dialog, which took a different branch from the savedata dialog that then hung Freak Out.\n\n## Build and validation\n\n- Linux/Mac: `./b.sh --debug` for a full configure+build; after that, `cd build ; make -j32; cd ..`.\n- Windows: always build through `Windows/PPSSPP.sln`, even if a stray CMake-generated `build/` directory\n  exists at the repo root. Drive it with `MSBuild.exe` (found via `vswhere.exe`) rather than the GUI:\n\n```powershell\n$installPath = & \"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -property installationPath\n$msbuild = \"$installPath\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n& $msbuild \"Windows\\PPSSPP.sln\" /t:UnitTest /p:Configuration=Debug /p:Platform=<platform> /m\n```\n\n- **`<platform>` is whatever the machine is - look it up, don't assume.** It is `ARM64` or `x64`, and\n  the build lands in `Windows\\<platform>\\<configuration>\\` to match, so building one and running the\n  other is easy to do without noticing. On Windows-on-ARM an x64 build runs anyway, under emulation,\n  which is what makes it easy to miss: it works, but it is slower than the native build, it is not\n  the code ARM users get, and any benchmark from it measures the emulator. Get the host from\n  `python -c \"import platform; print(platform.machine())\"`, not `$PROCESSOR_ARCHITECTURE`, which\n  describes the *shell* and says `AMD64` from an emulated one. The binaries say which they are too -\n  `UnitTest.exe` prints an `ABI:` line at startup.\n- Kill leftover `PPSSPPHeadless.exe`/`PPSSPP*.exe` instances before building - one holding the exe makes\n  the link fail with `LNK1168`, which looks like a build problem and isn't.\n- **A stale binary lies consistently.** After a `git stash` cycle that touched a header, do a\n  `/t:Rebuild`; when bisecting a behavioural change, confirm the binary actually changed before you\n  believe the result.\n- **If a savestate fails to load, first suspect the branch, not the loader.** Either your branch is\n  behind the build that made the state (rebase it on `origin/master` and rebuild), or the state was\n  made on a branch that hasn't been merged yet. Only once you've ruled out both is it a savestate\n  compatibility bug.\n\nUWP, the legacy Android NDK build and the libretro core have their own build systems.\n\n## Testing\n\nAfter a chunk of work (not after every edit), run both suites:\n\n- C++ unit tests: build the `UnitTest` project and run `Windows/<platform>/Debug/UnitTest.exe all`\n  (Linux/Mac: configure with `-DUNITTEST=ON`, run `build/PPSSPPUnitTest all`). Tests are listed in\n  `availableTests` in `unittest/UnitTest.cpp`; pass names instead of `all` to run a subset.\n- pspautotests (HLE coverage) - run them **exactly the way CI does**:\n\n```bash\npython test.py -g --graphics=software\n```\n\n  **The `-g` matters**: without it you also get `tests_next`, the expected-to-fail to-do list, and\n  around a hundred failures that mean nothing is wrong. The only meaningful result is `0 tests failed`.\n  (The debug-CRT \"Detected memory leaks!\" dump after the summary line is normal, not a failure.)\n\nWhen the thing under test is a commercial game rather than a suite, headless needs `--log` before it prints\nanything (to stderr). On Windows it defaults its memory stick to `<exe dir>/memstick` rather than the app's\n(on Mac and Linux it uses the app's default), so the firmware you installed in the app may not be there.\n`test.py` and `frametests.py` pass `<exe dir>/memstick` everywhere. Pass `--memstick=<the app's memstick>` for any run that is\nmeant to exercise a real firmware module. Without it, a library whose HLE the config has disabled has\nnothing left to resolve against, and the game does not fall back to our HLE - it gets unresolved imports and\ndies. The tell is a single line early on, `sceMpeg HLE is disabled, but flash0:/kd/mpeg.prx isn't in the\nfirmware and the game didn't bring its own`, followed by a run that logs happily for its whole timeout with\nzero `sceDisplaySetFramebuf` calls and a handful of cached textures. Count flips before concluding anything\nabout a game's behaviour. A run configured\ndifferently from what you asked for, or one that never reached the code, produces the same all-zero counts as\na clean one, so assert the exit code and a positive \"we got here\" counter before believing any error count.\nThe traps in full: [docs/debugging.md](docs/debugging.md).\n\nUnresolved-import errors early in a game run, for `scePsmfPlayer` in particular, are expected and not a\nproblem: many games ship libpsmfplayer (and similar libraries) on the disc, and the imports resolve when the\ngame loads the module. Don't change the setup (dropping `--memstick`, turning HLE back on) to make them go\naway; a run should be as close to a real one as possible, with firmware modules running as LLE.\n\nKeep game runs short and fast, so nobody has to watch them: always pass `--timeout-wall=30` (alongside any\n`--timeout-emulated`) unless there's a real reason for longer, use `--graphics=vulkan` rather than\n`--graphics=software` (if Vulkan doesn't work in headless, fix that), prefer a Release build, and leave out\ndebug-level `--log` unless you need it. Each of those can cost an order of magnitude: Outrun loading a state\ntook minutes with a Debug build, software rendering and a 300MB log, and about a second without them. To\ncheck that a game renders, `--screenshot-save=FILE.png` beats grepping the log. (pspautotests through\n`test.py` are different: they have their own per-test timeouts and use `--graphics=software` like CI.)\n\nTesting on a real Android device has its own traps, the worst of which is silent: `adb install -r`\nof a shorter branch over a longer one fails with `INSTALL_FAILED_VERSION_DOWNGRADE`, because\n`versionCode` is the commit count. The device keeps running the old build and every measurement after\nthat describes the wrong binary. Use `adb install -r -d`, don't swallow the install output, and check\n`adb shell dumpsys package org.ppsspp.ppsspp | grep versionName` afterwards. See\n[docs/android-device-testing.md](docs/android-device-testing.md).\n\nNew unit tests are added to `availableTests`; large ones go in their own file in `unittest/`, which has\nto be listed in **three** build files, not two: `CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its\n`.filters`), and `android/jni/Android.mk`, which builds a unit test executable of its own. Miss the last\none and it builds everywhere you can easily try it, and fails on Android CI.\n\n## Multiplatform considerations\n\nThe emulator has multiple platform-specific entry points. Some of these will be merged or removed in the future, but are all\nstill there. To verify that a change works, technically we need to compile for all these systems, but in practice we'll\njust compile locally and test the platform we are currently on, and let CI handle the cross platform considerations.\n\nSystem_-prefixed wrapper functions implement kind of a platform wrapper for some functionality, and are implemented in\nthe following list of files for each system. If we change one, we need to change them all.\n\nWindows/main.cpp\nios/main.cpp\nSDL/SDLMain.cpp\nUWP/PPSSPP_UWPMain.cpp\nandroid/jni/app-android.cpp\nlibretro/libretro.cpp\n\n## Reverse-engineering the firmware\n\nWhen a question about hardware behaviour can't be settled from the docs or from JPCSP - what a field in\na codec context means, what a library returns when a buffer runs dry - read the firmware.\n`PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR` loads one PRX standalone and writes an\nannotated disassembly, the export/import tables with NIDs resolved, and a call graph. It needs a\nfirmware dump (`--memstick` pointing at one; `--unpack-updater` can produce one).\n\nTwo things to know before trusting what you read there:\n\n- **Don't infer a function's arity from the registers it reads.** MIPS code routinely leaves an\n  argument untouched for a callee to pick up, so a function that reads only `a0` may well take\n  three. The per-function register evidence block flags this as `FORWARDED`; follow the callees.\n- **Record how you know.** A comment saying which module and function a fact came from is worth\n  more than the fact alone, since the next person can re-derive it. Behavioural findings belong\n  in the tree; bulk transcriptions of Sony's code do not.\n\n## Command-line parsing\n\nAll command-line parsing for both the main app and headless builds belongs in `Core/CmdLine.cpp` /\n`Core/CmdLine.h` (`CommandLineOptions`), not in the platform entry points (`Windows/main.cpp`,\n`headless/Headless.cpp`, `UI/NativeApp.cpp`, etc.). Don't re-parse `argv` manually in those files - add\na field to `CommandLineOptions`, and push it into `g_Config` from `ApplyToConfig()` so every platform\ngets it for free. How to declare one: [docs/command-line.md](docs/command-line.md).\n\n## File formats, codecs, and other format handlers\n\nBefore implementing any file format handler, decompressor, codec, or similar from scratch, search the\ncodebase first - PPSSPP already has implementations of many formats (CSO, LZRC, zlib-based loaders, ISO\nhandlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of\nwriting a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).\n\nFor string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.\n\n## Framedump rendering tests (frametests)\n\n`frametests.py` replays GE frame dumps (`.ppdmp`) through PPSSPPHeadless and compares the output against\nreference images, with a JSON config per test set. Consider running these when changing rendering code.\n\nNote: `headless/Compare.cpp` reads back framebuffers top-down; the flip to bottom-up is only applied when\nwriting BMPs (and when reading BMP references). `TranslateDebugBufferToCompare` also exists as a copy in\n`libretro/LibretroGraphicsContext.cpp` - keep the two in sync.\n\n## Adding HLE modules\n\nHLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h`, as a `const HLEFunction\n<name>[]` table registered via `RegisterHLEModule()`. Both the function table and the `Register_*()`\ncalls are append-only - see Core Safety Checks above, which is what breaks old savestates silently\nif ignored.\n\nAlso: a new `.cpp`/`.c` file has to be added to **seven** build files (CMake, Core.vcxproj + filters,\nthe two UWP projects, `android/jni/Android.mk`, `libretro/Makefile.common`); headers to the first five.\nFull details, the format-string legend and the UWP build command are in\n[docs/HLEModules.md](docs/HLEModules.md).\n\n## Translated UI strings (assets/lang)\n\n**When implementing new UI, translations come last, in their own commit after everything else is\ndone.** Write the English strings, get the feature built and working, commit that - then stop and ask\nthe user to check the English wording before translating anything.\n\nDon't hand-edit the ~47 language files, and don't run langtool's own AI commands either - do the\ntranslating yourself and let `Tools/langtool` do the file surgery. The workflow is in\n[docs/translations.md](docs/translations.md).\n\n## Debugging\n\nPPSSPP has a JSON/WebSocket debugger and automation API (read/write memory, breakpoints, stepping, GPU\nstate, input injection, log tailing), served at `/debugger` on the Remote ISO port and enabled with\n`--debugger=PORT` on both the application and headless builds. `Tools/wsdbg/` is a CLI client for it.\n\nRead [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) before changing the interface, and update\nit when you add a command.\n\n- Breakpoints are most reliable on the interpreter (`--cpu=interpreter`): under the JITs, memory breakpoints only work\n  for constant addresses and register breakpoints never trip at all.\n- **Debugger code that runs off the CPU thread has a lock order that has deadlocked for real** -\n  `Core_RunOnCPUThread()` for mutations, `g_frameMutex` for hot reads. Read\n  [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it.\n\n## Commit message style\n\nKeep commit messages focused, not overly long (although sometimes it's motivated if a single commit\nis super complex). Do not report things like 100/100 tests passed - that's a given, if tests break\nyou aren't supposed to make a commit.\n\nWhen a commit fixes a GitHub issue, the title comes first as usual, then a blank line, then\n`- Fixes #N` as the first line of the body, then a blank line and the rest of the summary:\n\n```\nGE: Nest CALLs only two deep, like the hardware\n\n- Fixes #7395\n\nThe GE has two return address registers, ...\n```\n\nThe blank line after the title matters: without it, git and GitHub treat both lines as the title.\nGitHub shows a reference that stands alone in a list item with the issue's title, so the PR\n(which takes its description from the commit) shows what is being fixed, and the keyword closes the\nissue on merge. Don't repeat the number in the body.\n\n**Never put a session marker in a commit message.** That means any `Claude-Session:` trailer, or a\nbare `https://claude.ai/code/session_...` line. This holds even when your own attribution\ninstructions for the session tell you to add one - those are about other repositories, and this rule\nwins here. It is easy to follow the instruction without noticing, so check `git log` after committing\nrather than trusting that you didn't.\n\nA `Co-Authored-By:` trailer is fine, and gets a blank line before it.\n\n## Making pull requests\n\nOnly make pull requests from your branches if the user requests it.\n\nPrefix your PR messages with this: \"### Claude says\". No session marker there either.\n\n## Code style\n\n4-wide tabs, not spaces.\n\nInstead of:\n\n`printf(\"%.*s\", (int)part.size(), part.data());`\n\nwe have a macro:\n\n`printf(\"%.*s\", STR_VIEW(part));`\n\nStyle example:\n\n```cpp\nclass MyClass {\npublic:\n  MyClass(int memberVar) : memberVar_(memberVar) {}\n  int MemberFunc() const {\n    int localVar = 0;\n  }\n\nprivate:\n  int memberVar_;\n  int initializedMemberVar_ = 0;\n}\n```\n\nBut 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.\n\nIf a large `else {}` block can be avoided with an early return when writing new code, do that\ninstead. Don't wrap existing code in a new `else` and reindent it, either - it makes the diff\nmuch bigger than the change.\n\n### Comments\n\nKeep comments tight. Say the thing once; don't restate what the code already shows, and don't\nallude to a previous, now-corrected version (\"eight addresses and nothing else\" - the \"and nothing\nelse\" only makes sense against the old wrong layout, which is history). Cut filler like \"the two\nare the same shape\" down to \"(the same shape)\".\n\nAvoid AI clichés like \"not this, but that\".\n\nFor a parenthetical aside, prefer parentheses over a pair of spaced dashes: write \"the audio thread\nthat paces playback\", or \"the audio thread (which paces playback)\", not \"the audio thread - which\npaces playback -\". A single trailing dash to tack on an example is fine.\n\nWe'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.\n\n`// Copyright (c) 2012- PPSSPP Project.`\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/plugin.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# PPSSPP Agent instructions\n\nThese rules apply to this repository by default.\n\nIgnore the folder ai_instructions in the root directory, it's old stuff from contributors.\n\n## Detailed guides\n\nThe rules below are the short version. These docs hold the detail, look them up when the task calls\nfor it:\n\n| Doc | When you need it |\n|---|---|\n| [docs/building.md](docs/building.md) | Build commands for every target (VS/MSBuild, CMake, UWP, legacy Android NDK, libretro), unit tests, pspautotests |\n| [docs/android-device-testing.md](docs/android-device-testing.md) | Running a build on a real Android device or a Quest over adb: installing the build you think you are, forcing activity recreates, measuring retained activities, VR |\n| [docs/debugging.md](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`), finding a game's hot JIT blocks (`--jit-profile`), breakpoint reliability per CPU backend, debugging a game that works on hardware, finding and hashing a game function (like its own `memcpy`) for `ReplaceTables` when hardware backends miss CPU writes to VRAM |\n| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code |\n| [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in |\n| [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool |\n| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests, and running the tests that need the real firmware modules (`--disable-hle`, a firmware memstick) |\n| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |\n| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests, and replaying a dump on a real PSP |\n| [docs/sceGe.md](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 |\n| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |\n| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |\n| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does |\n| [docs/command-line.md](docs/command-line.md) | Adding a command-line option - the `g_autoParams` table, per-mode options, `ApplyToConfig()` |\n| [docs/patching-files.md](docs/patching-files.md) | Editing files from a script without wrecking the diff: line endings, and heredoc backslash escaping |\n| [docs/VSHBootInvestigation.md](docs/VSHBootInvestigation.md) | Booting the PSP's Visual Shell with `--vsh` against a real firmware dump |\n| [docs/POPS.md](docs/POPS.md) | PS1 EBOOTs: how the firmware's POPS manager and emulator are booted, and what's patched in them |\n| [docs/PsarFileFormat.md](docs/PsarFileFormat.md) | The PSAR archive inside a firmware updater - the format `Core/Util/PSARUnpack.cpp` walks |\n| [docs/pkg_notes.md](docs/pkg_notes.md) | The NPDRM `.pkg` format PSP game updates ship in, and how PPSSPP installs them |\n| [docs/cpu-equivalence.md](docs/cpu-equivalence.md) | Making the CPU cores agree bit for bit on every host: the `CPUEquivalence` fuzzer, what it found, and the differences still open (several waiting on a hardware test) |\n| [docs/dnas-patches.md](docs/dnas-patches.md) | Game patches installed on a real PSP (Killzone: Liberation's DLC): installing one over PSPLink, and the Fuse-ID-keyed PGD and type 3 PRX that `sceKernelLoadModuleDNAS` decrypts |\n| [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean |\n| [docs/shader-compilation.md](docs/shader-compilation.md) | Shader/pipeline compile stutter: where the time goes on MoltenVK (Metal's MSL compile, not glslang), the caches involved, how game-specific shaders are, and how to measure a cold start |\n| [docs/metal-backend.md](docs/metal-backend.md) | The Metal backend: shader translation, the render manager (recording, replay, pass merging), programmable blending, how it was tested against Vulkan, and what isn't done |\n| [docs/async-savestates.md](docs/async-savestates.md) | How savestate compression and file I/O run on a worker thread: the save/load/rewind pipelines, ordering, memory rules, what stays synchronous, and testing slots with the `game.state.*` debugger commands |\n\n## General instructions\n\n1. Keep style changes minimal unless requested. Follow existing code patterns and conventions.\n2. Keep cross-platform parity in mind when changing shared code. See below for more multiplatform tips\n3. **Commit freely, push only when asked.** Once a change is done and tested, commit it on its\n   branch without waiting to be told. Never `git push` (to any remote) without the user's explicit\n   approval. The exception is a useful new or re-recorded test in `pspautotests`, which goes\n   straight to its `master` without asking (see [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md#committing)).\n4. **Don't write code on `master`.** When asked to make a code change while on `master`, create an\n   appropriately named branch first (`git checkout -b some-descriptive-name`) and do the work there.\n   If you're already on a topic branch, just keep working on it.\n5. **Never assume a file's line endings - preserve whatever is on disk.** Which ending a file has\n   depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a\n   Linux checkout leaves files as they are stored, so the same file (`.vcxproj`, `.vcxproj.filters`,\n   `android/jni/Android.mk`, `libretro/Makefile.common`, this file, much of the source) is CRLF in one\n   working copy and LF in another. Don't hardcode either, and don't \"fix\" a file's endings to match\n   what a doc claims. If you patch one with a script, read *and* write with `newline=''`, which keeps\n   whatever was there; reading with Python's default universal-newline translation and writing with\n   `newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff.\n   Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in\n   the editor. Prefer the Edit tool, which does exact string replacement and can't do this.\n\n   **Don't verify this with `grep -c $'\\r$'`** - Git Bash's grep normalises line endings, so it counts\n   a bare-LF line as having a CR and reports any file as clean. The opposite mistake to a whole-file\n   rewrite is inserting a handful of LF lines into a CRLF file (a Python `\"\"\"...\"\"\"` block written with\n   `newline=''` does exactly this), and that shows in `--stat` only as the lines you meant to add. Check\n   the bytes instead, e.g. `python -c \"d=open(F,'rb').read(); print(d.count(b'\\r\\n'), d.count(b'\\n'))\"`,\n   and treat git's \"LF will be replaced by CRLF the next time Git touches it\" as the warning it is\n   rather than autocrlf noise.\n6. **Don't feed Python to `bash -c` via a heredoc when the code needs a literal backslash in its\n   *output*.** Git Bash strips one level of escaping on the way in even with a quoted delimiter\n   (`<<'PY'`), so `\"\\\\n\"` reaches Python as `\"\\n\"` and writes a real newline into the file - no error,\n   just a patch that quietly did nothing. Use the Edit tool, or write the script to a file and run it.\n   Details and the other two failure shapes: [docs/patching-files.md](docs/patching-files.md).\n\n7. **NEVER run more than one PSP hardware operation at a time.** There is one PSP. Every\n   `gentest.py`, `pspsh` or other PSPLink call gets its own message, containing that one tool call\n   and nothing else, and the next one starts only after the previous one has returned. Never put two\n   of them in the same message as parallel tool calls, never background one and start another, and\n   never loop over tests in one shell command. Re-recording a directory means one test, wait,\n   the next test. Concurrent runs collide in PSPLink and `host0:`: they time out, their results are\n   garbage, and the PSP wedges until someone resets it by hand. This has happened repeatedly; there\n   is no case where it is acceptable.\n8. **Update submodules when you start working.** Run `git submodule update --init --recursive`\n   before anything else, and again after switching or rebasing branches, so that builds and tests\n   use the submodule commits the branch records.\n\n## Core Safety Checks\n\n1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.\n2. Consider savestate compatibility when changing serialized state.\n3. **Never insert an entry into the middle of an `HLEFunction` array.** A savestate stores the\n   syscall opcode, which encodes the entry's *index* in that array - so inserting anywhere but the\n   end silently repoints every later entry, and old savestates start calling the wrong function.\n   This applies to adding a *single* function to an *existing* module, which is when it is easiest\n   to forget: put it last in the array even when alphabetical or NID order would put it elsewhere,\n   and even when the array is otherwise tidily sorted. The same rule governs the order of\n   `Register_*()` calls in `Core/HLE/HLETables.cpp` - new modules go at the very end.\n4. **Changing a shader cache key means bumping `CACHE_VERSION`, in both the OpenGL and Vulkan\n   caches** (`GPU/GLES/ShaderManagerGLES.cpp` and `GPU/Vulkan/ShaderManagerVulkan.cpp`). Their\n   on-disk caches store raw key bits with nothing to tell an old layout from a new one. OpenGL\n   stores `VShaderID`/`FShaderID`, and Vulkan stores those plus `VulkanPipelineKey` (the raster\n   state key and the decoded vertex format ID). So adding, removing, moving or reinterpreting any\n   of those bits needs the bump, or an old cache precompiles shaders for keys that now mean\n   something else, including combinations the generators assert on. D3D11 doesn't store shader IDs\n   on disk. Keys that only live in memory, like `SamplerCacheKey`, don't need a bump.\n5. **\"As on hardware\" needs a hardware test behind it.** A commit message or comment that says the\n   PSP does something names the pspautotests test that shows it, recorded before the change. And the\n   test has to reach the code path the change is about: when the HLE code handles the same call\n   differently by dialog type, mode or size, cover each of those. A dialog fix once claimed\n   hardware behaviour that the test recorded an hour later contradicted, and the test ran a message\n   dialog, which took a different branch from the savedata dialog that then hung Freak Out.\n\n## Build and validation\n\n- Linux/Mac: `./b.sh --debug` for a full configure+build; after that, `cd build ; make -j32; cd ..`.\n- Windows: always build through `Windows/PPSSPP.sln`, even if a stray CMake-generated `build/` directory\n  exists at the repo root. Drive it with `MSBuild.exe` (found via `vswhere.exe`) rather than the GUI:\n\n```powershell\n$installPath = & \"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -property installationPath\n$msbuild = \"$installPath\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n& $msbuild \"Windows\\PPSSPP.sln\" /t:UnitTest /p:Configuration=Debug /p:Platform=<platform> /m\n```\n\n- **`<platform>` is whatever the machine is - look it up, don't assume.** It is `ARM64` or `x64`, and\n  the build lands in `Windows\\<platform>\\<configuration>\\` to match, so building one and running the\n  other is easy to do without noticing. On Windows-on-ARM an x64 build runs anyway, under emulation,\n  which is what makes it easy to miss: it works, but it is slower than the native build, it is not\n  the code ARM users get, and any benchmark from it measures the emulator. Get the host from\n  `python -c \"import platform; print(platform.machine())\"`, not `$PROCESSOR_ARCHITECTURE`, which\n  describes the *shell* and says `AMD64` from an emulated one. The binaries say which they are too -\n  `UnitTest.exe` prints an `ABI:` line at startup.\n- Kill leftover `PPSSPPHeadless.exe`/`PPSSPP*.exe` instances before building - one holding the exe makes\n  the link fail with `LNK1168`, which looks like a build problem and isn't.\n- **A stale binary lies consistently.** After a `git stash` cycle that touched a header, do a\n  `/t:Rebuild`; when bisecting a behavioural change, confirm the binary actually changed before you\n  believe the result.\n- **If a savestate fails to load, first suspect the branch, not the loader.** Either your branch is\n  behind the build that made the state (rebase it on `origin/master` and rebuild), or the state was\n  made on a branch that hasn't been merged yet. Only once you've ruled out both is it a savestate\n  compatibility bug.\n\nUWP, the legacy Android NDK build and the libretro core have their own build systems.\n\n## Testing\n\nAfter a chunk of work (not after every edit), run both suites:\n\n- C++ unit tests: build the `UnitTest` project and run `Windows/<platform>/Debug/UnitTest.exe all`\n  (Linux/Mac: configure with `-DUNITTEST=ON`, run `build/PPSSPPUnitTest all`). Tests are listed in\n  `availableTests` in `unittest/UnitTest.cpp`; pass names instead of `all` to run a subset.\n- pspautotests (HLE coverage) - run them **exactly the way CI does**:\n\n```bash\npython test.py -g --graphics=software\n```\n\n  **The `-g` matters**: without it you also get `tests_next`, the expected-to-fail to-do list, and\n  around a hundred failures that mean nothing is wrong. The only meaningful result is `0 tests failed`.\n  (The debug-CRT \"Detected memory leaks!\" dump after the summary line is normal, not a failure.)\n\nWhen the thing under test is a commercial game rather than a suite, headless needs `--log` before it prints\nanything (to stderr). On Windows it defaults its memory stick to `<exe dir>/memstick` rather than the app's\n(on Mac and Linux it uses the app's default), so the firmware you installed in the app may not be there.\n`test.py` and `frametests.py` pass `<exe dir>/memstick` everywhere. Pass `--memstick=<the app's memstick>` for any run that is\nmeant to exercise a real firmware module. Without it, a library whose HLE the config has disabled has\nnothing left to resolve against, and the game does not fall back to our HLE - it gets unresolved imports and\ndies. The tell is a single line early on, `sceMpeg HLE is disabled, but flash0:/kd/mpeg.prx isn't in the\nfirmware and the game didn't bring its own`, followed by a run that logs happily for its whole timeout with\nzero `sceDisplaySetFramebuf` calls and a handful of cached textures. Count flips before concluding anything\nabout a game's behaviour. A run configured\ndifferently from what you asked for, or one that never reached the code, produces the same all-zero counts as\na clean one, so assert the exit code and a positive \"we got here\" counter before believing any error count.\nThe traps in full: [docs/debugging.md](docs/debugging.md).\n\nUnresolved-import errors early in a game run, for `scePsmfPlayer` in particular, are expected and not a\nproblem: many games ship libpsmfplayer (and similar libraries) on the disc, and the imports resolve when the\ngame loads the module. Don't change the setup (dropping `--memstick`, turning HLE back on) to make them go\naway; a run should be as close to a real one as possible, with firmware modules running as LLE.\n\nKeep game runs short and fast, so nobody has to watch them: always pass `--timeout-wall=30` (alongside any\n`--timeout-emulated`) unless there's a real reason for longer, use `--graphics=vulkan` rather than\n`--graphics=software` (if Vulkan doesn't work in headless, fix that), prefer a Release build, and leave out\ndebug-level `--log` unless you need it. Each of those can cost an order of magnitude: Outrun loading a state\ntook minutes with a Debug build, software rendering and a 300MB log, and about a second without them. To\ncheck that a game renders, `--screenshot-save=FILE.png` beats grepping the log. (pspautotests through\n`test.py` are different: they have their own per-test timeouts and use `--graphics=software` like CI.)\n\nTesting on a real Android device has its own traps, the worst of which is silent: `adb install -r`\nof a shorter branch over a longer one fails with `INSTALL_FAILED_VERSION_DOWNGRADE`, because\n`versionCode` is the commit count. The device keeps running the old build and every measurement after\nthat describes the wrong binary. Use `adb install -r -d`, don't swallow the install output, and check\n`adb shell dumpsys package org.ppsspp.ppsspp | grep versionName` afterwards. See\n[docs/android-device-testing.md](docs/android-device-testing.md).\n\nNew unit tests are added to `availableTests`; large ones go in their own file in `unittest/`, which has\nto be listed in **three** build files, not two: `CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its\n`.filters`), and `android/jni/Android.mk`, which builds a unit test executable of its own. Miss the last\none and it builds everywhere you can easily try it, and fails on Android CI.\n\n## Multiplatform considerations\n\nThe emulator has multiple platform-specific entry points. Some of these will be merged or removed in the future, but are all\nstill there. To verify that a change works, technically we need to compile for all these systems, but in practice we'll\njust compile locally and test the platform we are currently on, and let CI handle the cross platform considerations.\n\nSystem_-prefixed wrapper functions implement kind of a platform wrapper for some functionality, and are implemented in\nthe following list of files for each system. If we change one, we need to change them all.\n\nWindows/main.cpp\nios/main.cpp\nSDL/SDLMain.cpp\nUWP/PPSSPP_UWPMain.cpp\nandroid/jni/app-android.cpp\nlibretro/libretro.cpp\n\n## Reverse-engineering the firmware\n\nWhen a question about hardware behaviour can't be settled from the docs or from JPCSP - what a field in\na codec context means, what a library returns when a buffer runs dry - read the firmware.\n`PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR` loads one PRX standalone and writes an\nannotated disassembly, the export/import tables with NIDs resolved, and a call graph. It needs a\nfirmware dump (`--memstick` pointing at one; `--unpack-updater` can produce one).\n\nTwo things to know before trusting what you read there:\n\n- **Don't infer a function's arity from the registers it reads.** MIPS code routinely leaves an\n  argument untouched for a callee to pick up, so a function that reads only `a0` may well take\n  three. The per-function register evidence block flags this as `FORWARDED`; follow the callees.\n- **Record how you know.** A comment saying which module and function a fact came from is worth\n  more than the fact alone, since the next person can re-derive it. Behavioural findings belong\n  in the tree; bulk transcriptions of Sony's code do not.\n\n## Command-line parsing\n\nAll command-line parsing for both the main app and headless builds belongs in `Core/CmdLine.cpp` /\n`Core/CmdLine.h` (`CommandLineOptions`), not in the platform entry points (`Windows/main.cpp`,\n`headless/Headless.cpp`, `UI/NativeApp.cpp`, etc.). Don't re-parse `argv` manually in those files - add\na field to `CommandLineOptions`, and push it into `g_Config` from `ApplyToConfig()` so every platform\ngets it for free. How to declare one: [docs/command-line.md](docs/command-line.md).\n\n## File formats, codecs, and other format handlers\n\nBefore implementing any file format handler, decompressor, codec, or similar from scratch, search the\ncodebase first - PPSSPP already has implementations of many formats (CSO, LZRC, zlib-based loaders, ISO\nhandlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of\nwriting a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).\n\nFor string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.\n\n## Framedump rendering tests (frametests)\n\n`frametests.py` replays GE frame dumps (`.ppdmp`) through PPSSPPHeadless and compares the output against\nreference images, with a JSON config per test set. Consider running these when changing rendering code.\n\nNote: `headless/Compare.cpp` reads back framebuffers top-down; the flip to bottom-up is only applied when\nwriting BMPs (and when reading BMP references). `TranslateDebugBufferToCompare` also exists as a copy in\n`libretro/LibretroGraphicsContext.cpp` - keep the two in sync.\n\n## Adding HLE modules\n\nHLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h`, as a `const HLEFunction\n<name>[]` table registered via `RegisterHLEModule()`. Both the function table and the `Register_*()`\ncalls are append-only - see Core Safety Checks above, which is what breaks old savestates silently\nif ignored.\n\nAlso: a new `.cpp`/`.c` file has to be added to **seven** build files (CMake, Core.vcxproj + filters,\nthe two UWP projects, `android/jni/Android.mk`, `libretro/Makefile.common`); headers to the first five.\nFull details, the format-string legend and the UWP build command are in\n[docs/HLEModules.md](docs/HLEModules.md).\n\n## Translated UI strings (assets/lang)\n\n**When implementing new UI, translations come last, in their own commit after everything else is\ndone.** Write the English strings, get the feature built and working, commit that - then stop and ask\nthe user to check the English wording before translating anything.\n\nDon't hand-edit the ~47 language files, and don't run langtool's own AI commands either - do the\ntranslating yourself and let `Tools/langtool` do the file surgery. The workflow is in\n[docs/translations.md](docs/translations.md).\n\n## Debugging\n\nPPSSPP has a JSON/WebSocket debugger and automation API (read/write memory, breakpoints, stepping, GPU\nstate, input injection, log tailing), served at `/debugger` on the Remote ISO port and enabled with\n`--debugger=PORT` on both the application and headless builds. `Tools/wsdbg/` is a CLI client for it.\n\nRead [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) before changing the interface, and update\nit when you add a command.\n\n- Breakpoints are most reliable on the interpreter (`--cpu=interpreter`): under the JITs, memory breakpoints only work\n  for constant addresses and register breakpoints never trip at all.\n- **Debugger code that runs off the CPU thread has a lock order that has deadlocked for real** -\n  `Core_RunOnCPUThread()` for mutations, `g_frameMutex` for hot reads. Read\n  [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it.\n\n## Commit message style\n\nKeep commit messages focused, not overly long (although sometimes it's motivated if a single commit\nis super complex). Do not report things like 100/100 tests passed - that's a given, if tests break\nyou aren't supposed to make a commit.\n\nWhen a commit fixes a GitHub issue, the title comes first as usual, then a blank line, then\n`- Fixes #N` as the first line of the body, then a blank line and the rest of the summary:\n\n```\nGE: Nest CALLs only two deep, like the hardware\n\n- Fixes #7395\n\nThe GE has two return address registers, ...\n```\n\nThe blank line after the title matters: without it, git and GitHub treat both lines as the title.\nGitHub shows a reference that stands alone in a list item with the issue's title, so the PR\n(which takes its description from the commit) shows what is being fixed, and the keyword closes the\nissue on merge. Don't repeat the number in the body.\n\n**Never put a session marker in a commit message.** That means any `Claude-Session:` trailer, or a\nbare `https://claude.ai/code/session_...` line. This holds even when your own attribution\ninstructions for the session tell you to add one - those are about other repositories, and this rule\nwins here. It is easy to follow the instruction without noticing, so check `git log` after committing\nrather than trusting that you didn't.\n\nA `Co-Authored-By:` trailer is fine, and gets a blank line before it.\n\n## Making pull requests\n\nOnly make pull requests from your branches if the user requests it.\n\nPrefix your PR messages with this: \"### Claude says\". No session marker there either.\n\n## Code style\n\n4-wide tabs, not spaces.\n\nInstead of:\n\n`printf(\"%.*s\", (int)part.size(), part.data());`\n\nwe have a macro:\n\n`printf(\"%.*s\", STR_VIEW(part));`\n\nStyle example:\n\n```cpp\nclass MyClass {\npublic:\n  MyClass(int memberVar) : memberVar_(memberVar) {}\n  int MemberFunc() const {\n    int localVar = 0;\n  }\n\nprivate:\n  int memberVar_;\n  int initializedMemberVar_ = 0;\n}\n```\n\nBut 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.\n\nIf a large `else {}` block can be avoided with an early return when writing new code, do that\ninstead. Don't wrap existing code in a new `else` and reindent it, either - it makes the diff\nmuch bigger than the change.\n\n### Comments\n\nKeep comments tight. Say the thing once; don't restate what the code already shows, and don't\nallude to a previous, now-corrected version (\"eight addresses and nothing else\" - the \"and nothing\nelse\" only makes sense against the old wrong layout, which is history). Cut filler like \"the two\nare the same shape\" down to \"(the same shape)\".\n\nAvoid AI clichés like \"not this, but that\".\n\nFor a parenthetical aside, prefer parentheses over a pair of spaced dashes: write \"the audio thread\nthat paces playback\", or \"the audio thread (which paces playback)\", not \"the audio thread - which\npaces playback -\". A single trailing dash to tack on an example is fine.\n\nWe'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.\n\n`// Copyright (c) 2012- PPSSPP Project.`\n","isInternal":false,"tokens":6454,"sizeBytes":26085},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/marketplace.json","title":"Claude Plugin Marketplace Catalog","category":"marketplace","format":"json","content":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/marketplace.json)","tokens":300,"sizeBytes":194},{"name":"plugin.json","path":".claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/plugin.json","title":"claude-plugin Plugin Manifest","category":"plugin-manifest","format":"json","content":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/plugin.json)","tokens":300,"sizeBytes":180},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.cursor/mcp.json)","tokens":300,"sizeBytes":173}],"systemPromptSnippet":"<yakaai_skills repo=\"hrydgard/ppsspp\">\n<!-- File: AGENTS.md (Tokens: ~6454 | Category: root-instruction) -->\n# PPSSPP Agent instructions\n\nThese rules apply to this repository by default.\n\nIgnore the folder ai_instructions in the root directory, it's old stuff from contributors.\n\n## Detailed guides\n\nThe rules below are the short version. These docs hold the detail, look them up when the task calls\nfor it:\n\n| Doc | When you need it |\n|---|---|\n| [docs/building.md](docs/building.md) | Build commands for every target (VS/MSBuild, CMake, UWP, legacy Android NDK, libretro), unit tests, pspautotests |\n| [docs/android-device-testing.md](docs/android-device-testing.md) | Running a build on a real Android device or a Quest over adb: installing the build you think you are, forcing activity recreates, measuring retained activities, VR |\n| [docs/debugging.md](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`), finding a game's hot JIT blocks (`--jit-profile`), breakpoint reliability per CPU backend, debugging a game that works on hardware, finding and hashing a game function (like its own `memcpy`) for `ReplaceTables` when hardware backends miss CPU writes to VRAM |\n| [docs/DebuggerThreading.md](docs/DebuggerThreading.md) | `Core_RunOnCPUThread` / `g_frameMutex` / shutdown-lock rules - required reading before touching debugger code |\n| [docs/HLEModules.md](docs/HLEModules.md) | Adding an HLE module or function, and the seven build files a new source file goes in |\n| [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool |\n| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests, and running the tests that need the real firmware modules (`--disable-hle`, a firmware memstick) |\n| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |\n| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests, and replaying a dump on a real PSP |\n| [docs/sceGe.md](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 |\n| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |\n| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |\n| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does |\n| [docs/command-line.md](docs/command-line.md) | Adding a command-line option - the `g_autoParams` table, per-mode options, `ApplyToConfig()` |\n| [docs/patching-files.md](docs/patching-files.md) | Editing files from a script without wrecking the diff: line endings, and heredoc backslash escaping |\n| [docs/VSHBootInvestigation.md](docs/VSHBootInvestigation.md) | Booting the PSP's Visual Shell with `--vsh` against a real firmware dump |\n| [docs/POPS.md](docs/POPS.md) | PS1 EBOOTs: how the firmware's POPS manager and emulator are booted, and what's patched in them |\n| [docs/PsarFileFormat.md](docs/PsarFileFormat.md) | The PSAR archive inside a firmware updater - the format `Core/Util/PSARUnpack.cpp` walks |\n| [docs/pkg_notes.md](docs/pkg_notes.md) | The NPDRM `.pkg` format PSP game updates ship in, and how PPSSPP installs them |\n| [docs/cpu-equivalence.md](docs/cpu-equivalence.md) | Making the CPU cores agree bit for bit on every host: the `CPUEquivalence` fuzzer, what it found, and the differences still open (several waiting on a hardware test) |\n| [docs/dnas-patches.md](docs/dnas-patches.md) | Game patches installed on a real PSP (Killzone: Liberation's DLC): installing one over PSPLink, and the Fuse-ID-keyed PGD and type 3 PRX that `sceKernelLoadModuleDNAS` decrypts |\n| [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean |\n| [docs/shader-compilation.md](docs/shader-compilation.md) | Shader/pipeline compile stutter: where the time goes on MoltenVK (Metal's MSL compile, not glslang), the caches involved, how game-specific shaders are, and how to measure a cold start |\n| [docs/metal-backend.md](docs/metal-backend.md) | The Metal backend: shader translation, the render manager (recording, replay, pass merging), programmable blending, how it was tested against Vulkan, and what isn't done |\n| [docs/async-savestates.md](docs/async-savestates.md) | How savestate compression and file I/O run on a worker thread: the save/load/rewind pipelines, ordering, memory rules, what stays synchronous, and testing slots with the `game.state.*` debugger commands |\n\n## General instructions\n\n1. Keep style changes minimal unless requested. Follow existing code patterns and conventions.\n2. Keep cross-platform parity in mind when changing shared code. See below for more multiplatform tips\n3. **Commit freely, push only when asked.** Once a change is done and tested, commit it on its\n   branch without waiting to be told. Never `git push` (to any remote) without the user's explicit\n   approval. The exception is a useful new or re-recorded test in `pspautotests`, which goes\n   straight to its `master` without asking (see [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md#committing)).\n4. **Don't write code on `master`.** When asked to make a code change while on `master`, create an\n   appropriately named branch first (`git checkout -b some-descriptive-name`) and do the work there.\n   If you're already on a topic branch, just keep working on it.\n5. **Never assume a file's line endings - preserve whatever is on disk.** Which ending a file has\n   depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a\n   Linux checkout leaves files as they are stored, so the same file (`.vcxproj`, `.vcxproj.filters`,\n   `android/jni/Android.mk`, `libretro/Makefile.common`, this file, much of the source) is CRLF in one\n   working copy and LF in another. Don't hardcode either, and don't \"fix\" a file's endings to match\n   what a doc claims. If you patch one with a script, read *and* write with `newline=''`, which keeps\n   whatever was there; reading with Python's default universal-newline translation and writing with\n   `newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff.\n   Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in\n   the editor. Prefer the Edit tool, which does exact string replacement and can't do this.\n\n   **Don't verify this with `grep -c $'\\r$'`** - Git Bash's grep normalises line endings, so it counts\n   a bare-LF line as having a CR and reports any file as clean. The opposite mistake to a whole-file\n   rewrite is inserting a handful of LF lines into a CRLF file (a Python `\"\"\"...\"\"\"` block written with\n   `newline=''` does exactly this), and that shows in `--stat` only as the lines you meant to add. Check\n   the bytes instead, e.g. `python -c \"d=open(F,'rb').read(); print(d.count(b'\\r\\n'), d.count(b'\\n'))\"`,\n   and treat git's \"LF will be replaced by CRLF the next time Git touches it\" as the warning it is\n   rather than autocrlf noise.\n6. **Don't feed Python to `bash -c` via a heredoc when the code needs a literal backslash in its\n   *output*.** Git Bash strips one level of escaping on the way in even with a quoted delimiter\n   (`<<'PY'`), so `\"\\\\n\"` reaches Python as `\"\\n\"` and writes a real newline into the file - no error,\n   just a patch that quietly did nothing. Use the Edit tool, or write the script to a file and run it.\n   Details and the other two failure shapes: [docs/patching-files.md](docs/patching-files.md).\n\n7. **NEVER run more than one PSP hardware operation at a time.** There is one PSP. Every\n   `gentest.py`, `pspsh` or other PSPLink call gets its own message, containing that one tool call\n   and nothing else, and the next one starts only after the previous one has returned. Never put two\n   of them in the same message as parallel tool calls, never background one and start another, and\n   never loop over tests in one shell command. Re-recording a directory means one test, wait,\n   the next test. Concurrent runs collide in PSPLink and `host0:`: they time out, their results are\n   garbage, and the PSP wedges until someone resets it by hand. This has happened repeatedly; there\n   is no case where it is acceptable.\n8. **Update submodules when you start working.** Run `git submodule update --init --recursive`\n   before anything else, and again after switching or rebasing branches, so that builds and tests\n   use the submodule commits the branch records.\n\n## Core Safety Checks\n\n1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.\n2. Consider savestate compatibility when changing serialized state.\n3. **Never insert an entry into the middle of an `HLEFunction` array.** A savestate stores the\n   syscall opcode, which encodes the entry's *index* in that array - so inserting anywhere but the\n   end silently repoints every later entry, and old savestates start calling the wrong function.\n   This applies to adding a *single* function to an *existing* module, which is when it is easiest\n   to forget: put it last in the array even when alphabetical or NID order would put it elsewhere,\n   and even when the array is otherwise tidily sorted. The same rule governs the order of\n   `Register_*()` calls in `Core/HLE/HLETables.cpp` - new modules go at the very end.\n4. **Changing a shader cache key means bumping `CACHE_VERSION`, in both the OpenGL and Vulkan\n   caches** (`GPU/GLES/ShaderManagerGLES.cpp` and `GPU/Vulkan/ShaderManagerVulkan.cpp`). Their\n   on-disk caches store raw key bits with nothing to tell an old layout from a new one. OpenGL\n   stores `VShaderID`/`FShaderID`, and Vulkan stores those plus `VulkanPipelineKey` (the raster\n   state key and the decoded vertex format ID). So adding, removing, moving or reinterpreting any\n   of those bits needs the bump, or an old cache precompiles shaders for keys that now mean\n   something else, including combinations the generators assert on. D3D11 doesn't store shader IDs\n   on disk. Keys that only live in memory, like `SamplerCacheKey`, don't need a bump.\n5. **\"As on hardware\" needs a hardware test behind it.** A commit message or comment that says the\n   PSP does something names the pspautotests test that shows it, recorded before the change. And the\n   test has to reach the code path the change is about: when the HLE code handles the same call\n   differently by dialog type, mode or size, cover each of those. A dialog fix once claimed\n   hardware behaviour that the test recorded an hour later contradicted, and the test ran a message\n   dialog, which took a different branch from the savedata dialog that then hung Freak Out.\n\n## Build and validation\n\n- Linux/Mac: `./b.sh --debug` for a full configure+build; after that, `cd build ; make -j32; cd ..`.\n- Windows: always build through `Windows/PPSSPP.sln`, even if a stray CMake-generated `build/` directory\n  exists at the repo root. Drive it with `MSBuild.exe` (found via `vswhere.exe`) rather than the GUI:\n\n```powershell\n$installPath = & \"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -property installationPath\n$msbuild = \"$installPath\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n& $msbuild \"Windows\\PPSSPP.sln\" /t:UnitTest /p:Configuration=Debug /p:Platform=<platform> /m\n```\n\n- **`<platform>` is whatever the machine is - look it up, don't assume.** It is `ARM64` or `x64`, and\n  the build lands in `Windows\\<platform>\\<configuration>\\` to match, so building one and running the\n  other is easy to do without noticing. On Windows-on-ARM an x64 build runs anyway, under emulation,\n  which is what makes it easy to miss: it works, but it is slower than the native build, it is not\n  the code ARM users get, and any benchmark from it measures the emulator. Get the host from\n  `python -c \"import platform; print(platform.machine())\"`, not `$PROCESSOR_ARCHITECTURE`, which\n  describes the *shell* and says `AMD64` from an emulated one. The binaries say which they are too -\n  `UnitTest.exe` prints an `ABI:` line at startup.\n- Kill leftover `PPSSPPHeadless.exe`/`PPSSPP*.exe` instances before building - one holding the exe makes\n  the link fail with `LNK1168`, which looks like a build problem and isn't.\n- **A stale binary lies consistently.** After a `git stash` cycle that touched a header, do a\n  `/t:Rebuild`; when bisecting a behavioural change, confirm the binary actually changed before you\n  believe the result.\n- **If a savestate fails to load, first suspect the branch, not the loader.** Either your branch is\n  behind the build that made the state (rebase it on `origin/master` and rebuild), or the state was\n  made on a branch that hasn't been merged yet. Only once you've ruled out both is it a savestate\n  compatibility bug.\n\nUWP, the legacy Android NDK build and the libretro core have their own build systems.\n\n## Testing\n\nAfter a chunk of work (not after every edit), run both suites:\n\n- C++ unit tests: build the `UnitTest` project and run `Windows/<platform>/Debug/UnitTest.exe all`\n  (Linux/Mac: configure with `-DUNITTEST=ON`, run `build/PPSSPPUnitTest all`). Tests are listed in\n  `availableTests` in `unittest/UnitTest.cpp`; pass names instead of `all` to run a subset.\n- pspautotests (HLE coverage) - run them **exactly the way CI does**:\n\n```bash\npython test.py -g --graphics=software\n```\n\n  **The `-g` matters**: without it you also get `tests_next`, the expected-to-fail to-do list, and\n  around a hundred failures that mean nothing is wrong. The only meaningful result is `0 tests failed`.\n  (The debug-CRT \"Detected memory leaks!\" dump after the summary line is normal, not a failure.)\n\nWhen the thing under test is a commercial game rather than a suite, headless needs `--log` before it prints\nanything (to stderr). On Windows it defaults its memory stick to `<exe dir>/memstick` rather than the app's\n(on Mac and Linux it uses the app's default), so the firmware you installed in the app may not be there.\n`test.py` and `frametests.py` pass `<exe dir>/memstick` everywhere. Pass `--memstick=<the app's memstick>` for any run that is\nmeant to exercise a real firmware module. Without it, a library whose HLE the config has disabled has\nnothing left to resolve against, and the game does not fall back to our HLE - it gets unresolved imports and\ndies. The tell is a single line early on, `sceMpeg HLE is disabled, but flash0:/kd/mpeg.prx isn't in the\nfirmware and the game didn't bring its own`, followed by a run that logs happily for its whole timeout with\nzero `sceDisplaySetFramebuf` calls and a handful of cached textures. Count flips before concluding anything\nabout a game's behaviour. A run configured\ndifferently from what you asked for, or one that never reached the code, produces the same all-zero counts as\na clean one, so assert the exit code and a positive \"we got here\" counter before believing any error count.\nThe traps in full: [docs/debugging.md](docs/debugging.md).\n\nUnresolved-import errors early in a game run, for `scePsmfPlayer` in particular, are expected and not a\nproblem: many games ship libpsmfplayer (and similar libraries) on the disc, and the imports resolve when the\ngame loads the module. Don't change the setup (dropping `--memstick`, turning HLE back on) to make them go\naway; a run should be as close to a real one as possible, with firmware modules running as LLE.\n\nKeep game runs short and fast, so nobody has to watch them: always pass `--timeout-wall=30` (alongside any\n`--timeout-emulated`) unless there's a real reason for longer, use `--graphics=vulkan` rather than\n`--graphics=software` (if Vulkan doesn't work in headless, fix that), prefer a Release build, and leave out\ndebug-level `--log` unless you need it. Each of those can cost an order of magnitude: Outrun loading a state\ntook minutes with a Debug build, software rendering and a 300MB log, and about a second without them. To\ncheck that a game renders, `--screenshot-save=FILE.png` beats grepping the log. (pspautotests through\n`test.py` are different: they have their own per-test timeouts and use `--graphics=software` like CI.)\n\nTesting on a real Android device has its own traps, the worst of which is silent: `adb install -r`\nof a shorter branch over a longer one fails with `INSTALL_FAILED_VERSION_DOWNGRADE`, because\n`versionCode` is the commit count. The device keeps running the old build and every measurement after\nthat describes the wrong binary. Use `adb install -r -d`, don't swallow the install output, and check\n`adb shell dumpsys package org.ppsspp.ppsspp | grep versionName` afterwards. See\n[docs/android-device-testing.md](docs/android-device-testing.md).\n\nNew unit tests are added to `availableTests`; large ones go in their own file in `unittest/`, which has\nto be listed in **three** build files, not two: `CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its\n`.filters`), and `android/jni/Android.mk`, which builds a unit test executable of its own. Miss the last\none and it builds everywhere you can easily try it, and fails on Android CI.\n\n## Multiplatform considerations\n\nThe emulator has multiple platform-specific entry points. Some of these will be merged or removed in the future, but are all\nstill there. To verify that a change works, technically we need to compile for all these systems, but in practice we'll\njust compile locally and test the platform we are currently on, and let CI handle the cross platform considerations.\n\nSystem_-prefixed wrapper functions implement kind of a platform wrapper for some functionality, and are implemented in\nthe following list of files for each system. If we change one, we need to change them all.\n\nWindows/main.cpp\nios/main.cpp\nSDL/SDLMain.cpp\nUWP/PPSSPP_UWPMain.cpp\nandroid/jni/app-android.cpp\nlibretro/libretro.cpp\n\n## Reverse-engineering the firmware\n\nWhen a question about hardware behaviour can't be settled from the docs or from JPCSP - what a field in\na codec context means, what a library returns when a buffer runs dry - read the firmware.\n`PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR` loads one PRX standalone and writes an\nannotated disassembly, the export/import tables with NIDs resolved, and a call graph. It needs a\nfirmware dump (`--memstick` pointing at one; `--unpack-updater` can produce one).\n\nTwo things to know before trusting what you read there:\n\n- **Don't infer a function's arity from the registers it reads.** MIPS code routinely leaves an\n  argument untouched for a callee to pick up, so a function that reads only `a0` may well take\n  three. The per-function register evidence block flags this as `FORWARDED`; follow the callees.\n- **Record how you know.** A comment saying which module and function a fact came from is worth\n  more than the fact alone, since the next person can re-derive it. Behavioural findings belong\n  in the tree; bulk transcriptions of Sony's code do not.\n\n## Command-line parsing\n\nAll command-line parsing for both the main app and headless builds belongs in `Core/CmdLine.cpp` /\n`Core/CmdLine.h` (`CommandLineOptions`), not in the platform entry points (`Windows/main.cpp`,\n`headless/Headless.cpp`, `UI/NativeApp.cpp`, etc.). Don't re-parse `argv` manually in those files - add\na field to `CommandLineOptions`, and push it into `g_Config` from `ApplyToConfig()` so every platform\ngets it for free. How to declare one: [docs/command-line.md](docs/command-line.md).\n\n## File formats, codecs, and other format handlers\n\nBefore implementing any file format handler, decompressor, codec, or similar from scratch, search the\ncodebase first - PPSSPP already has implementations of many formats (CSO, LZRC, zlib-based loaders, ISO\nhandlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of\nwriting a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).\n\nFor string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.\n\n## Framedump rendering tests (frametests)\n\n`frametests.py` replays GE frame dumps (`.ppdmp`) through PPSSPPHeadless and compares the output against\nreference images, with a JSON config per test set. Consider running these when changing rendering code.\n\nNote: `headless/Compare.cpp` reads back framebuffers top-down; the flip to bottom-up is only applied when\nwriting BMPs (and when reading BMP references). `TranslateDebugBufferToCompare` also exists as a copy in\n`libretro/LibretroGraphicsContext.cpp` - keep the two in sync.\n\n## Adding HLE modules\n\nHLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h`, as a `const HLEFunction\n<name>[]` table registered via `RegisterHLEModule()`. Both the function table and the `Register_*()`\ncalls are append-only - see Core Safety Checks above, which is what breaks old savestates silently\nif ignored.\n\nAlso: a new `.cpp`/`.c` file has to be added to **seven** build files (CMake, Core.vcxproj + filters,\nthe two UWP projects, `android/jni/Android.mk`, `libretro/Makefile.common`); headers to the first five.\nFull details, the format-string legend and the UWP build command are in\n[docs/HLEModules.md](docs/HLEModules.md).\n\n## Translated UI strings (assets/lang)\n\n**When implementing new UI, translations come last, in their own commit after everything else is\ndone.** Write the English strings, get the feature built and working, commit that - then stop and ask\nthe user to check the English wording before translating anything.\n\nDon't hand-edit the ~47 language files, and don't run langtool's own AI commands either - do the\ntranslating yourself and let `Tools/langtool` do the file surgery. The workflow is in\n[docs/translations.md](docs/translations.md).\n\n## Debugging\n\nPPSSPP has a JSON/WebSocket debugger and automation API (read/write memory, breakpoints, stepping, GPU\nstate, input injection, log tailing), served at `/debugger` on the Remote ISO port and enabled with\n`--debugger=PORT` on both the application and headless builds. `Tools/wsdbg/` is a CLI client for it.\n\nRead [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) before changing the interface, and update\nit when you add a command.\n\n- Breakpoints are most reliable on the interpreter (`--cpu=interpreter`): under the JITs, memory breakpoints only work\n  for constant addresses and register breakpoints never trip at all.\n- **Debugger code that runs off the CPU thread has a lock order that has deadlocked for real** -\n  `Core_RunOnCPUThread()` for mutations, `g_frameMutex` for hot reads. Read\n  [docs/DebuggerThreading.md](docs/DebuggerThreading.md) before touching it.\n\n## Commit message style\n\nKeep commit messages focused, not overly long (although sometimes it's motivated if a single commit\nis super complex). Do not report things like 100/100 tests passed - that's a given, if tests break\nyou aren't supposed to make a commit.\n\nWhen a commit fixes a GitHub issue, the title comes first as usual, then a blank line, then\n`- Fixes #N` as the first line of the body, then a blank line and the rest of the summary:\n\n```\nGE: Nest CALLs only two deep, like the hardware\n\n- Fixes #7395\n\nThe GE has two return address registers, ...\n```\n\nThe blank line after the title matters: without it, git and GitHub treat both lines as the title.\nGitHub shows a reference that stands alone in a list item with the issue's title, so the PR\n(which takes its description from the commit) shows what is being fixed, and the keyword closes the\nissue on merge. Don't repeat the number in the body.\n\n**Never put a session marker in a commit message.** That means any `Claude-Session:` trailer, or a\nbare `https://claude.ai/code/session_...` line. This holds even when your own attribution\ninstructions for the session tell you to add one - those are about other repositories, and this rule\nwins here. It is easy to follow the instruction without noticing, so check `git log` after committing\nrather than trusting that you didn't.\n\nA `Co-Authored-By:` trailer is fine, and gets a blank line before it.\n\n## Making pull requests\n\nOnly make pull requests from your branches if the user requests it.\n\nPrefix your PR messages with this: \"### Claude says\". No session marker there either.\n\n## Code style\n\n4-wide tabs, not spaces.\n\nInstead of:\n\n`printf(\"%.*s\", (int)part.size(), part.data());`\n\nwe have a macro:\n\n`printf(\"%.*s\", STR_VIEW(part));`\n\nStyle example:\n\n```cpp\nclass MyClass {\npublic:\n  MyClass(int memberVar) : memberVar_(memberVar) {}\n  int MemberFunc() const {\n    int localVar = 0;\n  }\n\nprivate:\n  int memberVar_;\n  int initializedMemberVar_ = 0;\n}\n```\n\nBut 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.\n\nIf a large `else {}` block can be avoided with an early return when writing new code, do that\ninstead. Don't wrap existing code in a new `else` and reindent it, either - it makes the diff\nmuch bigger than the change.\n\n### Comments\n\nKeep comments tight. Say the thing once; don't restate what the code already shows, and don't\nallude to a previous, now-corrected version (\"eight addresses and nothing else\" - the \"and nothing\nelse\" only makes sense against the old wrong layout, which is history). Cut filler like \"the two\nare the same shape\" down to \"(the same shape)\".\n\nAvoid AI clichés like \"not this, but that\".\n\nFor a parenthetical aside, prefer parentheses over a pair of spaced dashes: write \"the audio thread\nthat paces playback\", or \"the audio thread (which paces playback)\", not \"the audio thread - which\npaces playback -\". A single trailing dash to tack on an example is fine.\n\nWe'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.\n\n`// Copyright (c) 2012- PPSSPP Project.`\n\n<!-- File: .claude-plugin/marketplace.json (Tokens: ~300 | Category: marketplace) -->\n# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/marketplace.json)\n\n<!-- File: .claude-plugin/plugin.json (Tokens: ~300 | Category: plugin-manifest) -->\n# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.claude-plugin/plugin.json)\n\n<!-- File: .cursor/mcp.json (Tokens: ~300 | Category: mcp-config) -->\n# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/hrydgard/ppsspp/HEAD/.cursor/mcp.json)\n</yakaai_skills>"}