# Technical Documentation: hrydgard/ppsspp

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/hrydgard/ppsspp)

### Overview (`README.md`)

PPSSPP - a fast and portable PSP emulator
=========================================

Created by Henrik Rydgård

Additional code by many contributors, see the Credits screen

Originally released under the GPL 2.0 (and later) in November 2012

Official website: https://www.ppsspp.org/

Discord: https://discord.gg/5NJB6dD

No BIOS file required to play, PPSSPP is an "HLE" emulator.  Default settings balance good compatibility and speed.

To contribute, see [Introduction to development](https://www.ppsspp.org/docs/development/getting-started). This page also includes our new AI policy.

Help with testing, investigating, or fixing bugs is always welcome.  See [the list of issues](https://github.com/hrydgard/ppsspp/issues).

For the latest source code, see [our GitHub page](https://github.com/hrydgard/ppsspp).

For documentation of all kinds (usage, reference, development), see the [documentation on the main website](https://www.ppsspp.org/docs)

For build instructions and other development tutorials, see [the wiki](https://github.com/hrydgard/ppsspp/wiki).

To download fresh development builds for Android, Windows and Mac, [go to the /devbuilds page](https://www.ppsspp.org/devbuilds)

For game compatibility, see [community compatibility feedback](https://report.ppsspp.org/games).

What's new in 1.20.4
--------------------
1.20.4 is an unusually big followup release, but given the new multiplayer developments, we need something to cover the longer gap to 1.21.
During this cycle, I've mostly focused on UX improvements.

- Ad-hoc
  - Support for "data.json" live status from adhoc relay servers ([#21496], [#21526])
  - RetroAchievements UI improvements around subsets ([#21536])
  - Assorted ad-hoc UI improvements ([#21530], [#21510])
  - Fixes for The Warriors ([#21464]) and Street Riders ([#21460]) in relay mode

- Texture upscaling
  - Fix old bug causing some textures to get stuck unscales on some hardware when using GPU texture upscaling ([#21533])
  - Improved performance of MMPX-adv upscaler ([#21555])
  - NNEDI3 and Spline36 GPU texture scaling ([#21527])

- Rendering fixes
  - Fix a bug in lens flare occlusion for the Syphon Filter games ([#21511])
  - Fix a bug in the software renderer ([#21648])
  
- Misc UI improvements
  - Instant type-to-search in game browser ([#21559], [#21565], [#21630])
  - Fix file picker problems on some Android devices (regression) ([#21614], [#21656])
  - Fix crash in text edit fields on Mac/iOS ([#21601])
  - PSP DVD prototypes can now load directly ([#21599], [#21601])
  - Cheat UI has been cleaned up and supports titles and comments natively ([#21590])
  - RetroAchievements subset display improvements ([#21536])
  - Basic deep link support on iOS ([#21615])
  - Soft keyboard has more symbols ([#21625])

- Other
  - Fix important (but mostly rare) crash bug affecting games that draw a lot ([#21669])
  - More plugin zip files can now auto-install ([#21556])
  - Pause on lost-focus on Linux ([#21517])
  - Frameskipping no longer breaks analog stick auto-rotation in GoW, however manual rotation still broken ([#21508])
  - Android: Restore "Landscape Auto" ([#21482]), the "Auto" screen rotation mode now overrides the user rotation lock ([#21507])
  - Contribution by lrzlin: Loongarch improvements ([#21499], [#21595])
  - Basic support for extracting ISO from 7zip files ([#21493])
  - Fix a bug that prevented loading certain texture packs ([#21491])
  - Fix music looping in Death JR ([#21490])
  - Hide the save-load indicator in the corner by default ([#21528])
  - Fix crash in the remote debugger, by Nemoumbra ([#21652])
  - Fix screenshot cropping bug ([#21665])
  - mfplat.dll is no longer required on Windows unless you need camera/microphone ([#21660])

What's new in 1.20.3
--------------------

- Fix issue preventing ad hoc relay servers from working when RetroAchievements are enabled on Windows ([#21420])
- Some ad hoc UI improvements ([#21432], [#21433])
- Fix crash/failure setting the background on iOS ([#21409])
- Fix logging to file ([#21412])
- Networking settings cleanup ([#21418])
- Some fixes for assorted rare crashes ([#21422])
- Fix issues when unpausing using a controller binding ([#21424]), DualSense Edge detection on Windows ([#21426])
- Fix missing savestate undo button ([#21425])
- MMPX texture upscaling algorithm has been restored, the new one has been improved ([#21376]) and renamed MMPX Advanced ([#21421])
- Fix support for custom game config for homebrew apps ([#21434])
- Improve insets handling on Android ([#21437])

What's new in 1.20.2
--------------------

- Improved server list for ad hoc multiplayer, dynamically updated and you can now add/remove entries ([#21326], [#21389], [#21351])
- Fix broken multitouch on iOS with OpenGL ([#21350])
- Ad hoc relay connection improvements ([#21352])
- Fix a lot of minor UI issues ([#21400], [#21362])
- Fix background image selection on Android ~~and iOS~~ ([#21345], [#21384], [#21371])
- Fix file permission issue on iOS ([#21374])
- Add a "hold" version of axis swap toggle ([#21357])
- Fix regression in Gripshift ([#21377])
- Fix crash on audio device switch on Windows ([#21341])
- Fix timing glitches in gamepad input on Windows ([#21393])
- And other assorted fixes.

What's new in 1.20
------------------

- User interface
  - New feature: Proper support for Portrait Mode UI - you can now rotate your phone to vertical mode, and things will mostly work fine! ([#21185], [#21044]...)
  - New feature: Separate touch controls and display layout configs for landscape/portrait, new default touch control layout for portrait mode
  - New feature: Upload files via HTTP from devices on the same network
  - Savestate count limit can now be configured ([#21112])
  - UI icons are now vector-based, making them look sharper on high-DPI screens ([#20824], [#20870])
  - Better handling of unsupported ISOs ([#21169], [#21166])
  - Misc: Improved text rendering, chat box improvements ([#20653]), PIC0 is now visible on the game screen ([#20686]), ([#20611]), rotation fixes ([#21145]), input fixes ([#21248]), fixes for XBox FullScreen Experience, background stretch ([#21211]), handle savedata in main game browser ([#21277]), Discord Presence improvement ([#21246])
  - Show ICON1.PMF on the game info screen ([#21303])

- Regression fixes
  - In 7th Dragon, voice-related crashes are fixed ([#20837])
  - Soundtrack is working again in Tony Hawk's Project 8 ([#20775])
  - Fix crash in StormBasic homebrew apps ([#20715])
  - Built-in songs in Beats work again ([#20662])
   - Fix adhoc in Syphon Filter games ([#20642])
  - Fix strange cutscene speedups in Power Stone 2 (Power Stone Collection) ([#2124])
  - Work around game bug to help more custom songs play again in GTA LCS ([#20692])

- Multiplayer
  - Support aemu relay servers for AdHoc multiplayer ([#21116], [#21271])

- Controller and touch screen input
  - Native support for DualSense ([#20580], [#20620], [#21191]), DualShock and Switch Pro ([#20647]) controllers on Windows - including tilt controls for DualSense and Switch Pro
  - Misc touch screen and controller mapping improvements ([#21195], [#21197], [#21258], [#21325])
  - Add simple virtual keyboard for text input for platforms that don't have one ([#21306])

- Graphics
  - Fixed multiple graphical effects in Tales of Phantasia X
  - Reworked the VSync setting to be more intuitive. It's now pretty much never a bad idea to turn it on.
  - Fixes for various glitches in Tales of Phantasia X ([#21173], [#21141], [#21205])
  - Some more framebuffer readback workarounds ([#20640], [#20631], [#20632])
  - MMPX upscaling enhancements by crashGG ([#20622], [#20541])
  - Huge rendering speedup in Brave Story ([#21151])
  - Fix rendering bug caused by CPU emulation bug in ATV Offroad Fury: Blazing Trails ([#21238])
  - Partially fix rendering problem in Mahjong Artifacts ([#21244]) and The Mystery of the Crystal Portal ([#21236])
  - Boost the GPU in Outrun 2006 to avoid unnecessary slowdowns ([#21304])

- Other game fixes
  - Fix savedata problem in Silent Hill: Shattered Memories ([#21294])

- Audio
  - Added "Smooth" playback mode, which evens out glitches (while adding a very small amount of latency)
  - Remove DirectSound support ([#20533]), add support for ultra-low-latency streams on WASAPI, available on some devices ([#20535])

- Platform support
  - Windows
    - D3D9 support has been removed ([#19951], [#20490])
    - ARM64 is now officially supported, and works great ([#20863])
    - Correct fullscreen mode in Xbox Fullscreen Experience ([#21189])
    - Assorted improvements ([#20778], [#20774])
  - Android
    - The minimum Android version has been raised for technical reasons, KitKat is no longer supported ([#19658])
    - Support for Android devices with 16KB page size. This is a technical change, required by Google Play, that unfortunately forced us to drop compatibility with the very oldest devices (Android < 5). ([#20788])
    - Shortcut widgets can now be created even if the app isn't running ([#20798])
    - Removed support for classic Moga controllers, these were only a thing on early Android ([#20762])
  - iOS
    - Add support for screen rotation
  - Linux
    - Loongarch improvements by lrzlin ([#20683], [#20644]) and KatyushaScarlet ([#20599], [#20594]), text rendering improvements ([#21163])
    - SDL fullscreen problems fixed ([#21300], more)
  - UWP
    - Migrate code base to C++/WinRT ([#21100])

- Debugger
  - ImDebugger improvements ([#20861], [#20779], [#20657], [#20637], [#20550], [#20523])
  - Websocket debugger fixes ([#20749])

- Other
  - RetroAchievements: Upgrade the support library with perf fixes ([#21081]), various fixes and improvements
  - Add workaround for infamous God of War crash ([#21148])
  - Fix assorted minor UI bugs ([#21042], ...)

What's new in 1.19.3
--------------------

- Fixed crash on startup in Tony Hawk's Underground 2 ([#20573])
- Fixed crash/hang when accessing the menu in FFII if readbacks were configured to copy-to-tex ([#20573])
- Fix issue with detection of some types of saving, for the save reminders on exit ([#20623])
- Fixed music and other audio in modded games (various football games, Crazy Taxi w/ original soundtrack, etc) ([#20566], [#20571])
- Fix exiting not functioning correctly on Windows in some circumstances ([#20607])
- Minor UI and key binding fixes ([#20604])
- Fix crash/blackscreen when switching from skip buffer effects to auto-frameskip rendering ([#20605])
- Fix bug in Mac/Linux builds where the mouse got hidden and stuck if you enabled mouse input mapping ([#20612])
- Fix black screen on save/load in the Football Manager Handheld games ([#20616])
- Translation improvements

What's new in 1.19.2
--------------------

- In-game save/load not properly detected for saving reminder ([#20500])
- Install savedata from ZIP was partially broken ([#20498])
- Fixed module loader bug affecting a few games ([#20513])
- Fixed some hangs in the Windows debugger ([#20510])
- Fix performance problem in texture replacement ([#20520])
- Assorted fixes ([#20518], [#20514], [#20502], [#20515])

What's new in 1.19.1
--------------------

- Fix selecting background image on Android ([#20477])
- Fix RetroAchievements regression for multi-executable games ([#20469])
- Possible fix for Mac audio device selection issue ([#20482])
- Add workaround for Dragon's Lair not working with LLE scePsmf ([#20468])
- Prevent trying to load obviously-corrupt CSO/CHD files ([#20466])
- Fix regression for homebrew apps that request extra memory ([#20457])
- Fix grid drawing in the various Robot Taisen games ([#20456])
- Fix crash in UI when viewing a directory with multiple NPDRM ISOs ([#20453])

What's new in 1.19
------------------

- Audio
	- Brand new sceAtrac implementation, fixing Flatout and multiple other long-standing issues! ([#20098], [#20136], [#20138], [#20162])
	- More correctly implemented sceAtrac-through-sceSas functionality, fixing voice in Sol Trigger and multiple minis ([#20156])
	- Better support for using sceAudiocodec directly, fixing music in some homebrew apps ([#20199], [#20246], [#20253], [#20209])
	- Volume control improvement ([#19969], [#19971])

- Rendering fixes and performance
    - Important: This will be the last major version with D3D9 support. D3D11 will of course continue to be supported, along with Vulkan and OpenGL.
	- Fix smoke effects in Jak'n'Daxter by re-using the fix from Ratchet & Clank ([#20032])
    - Software depth buffer rendering added to fix lens flares in multiple games efficiently (Midnight Club LA, Wipeout, Syphon Filter etc) ([#19748], [#19764], [#20231], [#19955], etc)
	- Multiple rendering-related crash fixes ([#20270], [#20346], [#20344])
	- Texture replacement load speed corrected and can now be controlled ([#20286]), regression fix for zipped packs ([#19860])
	- Additional assorted compatibility and other fixes ([#20376], [#20070], [#19685])
	- Persona 1/2 readback fixes ([#20067], [#20068])
	- Other correctness fixes ([#20233], [#20255], [#19686])

- General emulation
	- Allow disabling HLE on a per-module basis (if games ship the module so we can load it). Enabled for sceCcc ([#20218]), and more importantly scePsmf ([#20208]), fixing multiple issues.
	- Additional module loading improvements ([#20114])
	- More precise sleep function on Windows ([#20054])
	- More replacement texture improvements, allow replacing game icons ([#19975], [#19978])
	- Disable certain features like fast-forward when networking is on, to avoid de-syncs ([#20311])
	- Fixes mono Atrac3 as video soundtrack ([#19800])
	- Improvements to RISC-V support ([#20352])

- Control and input
	- Touch gliding support added ([#19983])
	- Allow simultaneous DInput and XInput input ([#20347])

- New "Dear ImGui"-based PSP debugger: ImDebugger
	- Allows very rapid development of debugging features as-needed, and also implements a pretty good Ge debugger for stepping through draws.
	- Unlike the old Win32 debugger (but similar to the unmaintained web debugger), works on all platforms (though cumbersome on touchscreens)
	- Major commits (though there are many more): ([#20242], [#20240], [#20294])

- Multiplayer
	- Added Infrastructure multiplayer support, with automatic DNS configuration ([#19865], [#19869], [#20221], [#20220], [#19875])
	    - Note: Only games that people have made "revival servers" for will work. See [here](https://www.ppsspp.org/docs/multiplayer/infrastructure-servers/).
	- Implement more of sceNet (prerequisite for the former feature).
	- Socket re-mapping implemented ([#19827])
	- Assorted bugfixes, thanks ANR2ME ([#20247], [#20245], [#19843], [#19849], [#19836])

- UI
	- Loading spinner now actually spins properly, the app no longer appears to hang ([#20341])
	- Minor features: Asks for confirmation on exit in most scenarios ([#19996], [#20023]), DPI scaling ([#20013]), can pause without menu ([#19883])
	- As usual a lot of tweaks, perf fixes, and fixes for hangs and crashes ([#20343], [#20332], [#20305], [#20303], [#20299], [#20163], [#20152], [#20143], [#20079], [#20137], [#20374])
	- Two new color themes ([#20334], [#20335]), related themability fixes ([#19984], [#19995], [#20308])
	- Improvements and bug fixes in the savedata manager ([#19771], [#20170])
	- Add "Move to trash" deletion functionality to multiple platforms ([#20230], [#20261])
	- Add ability to take "raw" screenshots of gameplay ([#20029])
	- More files can be loaded directly from ZIP ([#20243])
	- Developer Settings are now tabbed for easier access ([#20228])
	- Switch to the full libpng API so we can disable gamma correction, like the real PSP ([#20033])
	- Support displaying the battery percentage on more platforms ([#19973], [#19967])
	- Allow picking a background image on iOS ([#20370])

- Platform compatibility
	- Exclude older Macs from using Vulkan (too many black screens, hangs) ([#20236])
	- Use portable-file-dialogs to provide file-open dialogs on Linux ([#20175])
	- "Cache full ISO in RAM" is now correctly hidden where it doesn't work ([#20165])
	- Now rendering at proper resolution on newer Macs ([#20011])
	- Mouse input improved on Android, including separate button mapping ([#19915])
	- Use the correct font again on Mac/iOS ([#19874])
	- Multiple file access optimizations made to make the most out of the flawed foundation called Android Scoped Storage ([#19668])

What's new in earlier versions
------------------------------
See [history.md](history.md).

[comment]: # (LINK_LIST_BEGIN_HERE)
[#20098]: https://github.com/hrydgard/ppsspp/issues/20098 "New implementation of sceAtrac (the Atrac3+ module)"
[#20136]: https://github.com/hrydgard/ppsspp/issues/20136 "New sceAtrac impl: Fix low level decoding"
[#20138]: https://github.com/hrydgard/ppsspp/issues/20138 "Use the new sceAtrac implementation"
[#20162]: https://github.com/hrydgard/ppsspp/issues/20162 "at3_standalone: Make all allocations aligned."
[#20156]: https://github.com/hrydgard/ppsspp/issues/20156 "Reimplement Atrac-through-SAS"
[#20199]: https://github.com/hrydgard/ppsspp/issues/20199 "Partially implement sceAudiocodec"
[#20246]: https://github.com/hrydgard/ppsspp/issues/20246 "sceAudiocodec: Restore AAC support, add AT3 (non-plus) support"
[#20253]: https://github.com/hrydgard/ppsspp/issues/20253 "Revert to using FFMPEG for MP3 playback"
[#20209]: https://github.com/hrydgard/ppsspp/issues/20209 "More HLE cleanup, fix MP3 in sceAudiocodec"
[#19969]: https://github.com/hrydgard/ppsspp/issues/19969 "Volume control UI changes, part 1"
[#19971]: https://github.com/hrydgard/ppsspp/issues/19971 "Volume control UI changes, part 2"
[#20032]: https://github.com/hrydgard/ppsspp/issues/20032 "Fix Jak & daxter smoke effects (same problems as Ratchet)"
[#19748]: https://github.com/hrydgard/ppsspp/issues/19748 "Render a software depth buffer in parallel with HW rendering"
[#19764]: https://github.com/hrydgard/ppsspp/issues/19764 "Enable depth raster in all backends"
[#20231]: https://github.com/hrydgard/ppsspp/issues/20231 "Fix lens flare in L.A. Rush"
[#19955]: https://github.com/hrydgard/ppsspp/issues/19955 "CrossSIMD: Add a simple unit test, fix a couple of operations in the no-simd path"
[#20270]: https://github.com/hrydgard/ppsspp/issues/20270 "Avoid using shader blending in skip-buffer-effects mode"
[#20346]: https://github.com/hrydgard/ppsspp/issues/20346 "Metal Gear Acid 2 oil spill crashfix"
[#20344]: https://github.com/hrydgard/ppsspp/issues/20344 "Fix crash in texture saving, fix Mega Minis 2"
[#20286]: https://github.com/hrydgard/ppsspp/issues/20286 "New setting: Texture replacement load speed"
[#19860]: https://github.com/hrydgard/ppsspp/issues/19860 "Fix regression loading zipped texture packs"
[#20376]: https://github.com/hrydgard/ppsspp/issues/20376 "Vulkan semaphore fix"
[#20070]: https://github.com/hrydgard/ppsspp/issues/20070 "Software renderer: Fix regression with gouraud shaded lines"
[#19685]: https://github.com/hrydgard/ppsspp/issues/19685 "Cull through-mode 2D draws against scissor rectangle"
[#20067]: https://github.com/hrydgard/ppsspp/issues/20067 "Hook framebuffer readback function in Persona 1."
[#20068]: https://github.com/hrydgard/ppsspp/issues/20068 "Hook framebuffer readback function in Persona 2"
[#20233]: https://github.com/hrydgard/ppsspp/issues/20233 "Fix Star Ocean with MSAA enabled: don't use the blit optimization (Vulkan)"
[#20255]: https://github.com/hrydgard/ppsspp/issues/20255 "Vulkan: Auto Max Quality: Avoid conflict between aniso filtering and nearest filtering"
[#19686]: https://github.com/hrydgard/ppsspp/issues/19686 "Fix small accuracy issue in through-mode 2D culling"
[#20218]: https://github.com/hrydgard/ppsspp/issues/20218 "Misc fixes and cleanup, use DisableHLE with \"sceCcc\""
[#20208]: https://github.com/hrydgard/ppsspp/issues/20208 "Disable HLE of scePsmf and scePsmfPlayer"
[#20114]: https://github.com/hrydgard/ppsspp/issues/20114 "Fix sceUtilityLoadModuleAv, allow browsing memory tags in the memory viewer"
[#20054]: https://github.com/hrydgard/ppsspp/issues/20054 "Switch to sleep_precise for WaitUntil(), bump VMA and gradle versions"
[#19975]: https://github.com/hrydgard/ppsspp/issues/19975 "Allow custom game icons if texture replacement is enabled"
[#19978]: https://github.com/hrydgard/ppsspp/issues/19978 "ZipFileReader: Small performance optimization when reading"
[#19800]: https://github.com/hrydgard/ppsspp/issues/19800 "Fix playback of mono Atrac3+ tracks in videos"
[#20352]: https://github.com/hrydgard/ppsspp/issues/20352 "Fix RiscVEmitter::QuickFLI (#20351)"
[#19983]: https://github.com/hrydgard/ppsspp/issues/19983 "Touch: Implement \"Touch gliding\" (keep all dragged/touched buttons pressed until touch release)"
[#20347]: https://github.com/hrydgard/ppsspp/issues/20347 "DInput: Properly ignore XInput devices individually, instead of ignoring all if XInput is available"
[#20242]: https://github.com/hrydgard/ppsspp/issues/20242 "ImDebugger: Add some audio investigation tools"
[#20240]: https://github.com/hrydgard/ppsspp/issues/20240 "minor-breakpoint-improvements"
[#20294]: https://github.com/hrydgard/ppsspp/issues/20294 "Misc ImDebugger improvements"
[#19865]: https://github.com/hrydgard/ppsspp/issues/19865 "Infrastructure Auto DNS: Preconfigured per-game infrastructure DNS through JSON"
[#19869]: https://github.com/hrydgard/ppsspp/issues/19869 "DNS autoconf: Fix games that do their own DNS queries"
[#20221]: https://github.com/hrydgard/ppsspp/issues/20221 "More infrastructure networking fixes"
[#20220]: https://github.com/hrydgard/ppsspp/issues/20220 "Infrastructure multiplayer fixes"
[#19875]: https://github.com/hrydgard/ppsspp/issues/19875 "Online: Fix DNS server default, show revival team credits on pause screen"
[#19827]: https://github.com/hrydgard/ppsspp/issues/19827 "sceNetInet socket remap"
[#20247]: https://github.com/hrydgard/ppsspp/issues/20247 "[Adhoc] Fixed truncated adhoc group name issue."
[#20245]: https://github.com/hrydgard/ppsspp/issues/20245 "[Adhoc] Partially fixes multiplayer regression on GTA games."
[#19843]: https://github.com/hrydgard/ppsspp/issues/19843 "An attempt to fix Driver 76 multiplayer"
[#19849]: https://github.com/hrydgard/ppsspp/issues/19849 "Fix bug in sceNetInetPoll, similar to the previous select bug"
[#19836]: https://github.com/hrydgard/ppsspp/issues/19836 "An attempt to fix UNO single player."
[#20341]: https://github.com/hrydgard/ppsspp/issues/20341 "More async GPU init"
[#19996]: https://github.com/hrydgard/ppsspp/issues/19996 "Add confirmation on exit"
[#20023]: https://github.com/hrydgard/ppsspp/issues/20023 "Add confirmation popup support on Exit App key, libretro buildfix"
[#20013]: https://github.com/hrydgard/ppsspp/issues/20013 "UI DPI scale setting"
[#19883]: https://github.com/hrydgard/ppsspp/issues/19883 "Add new mappable key to pause without the pause menu."
[#20343]: https://github.com/hrydgard/ppsspp/issues/20343 "Avoid getting stuck in a loop when using auto-load-state and the state is bad"
[#20332]: https://github.com/hrydgard/ppsspp/issues/20332 "Fix reset bug, frame advance bug, translation issues"
[#20305]: https://github.com/hrydgard/ppsspp/issues/20305 "Fix exiting from framedump playback, some std::thread code cleanup"
[#20303]: https://github.com/hrydgard/ppsspp/issues/20303 "Some UI fixes, crashfixes"
[#20299]: https://github.com/hrydgard/ppsspp/issues/20299 "Don't call rc_client_do_frame when paused."
[#20163]: https://github.com/hrydgard/ppsspp/issues/20163 "Switch the recent files manager to the \"command processor on thread\" pattern, to avoid blocking the main thread"
[#20152]: https://github.com/hrydgard/ppsspp/issues/20152 "Settings: Load tabs on demand, instead of all at once"
[#20143]: https://github.com/hrydgard/ppsspp/issues/20143 "Fix crash when saving screenshots on a thread"
[#20079]: https://github.com/hrydgard/ppsspp/issues/20079 "Touch control layout editor: Resize the game image to fit the editing surface"
[#20137]: https://github.com/hrydgard/ppsspp/issues/20137 "Screenshot performance improvement"
[#20374]: https://github.com/hrydgard/ppsspp/issues/20374 "More crashfixes"
[#20334]: https://github.com/hrydgard/ppsspp/issues/20334 "Add Alpine theme"
[#20335]: https://github.com/hrydgard/ppsspp/issues/20335 "Add Strawberry theme"
[#19984]: https://github.com/hrydgard/ppsspp/issues/19984 "Theme system fixes and additions"
[#19995]: https://github.com/hrydgard/ppsspp/issues/19995 "More theming work"
[#20308]: https://github.com/hrydgard/ppsspp/issues/20308 "Make slider colors themable"
[#19771]: https://github.com/hrydgard/ppsspp/issues/19771 "UI fixes: Rework savedata manager a bit, default keyboard focus to Cancel in confirmation dialogs"
[#20170]: https://github.com/hrydgard/ppsspp/issues/20170 "Fix bugs in savedata manager"
[#20230]: https://github.com/hrydgard/ppsspp/issues/20230 "Move to trash instead of deleting important files like savedata (Windows only so far)"
[#20261]: https://github.com/hrydgard/ppsspp/issues/20261 "Trash handling is too high level for FileUtil, move it up."
[#20029]: https://github.com/hrydgard/ppsspp/issues/20029 "Add long-requested feature to take screenshots of the raw game image instead of the processed output."
[#20243]: https://github.com/hrydgard/ppsspp/issues/20243 "Add a ZipFileLoader, which can let us load any single-file file type from a standard zip file"
[#20033]: https://github.com/hrydgard/ppsspp/issues/20033 "Use libpng's full API so we can ignore gamma. Fixes Driver '76's icon."
[#20228]: https://github.com/hrydgard/ppsspp/issues/20228 "Developer tools screen: Use tabs"
[#19973]: https://github.com/hrydgard/ppsspp/issues/19973 "Add support for displaying the battery percentage on Windows."
[#19967]: https://github.com/hrydgard/ppsspp/issues/19967 "Support battery percentage display on SDL"
[#20370]: https://github.com/hrydgard/ppsspp/issues/20370 "iOS: Implement a background image picker"
[#20236]: https://github.com/hrydgard/ppsspp/issues/20236 "Blacklist older Intel GPUs from using Vulkan on Mac"
[#20175]: https://github.com/hrydgard/ppsspp/issues/20175 "Add Linux file dialog support through \"portable-file-dialogs\""
[#20165]: https://github.com/hrydgard/ppsspp/issues/20165 "Reintroduce and fix feature checks for \"Cache full ISO in RAM\""
[#20011]: https://github.com/hrydgard/ppsspp/issues/20011 "macOS SDL: Set the metal layer resolution properly, remove DPI hacks."
[#19915]: https://github.com/hrydgard/ppsspp/issues/19915 "Android: Improve mouse input"
[#19874]: https://github.com/hrydgard/ppsspp/issues/19874 "macOS/iOS: register font with CoreText"
[#19668]: https://github.com/hrydgard/ppsspp/issues/19668 "File system perf part 1: Remove some unnecessary file access"
[#20311]: https://github.com/hrydgard/ppsspp/issues/20311 "Automatically disable fast forward feature when doing multiplayer."
[#20477]: https://github.com/hrydgard/ppsspp/issues/20477 "Fix background image selection on Android"
[#20469]: https://github.com/hrydgard/ppsspp/issues/20469 "Fix RetroAchievements for multi-exe games, add some sanity checks"
[#20482]: https://github.com/hrydgard/ppsspp/issues/20482 "SDL audio: Improve logging, don't auto-switch device during the first 3 seconds of execution"
[#20468]: https://github.com/hrydgard/ppsspp/issues/20468 "Add workaround for scePsmf LLE not working with Dragon's Lair"
[#20466]: https://github.com/hrydgard/ppsspp/issues/20466 "ISO loading: Check CSO and CHD files \"early\""
[#20457]: https://github.com/hrydgard/ppsspp/issues/20457 "Fix homebrew apps that request large memory"
[#20456]: https://github.com/hrydgard/ppsspp/issues/20456 "Fix grid drawing in Robot Taisen games."
[#20453]: https://github.com/hrydgard/ppsspp/issues/20453 "libkirk concurrency fixes"
[#20500]: https://github.com/hrydgard/ppsspp/issues/20500 "Correct save/load detection for the reminder."
[#20498]: https://github.com/hrydgard/ppsspp/issues/20498 "Install savedata from zip seems to have regressed"
[#20513]: https://github.com/hrydgard/ppsspp/issues/20513 "ELF loader: Revert bad export check"
[#20510]: https://github.com/hrydgard/ppsspp/issues/20510 "Fix bug in Win32 debugger, misc fixes"
[#20520]: https://github.com/hrydgard/ppsspp/issues/20520 "Replacement textures: Don't spend frame time waiting for a texture to be finished"
[#20518]: https://github.com/hrydgard/ppsspp/issues/20518 "Disable the Cache full ISO in RAM feature on 32-bit builds"
[#20514]: https://github.com/hrydgard/ppsspp/issues/20514 "Developer tools: Add UI to force-enable HLE of modules that we have disabled HLE of"
[#20502]: https://github.com/hrydgard/ppsspp/issues/20502 "Switching from IR Interpreter to JIT in-game crashes"
[#20515]: https://github.com/hrydgard/ppsspp/issues/20515 "Additional fixes for 1.19.2"
[#20573]: https://github.com/hrydgard/ppsspp/issues/20573 "Fix crash on second logo in Tony Hawk's Underground 2"
[#20566]: https://github.com/hrydgard/ppsspp/issues/20566 "Atrac code cleanup, logging and comment fixes"
[#20571]: https://github.com/hrydgard/ppsspp/issues/20571 "More Atrac3 refactor"
[#20607]: https://github.com/hrydgard/ppsspp/issues/20607 "Fix issue where PPSSPP wouldn't correctly quit if you closed it with the corner X button in-game"
[#20604]: https://github.com/hrydgard/ppsspp/issues/20604 "Fix some small UI regressions"
[#20605]: https://github.com/hrydgard/ppsspp/issues/20605 "Fix strange edge case when switching to auto-frameskip from non-buffered rendering"
[#20612]: https://github.com/hrydgard/ppsspp/issues/20612 "SDL: Fix bug where the mouse got stuck in relative mode when mapping mouse inputs"
[#20616]: https://github.com/hrydgard/ppsspp/issues/20616 "Football Manager Handheld series: Fix blackscreen"
[#20623]: https://github.com/hrydgard/ppsspp/issues/20623 "Fix tracking for the savedata reminder for several invisible save types"
[#21185]: https://github.com/hrydgard/ppsspp/issues/21185 "Move controls up a bit in portrait mode, render fix of main screen buttons when held"
[#21044]: https://github.com/hrydgard/ppsspp/issues/21044 "Add convenient UI on the pause screen for changing/locking the screen orientation"
[#21112]: https://github.com/hrydgard/ppsspp/issues/21112 "Rework listing of savestates - a single scan operation instead of lots of File::Exists and GetFileInfo calls."
[#20824]: https://github.com/hrydgard/ppsspp/issues/20824 "Load UI atlas images from SVG"
[#20870]: https://github.com/hrydgard/ppsspp/issues/20870 "UI: Convert rendering to use premultiplied alpha"
[#21169]: https://github.com/hrydgard/ppsspp/issues/21169 "Make UMD_VIDEO discs with game data detect as games."
[#21166]: https://github.com/hrydgard/ppsspp/issues/21166 "Improve handling of the UMD VIDEO error case (no, we still don't support them)"
[#20653]: https://github.com/hrydgard/ppsspp/issues/20653 "Fix assorted interaction problems with the chat menu"
[#20686]: https://github.com/hrydgard/ppsspp/issues/20686 "PIC0 support"
[#20611]: https://github.com/hrydgard/ppsspp/issues/20611 "Show a tiny indicator in the top left of the screen when the game is saving or loading"
[#21145]: https://github.com/hrydgard/ppsspp/issues/21145 "Correct functionality of the display rotation control rotation."
[#21248]: https://github.com/hrydgard/ppsspp/issues/21248 "Control mapper refactor, allow unpausing using analog triggers if mapped"
[#21211]: https://github.com/hrydgard/ppsspp/issues/21211 "Background stretch: Only resort to crop to avoid extreme squishing of the image."
[#21277]: https://github.com/hrydgard/ppsspp/issues/21277 "Fix a rare savedata crash, handle savedata files better in the game browser"
[#21246]: https://github.com/hrydgard/ppsspp/issues/21246 "Reflect Gold status in Discord Rich Presence"
[#21303]: https://github.com/hrydgard/ppsspp/issues/21303 "Show ICON1.PMF videos on game info screen"
[#20837]: https://github.com/hrydgard/ppsspp/issues/20837 "Fix a load slowdown, possible fix for voices in 7th Dragon II"
[#20775]: https://github.com/hrydgard/ppsspp/issues/20775 "Tony Hawk's Project 8 Soundtrack features disabled after update"
[#20715]: https://github.com/hrydgard/ppsspp/issues/20715 "Fix typo in mono atrac initialization. Fixes crash in StormBasic homebrews"
[#20662]: https://github.com/hrydgard/ppsspp/issues/20662 "Atrac3+: Fix parsing for the AA3 file format"
[#20642]: https://github.com/hrydgard/ppsspp/issues/20642 "Disable the memcpy slicing thing (for HLE memcpys) in Syphon Filter games"
[#2124]: https://github.com/hrydgard/ppsspp/issues/2124 "Fixed a build error on non-win32 platfroms"
[#20692]: https://github.com/hrydgard/ppsspp/issues/20692 "GTA Liberty City Stories no longer plays custom AT3 music"
[#21116]: https://github.com/hrydgard/ppsspp/issues/21116 "Support adhoc relay from aemu to ppsspp"
[#21271]: https://github.com/hrydgard/ppsspp/issues/21271 "Fix FlatOut Head On in aemu_postoffice relay mode"
[#20580]: https://github.com/hrydgard/ppsspp/issues/20580 "Windows: Add basic native support for DualShock / DualSense"
[#20620]: https://github.com/hrydgard/ppsspp/issues/20620 "Control the LEDs on DualShock/DualSense gamepads"
[#21191]: https://github.com/hrydgard/ppsspp/issues/21191 "DualSense on Windows: Support Bluetooth control messages"
[#20647]: https://github.com/hrydgard/ppsspp/issues/20647 "Windows: Add basic native support for Switch Pro controllers"
[#21195]: https://github.com/hrydgard/ppsspp/issues/21195 "Touch controls: Don't fade out while buttons are being held"
[#21197]: https://github.com/hrydgard/ppsspp/issues/21197 "Split gesture control into left and right zones"
[#21258]: https://github.com/hrydgard/ppsspp/issues/21258 "Fix Driver 76 not creating adhoc sockets"
[#21173]: https://github.com/hrydgard/ppsspp/issues/21173 "Add workaround for Tales of Phantasia X flicker problem"
[#21141]: https://github.com/hrydgard/ppsspp/issues/21141 "Fix problem with block image copies within a framebuffer in Vulkan, misc fixes"
[#21205]: https://github.com/hrydgard/ppsspp/issues/21205 "Fix issue with destination rectangles with image block copies"
[#20640]: https://github.com/hrydgard/ppsspp/issues/20640 "Hook FB Readbacks in games"
[#20631]: https://github.com/hrydgard/ppsspp/issues/20631 "Hook framebuffer readback function in Never7, Ever17, and Remember11"
[#20632]: https://github.com/hrydgard/ppsspp/issues/20632 "Hook framebuffer readback function in Steins;Gate"
[#20622]: https://github.com/hrydgard/ppsspp/issues/20622 "enhance MMPX algorithm final part"
[#20541]: https://github.com/hrydgard/ppsspp/issues/20541 "enhance MMPX algorithm part 2"
[#21151]: https://github.com/hrydgard/ppsspp/issues/21151 "Brave Story: Hack to make the bloom effect run much more efficiently"
[#21238]: https://github.com/hrydgard/ppsspp/issues/21238 "Fix rare ARM64 JIT bug"
[#21244]: https://github.com/hrydgard/ppsspp/issues/21244 "Fix black background in Mahjong Artifacts, minor GE debugger improvement"
[#21236]: https://github.com/hrydgard/ppsspp/issues/21236 "Texturing fix, initial window size fix, etc"
[#21304]: https://github.com/hrydgard/ppsspp/issues/21304 "UI fixes, add GPU \"overclock\" for Outrun"
[#21294]: https://github.com/hrydgard/ppsspp/issues/21294 "Add compat flag to freeze reported file creation times. Helps Silent Hill: Shattered Memories"
[#20533]: https://github.com/hrydgard/ppsspp/issues/20533 "Windows: Remove DirectSound support"
[#20535]: https://github.com/hrydgard/ppsspp/issues/20535 "Add the new low-latency WASAPI backend, add audio device selection on Windows"
[#19951]: https://github.com/hrydgard/ppsspp/issues/19951 "Remove D3D9 support, to make future changes easier"
[#20490]: https://github.com/hrydgard/ppsspp/issues/20490 "Remove some D3D9 leftovers"
[#20863]: https://github.com/hrydgard/ppsspp/issues/20863 "Arm64 FP controlword support on Windows, plus TR fix"
[#21189]: https://github.com/hrydgard/ppsspp/issues/21189 "Rework fullscreen on Windows based on tests"
[#20778]: https://github.com/hrydgard/ppsspp/issues/20778 "Windows: More startup performance"
[#20774]: https://github.com/hrydgard/ppsspp/issues/20774 "Windows: Speed up startup"
[#19658]: https://github.com/hrydgard/ppsspp/issues/19658 "Add support for 16kb page size on Android"
[#20788]: https://github.com/hrydgard/ppsspp/issues/20788 "Android: Update to NDK 28"
[#20798]: https://github.com/hrydgard/ppsspp/issues/20798 "Android shortcuts: Fix so that setting icons work even if PPSSPP isn't running"
[#20762]: https://github.com/hrydgard/ppsspp/issues/20762 "Android: Remove support for Moga controllers"
[#20683]: https://github.com/hrydgard/ppsspp/issues/20683 "loongarch: Fix various IR JIT & VertexJIT bugs"
[#20644]: https://github.com/hrydgard/ppsspp/issues/20644 "loongarch: Implement Morph in VertexJIT & QuickTexHashLSX"
[#20599]: https://github.com/hrydgard/ppsspp/issues/20599 "Detect and enable LSX/LASX on LoongArch based on compiler predefined macros."
[#20594]: https://github.com/hrydgard/ppsspp/issues/20594 "Disable LASX on LoongArch64"
[#21163]: https://github.com/hrydgard/ppsspp/issues/21163 "SDL text drawer: Fix memory leak, add additional checks"
[#21300]: https://github.com/hrydgard/ppsspp/issues/21300 "Fix fullscreen bugs in SDL port."
[#20861]: https://github.com/hrydgard/ppsspp/issues/20861 "ImDebugger: Add a JIT viewer window"
[#20779]: https://github.com/hrydgard/ppsspp/issues/20779 "Add simple ParamSFO viewer to ImDebugger"
[#20657]: https://github.com/hrydgard/ppsspp/issues/20657 "Default to Vulkan on earlier Windows versions, show sceAac contexts in debugger"
[#20637]: https://github.com/hrydgard/ppsspp/issues/20637 "Use a TTF font for fixed-width text in the debugger"
[#20550]: https://github.com/hrydgard/ppsspp/issues/20550 "ImMemView: Refined the keyboard shortcuts."
[#20523]: https://github.com/hrydgard/ppsspp/issues/20523 "[ImMemView] Editable Memory"
[#20749]: https://github.com/hrydgard/ppsspp/issues/20749 "More debugger fixes"
[#21081]: https://github.com/hrydgard/ppsspp/issues/21081 "Update the rcheevos library to 1.12.2."
[#21148]: https://github.com/hrydgard/ppsspp/issues/21148 "Add workaround for infamous GoW crash"
[#21042]: https://github.com/hrydgard/ppsspp/issues/21042 "Setting out of game UI confirmation button to O makes both O and X into UI confirmation buttons"
[#21325]: https://github.com/hrydgard/ppsspp/issues/21325 "Controls: Add a setting for the threshold used to map analog stick inputs to digital buttons"
[#21306]: https://github.com/hrydgard/ppsspp/issues/21306 "Server list update, add virtual keyboard to PopupTextInputChoice"
[#21326]: https://github.com/hrydgard/ppsspp/issues/21326 "Adhoc server list: Show metadata"
[#21389]: https://github.com/hrydgard/ppsspp/issues/21389 "Adhoc server list work, add missing translations"
[#21351]: https://github.com/hrydgard/ppsspp/issues/21351 "Ad hoc server list refactor: Parse the server list from a json file"
[#21350]: https://github.com/hrydgard/ppsspp/issues/21350 "Fix multitouch in OpenGL mode on iOS. Oops."
[#21352]: https://github.com/hrydgard/ppsspp/issues/21352 "Handle adhoc relay connection a bit better"
[#21400]: https://github.com/hrydgard/ppsspp/issues/21400 "Fix minor UI centering issues and similar"
[#21362]: https://github.com/hrydgard/ppsspp/issues/21362 "More UI-related fixes"
[#21345]: https://github.com/hrydgard/ppsspp/issues/21345 "Fix image picker on Android, plus smoother fullscreen startup on Windows"
[#21384]: https://github.com/hrydgard/ppsspp/issues/21384 "Android background image selection: Scale down selected background image when too large"
[#21371]: https://github.com/hrydgard/ppsspp/issues/21371 "Various cleanups, iCloud fix"
[#21374]: https://github.com/hrydgard/ppsspp/issues/21374 "iOS: Preserve permissions to access files across runs"
[#21357]: https://github.com/hrydgard/ppsspp/issues/21357 "Add a \"hold\" version of the axis swap toggle. Often more convenient."
[#21377]: https://github.com/hrydgard/ppsspp/issues/21377 "Windows audio fix, Gripshift glitch workaround"
[#21341]: https://github.com/hrydgard/ppsspp/issues/21341 "Fix crash on audio device switch in Windows"
[#21393]: https://github.com/hrydgard/ppsspp/issues/21393 "Windows input optimizations"
[#21100]: https://github.com/hrydgard/ppsspp/issues/21100 "UWP: Migrate from C++/CX to C++/WinRT"
[#21420]: https://github.com/hrydgard/ppsspp/issues/21420 "Misc: Update aemu_postoffice, add confirmation dialog when creating game configs"
[#21409]: https://github.com/hrydgard/ppsspp/issues/21409 "Image picker on iOS: Fix crash, use a newer method that opens a lot quicker"
[#21412]: https://github.com/hrydgard/ppsspp/issues/21412 "Fix logging to file, assorted minor fixes"
[#21418]: https://github.com/hrydgard/ppsspp/issues/21418 "Networking settings: Some reordering and naming cleanup, link to quickstart guide"
[#21422]: https://github.com/hrydgard/ppsspp/issues/21422 "Fixes for some rarer crashes from Play reports"
[#21424]: https://github.com/hrydgard/ppsspp/issues/21424 "Fix control input issues when toggling the pause menu using a controller"
[#21425]: https://github.com/hrydgard/ppsspp/issues/21425 "Fix accidentally missing undo button on the savestate popup."
[#21376]: https://github.com/hrydgard/ppsspp/issues/21376 "enhance MMPX algorithm bug fixes and logic optimizations"
[#21421]: https://github.com/hrydgard/ppsspp/issues/21421 "Split MMPX texture upscaling shader into regular and advanced"
[#21426]: https://github.com/hrydgard/ppsspp/issues/21426 "Add detection of Dualsense Edge controllers on Windows, update README.md"
[#21432]: https://github.com/hrydgard/ppsspp/issues/21432 "Rename \"Hostname\" to \"Hostname or IP\" in adhoc server settings"
[#21433]: https://github.com/hrydgard/ppsspp/issues/21433 "Add a \"quick-edit\" button for the current server host/ip, if it's custom"
[#21434]: https://github.com/hrydgard/ppsspp/issues/21434 "Fix support for custom game configs for homebrews"
[#21437]: https://github.com/hrydgard/ppsspp/issues/21437 "Android: Handle inset adjustments on the C++ side, handle them better in the UI."
[#21496]: https://github.com/hrydgard/ppsspp/issues/21496 "Add live server status for servers supporting data.json"
[#21526]: https://github.com/hrydgard/ppsspp/issues/21526 "Fix json parsing bugs, add data.json for Relay Brasiliero"
[#21536]: https://github.com/hrydgard/ppsspp/issues/21536 "Show RA subsets on separate tabs"
[#21530]: https://github.com/hrydgard/ppsspp/issues/21530 "Pause screen network UI improvements"
[#21510]: https://github.com/hrydgard/ppsspp/issues/21510 "Chat popup improvements"
[#21464]: https://github.com/hrydgard/ppsspp/issues/21464 "fix the warriors in relay mode"
[#21460]: https://github.com/hrydgard/ppsspp/issues/21460 "fix street rider sending pdp packets using wrong pdp dst addresses in relay mode"
[#21533]: https://github.com/hrydgard/ppsspp/issues/21533 "Fix some textures getting stuck unscaled with GPU texture upscalers"
[#21555]: https://github.com/hrydgard/ppsspp/issues/21555 "MMPX_adv  algorithm bug fixes and logic optimizations"
[#21527]: https://github.com/hrydgard/ppsspp/issues/21527 "NNEDI3 and Spline36 GPU texture scaling"
[#21511]: https://github.com/hrydgard/ppsspp/issues/21511 "Depth raster: Handle triangles properly when backface culling is disabled"
[#21528]: https://github.com/hrydgard/ppsspp/issues/21528 "Hide the save/load indicator"
[#21556]: https://github.com/hrydgard/ppsspp/issues/21556 "Fix for auto-installing plugins from zip"
[#21517]: https://github.com/hrydgard/ppsspp/issues/21517 "Implement pause on lost focus on SDL"
[#21508]: https://github.com/hrydgard/ppsspp/issues/21508 "Analog stick autorotation now uses game timing"
[#21482]: https://github.com/hrydgard/ppsspp/issues/21482 "Restore the \"Landscape auto\" orientation mode"
[#21507]: https://github.com/hrydgard/ppsspp/issues/21507 "Android: Make the \"Auto\" rotation mode override user rotation lock (SENSOR mode)"
[#21499]: https://github.com/hrydgard/ppsspp/issues/21499 "loongarch: Implement excepetion handler and JIT bug fix"
[#21493]: https://github.com/hrydgard/ppsspp/issues/21493 "Initial 7z support"
[#21491]: https://github.com/hrydgard/ppsspp/issues/21491 "IniFile: Disallow section headers from starting at other line offsets than the start"
[#21490]: https://github.com/hrydgard/ppsspp/issues/21490 "Atrac3: Allow sending 0xFFFFFFFF as data size (Death JR music looping)"
[#21625]: https://github.com/hrydgard/ppsspp/issues/21625 "Soft keyboard work, checkboxes to enable/disable control input sources"
[#21615]: https://github.com/hrydgard/ppsspp/issues/21615 "Add deep link support on iOS"
[#21614]: https://github.com/hrydgard/ppsspp/issues/21614 "Use a proxy activity for Android file pickers, simplify handling of Vblank events"
[#21601]: https://github.com/hrydgard/ppsspp/issues/21601 "Mac/iOS: Crash and logic fixes for special characters in TextEdit"
[#21599]: https://github.com/hrydgard/ppsspp/issues/21599 "Support loading PSP prototype DVD-R disc dumps directly"
[#21590]: https://github.com/hrydgard/ppsspp/issues/21590 "UI improvements: CwCheat titles/comments, easier to scroll tabs"
[#21559]: https://github.com/hrydgard/ppsspp/issues/21559 "UI: Instant type-to-search in game browsers"
[#21565]: https://github.com/hrydgard/ppsspp/issues/21565 "Improve search to support full-width chars and to clear search on navigation"
[#21630]: https://github.com/hrydgard/ppsspp/issues/21630 "Refactor type-to-search code for reuse, use in cheats dialog"
[#21595]: https://github.com/hrydgard/ppsspp/issues/21595 "loongarch: Fix Jit_WeightsU16Skin in VertexDecoderLoongArch64.cpp"
[#21648]: https://github.com/hrydgard/ppsspp/issues/21648 "Remove 2 cases for fast path for soft gpu"
[#21656]: https://github.com/hrydgard/ppsspp/issues/21656 "Fix the java exception reporting for Android file picker errors, fix some search issues"
[#21669]: https://github.com/hrydgard/ppsspp/issues/21669 "Add \"allocation slack\" to our pushbuffers. Fixes a memory overwrite bug"
[#21652]: https://github.com/hrydgard/ppsspp/issues/21652 "Critical bug in the remote debugger"
[#21665]: https://github.com/hrydgard/ppsspp/issues/21665 "Raw screenshot cropping fix"
[#21660]: https://github.com/hrydgard/ppsspp/issues/21660 "Remove the mfplat.dll hard dependency (now fully dynamic)."

---

### Overview (`headless/README.md`)

# PPSSPPHeadless

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

Primarily intended for:
- Automated regression testing (via [pspautotests](https://github.com/hrydgard/pspautotests/))
- Replaying GE frame dumps (`.ppdmp`) to compare rendering output across GPU backends or settings
- CI pipelines that need to verify emulation correctness

## Building

### Windows (Visual Studio)

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

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

### Linux / macOS (CMake)

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

## Usage

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

### Options

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

## Program Output

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

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

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

## GPU Backends

The `--graphics` option selects the rendering backend:

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

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

## Frame Dump Replay (`.ppdmp`)

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

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

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

### Example: Generate a reference screenshot

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

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

### Example: Compare against a reference

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

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

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

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

## Screenshot Comparison Details

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

## Text Output Comparison

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

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

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

## Batch Testing

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

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

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

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

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

## CI Integration

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

Example workflow step:

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

## Configuration

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

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

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

---

### Overview (`ios/README.md`)

Updating with a self-built MoltenVK
===================================

Probably won't be needed again.

cp -r ../dev/build-molten/MoltenVK/Package/Release/MoltenVK/static/MoltenVK.xcframework ios/MoltenVK

The Old iOS Build Instructions
==============================

Important Notes
---------------

These instructions have not been updated for a long time and may be outdated.


Prerequisites:
--------------

* Xcode (from the Mac App Store) with command line tools installed
* MacPorts (from macports.org); easiest to install with their package installers
* cmake build system (from MacPorts); run "sudo port install cmake" from the command line

If you need to build ffmpeg yourself too, then you'll also need:

* gas-preprocessor; download the zip from https://github.com/mansr/gas-preprocessor, unzip and from the command line run:

        sudo cp gas-preprocessor.pl /usr/bin/
        sudo chmod +rw /usr/bin/gas-preprocessor.pl

* you may need pkg-config (from MacPorts); run "sudo port install pkgconfig" from the command line

Most of this is done from the command line:
-------------------------------------------

Change directory to wherever you want to install ppsspp (eg. "cd ~"), and then clone the main ppsspp repository:

    git clone https://github.com/hrydgard/ppsspp.git

Change directory to the newly created ppsspp directory and run:

    git submodule update --init

The above command will pull in the submodules required by PPSSPP, including the native, ffmpeg, and lang directories.  Included in the ffmpeg directory should be the necessary libs and includes for ffmpeg, so most people can skip the next command.  However, if you need to recompile ffmpeg for some reason, change directory into ffmpeg and run (this will take a while):

    ./ios-build.sh

Change directory back up to the main ppsspp directory and do the following:

    mkdir build-ios
    cd build-ios
    cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchains/ios.cmake -GXcode ..

You now should have an Xcode project file in the build-ios directory named PPSSPP.xcodeproj.  Open it up in Xcode and do Product->Build to build the debug version, or Product->Build For->Archiving to build the release version (which is much faster).  If your iOS device is plugged in, you may be able to just Run in Xcode to install and test it.  Otherwise, copy the PPSSPP app from build-ios/Debug-iphoneos/PPSSPP.app or build-ios/Release-iphoneos/PPSSPP.app to the /Applications directory on your device and from ssh or MobileTerminal do a "chmod +x PPSSPP" inside the PPSSPP.app directory.  If this is the first time you've installed the PPSSPP app, you'll have to respring or restart your device for the icon to show up.

---

### Overview (`macOS/README.md`)

# MacOS notes

Entitlements.plist is to be used for code signing on macOS.

We enable "hardened runtime" in order to make notarization happy, but we need some exceptions.

## MoltenVK on Mac

To update MoltenVK, download the latest version and get the files from dylib/macOS and put them in ext/vulkan/macOS/Frameworks.

---

### Overview (`Tools/langtool/README.md`)

# Language ini file processor tool

To install Rust and cargo, [go here](https://www.rust-lang.org/learn/get-started).

To run, with rust installed, change to this Tools/langtool directory, then:

```bash
cargo run
```

(Or just move the binary out of target/ and run it directly).

To see command line usage, type:

```bash
cargo run -- --help
```

## Validating

```bash
cargo run -- validate
```

Checks every translated string in every language file against the English one it came from:
placeholders (`%1`, `%d`, ...) must survive translation, no line breaks or stray quotes, nothing
empty. Exits with a non-zero code if it finds anything, so it can be used as a check in a script.

The same checks run on anything the AI produces, before it gets written to a file.

## AI translation

Some commands (`add-new-key-ai`, `add-new-key-value-ai`, `finish-language-with-ai`) use an LLM to
generate translations. Two providers are supported, pick one by setting its API key in the
environment:

* Claude (Anthropic): `ANTHROPIC_API_KEY`
* OpenAI: `OPENAI_API_KEY`

If both are set, Claude is used unless you say otherwise with `--provider openai`. To pick a
specific model, use `--model`, for example:

```bash
cargo run -- --model claude-sonnet-5 finish-language-with-ai sv_SE
cargo run -- --model gpt-4o finish-language-with-ai sv_SE
```

The provider is inferred from the model name, so `--provider` is only needed to disambiguate.

Strings that are deliberately the same as the English one (terms like "Vsync") are marked with a
`# same as English` comment. `finish-language-with-ai` skips those instead of paying to have them
"translated" again on every run, and writes the comment itself when the AI hands the English string
back unchanged. Delete the comment if you disagree with it and it'll be picked up again next time.

To autoformat the code, use:

```bash
cargo fmt
```

or set up your IDE to do that (it's already pre-set-up for VS Code, if you open the langtool folder directly).

---

### Overview (`Tools/wsdbg/README.md`)

# wsdbg - WebSocket debugger CLI

A small standalone client for talking directly to PPSSPP's WebSocket debugger
interface (see `docs/WebSocketDebugger.md` in the root of the repo for the
full protocol reference and event catalog).

To install Rust and cargo, [go here](https://www.rust-lang.org/learn/get-started).

To run, with rust installed, change to this `Tools/wsdbg` directory, then:

```bash
cargo run -- <port>
```

(Or build once with `cargo build --release` and run the binary directly from
`target/release/`.)

## Usage

Start PPSSPP with the remote debugger enabled (`--debugger` on the command
line, or Settings > Tools > Developer Tools > Allow remote debugger) and note
the port it's listening on (shown in that same settings screen, and logged at
startup).

### Letting wsdbg start PPSSPP

For headless scripting, `--launch` removes all of that. It starts PPSSPP, learns the debugger port
from its output, connects once the socket accepts, and kills it again on the way out:

```bash
wsdbg --sync --compact --quiet --launch ./PPSSPPHeadless.exe --vsh --cpu=interpreter --graphics=vulkan < script.txt
```

`--launch` takes the executable and all of its arguments, so it has to come last - everything after
it belongs to PPSSPP, and wsdbg's own flags go before it. `--debugger=0` is appended unless the
arguments already ask for a debugger port, so the OS picks a free one and nothing has to agree on a
port number in advance. PPSSPP's output is forwarded to wsdbg's stderr, separate from the protocol
messages on stdout, so `2>emu.log` keeps them apart.

This replaces the wrapper script the same job used to need: no launching in the background, no
polling a log file for the port (a race - you can attach to a *previous* run that still holds one),
no fixed `sleep` before connecting, and no leftover emulator processes, which `--timeout`'s
wall-clock budget otherwise leaves running for as long as it says.

Interactive REPL - type an event name and `key=value` params, get responses
and broadcasts (log messages, stepping notifications, etc.) printed as they
arrive:

```bash
cargo run -- 12345
> game.status
-> (ticket 2) {"event":"game.status","ticket":2}

<- {
  "event": "game.status",
  "game": null,
  "paused": false
}
```

One-shot mode - send a single event and exit as soon as its reply arrives, handy
for scripting:

```bash
cargo run -- 12345 gpu.stats.get
cargo run -- 12345 cpu.setReg thread=0 name=4 value=1000
cargo run -- 12345 --raw '{"event":"cpu.evaluate","expression":"pc"}'
```

The reply is matched by ticket, so this returns in milliseconds rather than
padding every invocation with a fixed sleep. `--wait` (default 10s) is only the
upper bound before it gives up and exits non-zero. Pass `--wait-all` to go back
to "print everything that arrives for `--wait` seconds", which is what you want
when watching broadcasts (log lines, `gpu.stats.feed`) rather than asking a
question.

An `error` reply carrying the request's ticket makes it exit non-zero too (with `--wait-all`,
once the interval is up).

Type `:help` in the REPL for a quick reminder, `:quit` to disconnect.

## Scripting a multi-step sequence

Piping several commands into the REPL (`(echo cmd1; echo cmd2; ...) | wsdbg PORT`) sends them all
immediately by default - nothing waits for a response before moving to the next line, so scripts
traditionally needed `sleep N` between commands to guess how long each one takes. Pass `--sync`
to remove the guessing: each line blocks until its own response arrives (matched by ticket) before
the next line is read, and for `cpu.resume`/`cpu.stepInto`/`cpu.stepOver`/`cpu.stepOut`/
`cpu.runUntil`/`cpu.runUntilTime`/`cpu.nextHLE` it also waits for the following `cpu.stepping`
broadcast - the actual "the CPU stopped again" signal those commands imply. Everything still
prints as it arrives; this only changes when the *next* line gets sent. A breakpoint that never
trips would otherwise hang the script forever, so it gives up after `--sync-timeout` seconds
(default 30), reports it, and makes the run exit non-zero.

An `error` reply to a line doesn't stop the script, but makes the run exit non-zero at the end.

Matching is by ticket, always - `--sync` never waits for "whatever message arrives next", which is
what used to quietly desynchronise a script. So prefer the `key=value` form for nested parameters
too: values are parsed as JSON, and single quotes keep the inner double quotes intact, as in
`input.buttons.send buttons='{"cross":true}'`. A raw JSON line is sent exactly as written, so it's
waited for only if *you* gave it a `ticket`;
without one there is nothing to match and `--sync` moves straight on to the next line. Raw lines
are rejected up front, rather than sent and left to fail somewhere downstream, if they aren't valid
JSON, aren't an object, have no string `event`, or carry a `ticket` that isn't an integer.

```bash
(
  echo 'cpu.breakpoint.add address=134348800 enabled=true'
  echo 'cpu.resume'
  echo 'cpu.getAllRegs'
) | cargo run -- 12345 --sync
```

### Script directives

A script often needs to wait for something that isn't a direct response to the line before it.
Doing that by splitting the script across several `wsdbg` invocations costs a process, a TCP
connection and a handshake per pause, which is slow enough to matter - a polling loop built that
way took minutes per run. These run inside the one session instead:

| Directive | What it does |
|---|---|
| `:sleep <seconds>` | Wall-clock pause. Keeps draining and printing messages while it waits. |
| `:wait <event> [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. |
| `:screenshot <file.png>` | Saves `gpu.buffer.screenshot` as a PNG, without printing the image data. Needs the CPU stopped. Use a native path (`C:/...`) on Windows. Works in headless with `--graphics=vulkan`. |
| `:nowait <command>` | Sends the command and moves straight on, even under `--sync`; its reply prints whenever it arrives. For requests that only answer once the CPU has run, like `input.buttons.press`. |
| `:echo <text>` | Prints text, for marking up a script's output. |
| `# comment` | Ignored. |

`--compact` prints one line per message (`<- event {json}`) instead of pretty-printed JSON, and
drops the banner and prompt - much easier for a shell to grep. wsdbg exits non-zero if a `:wait`
timed out, so a script can be checked without parsing its output at all.

A complete repro - boot, get several seconds in, step, inspect - in one file and one connection:

```
# hand this to: wsdbg PORT --sync --compact --quiet < script.txt
:echo === run to 1.5s of emulated time ===
cpu.runUntilTime us=1500000
cpu.status
:echo === step twice ===
cpu.stepInto
cpu.stepInto
:quit
```

**Don't add `:wait cpu.stepping` after `cpu.runUntilTime` there** (an earlier version of this
example did). Under `--sync` those commands already wait for the `cpu.stepping` that follows, so an
explicit `:wait` blocks for a *second* one that never arrives and burns the entire
`--sync-timeout`. It is a nasty failure to diagnose, because the emulator has already stopped
exactly where you asked: the run just sits there, and a script written with `--sync-timeout 400`
takes seven minutes instead of three seconds while looking like a slow boot. `:wait` is for events
nothing else is waiting on - a breakpoint hit during a free run, say.

For the same reason, never put `"stepping":true` in `broadcast.config.set`'s `disallowed`:
`cpu.stepping` is a broadcast with no ticket of its own, and muting it means nothing can ever
observe that a run finished. `--quiet` above is the safe way to get the same noise reduction - it
disables `logger` and `input` only, and wsdbg warns if a hand-written `broadcast.config.set` mutes
`stepping`.

`cpu.runUntilTime` (see `docs/WebSocketDebugger.md`) is what makes that reproducible: it stops on
the requested emulated microsecond, so the same script reaches the same instruction every run.
Polling `cpu.status` in a loop instead lands somewhere different each time.

---

### Overview (`assets/lang/README.md`)

# lang

PPSSPP language ini repository

# Thanks for your interest in translating PPSSPP!

* Simply copy *en_US.ini* file to a new ini file with your language code, or use it to update an existing file with that name.
* To see a list of codes, view [this](http://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes) page.

# Please note, while translating:
* Ampersands `&` on the *RIGHT* side of an equals sign denote an underlined keyboard hotkey.
* The hotkeys are only supported currently in the *DesktopUI* section, however.
* Example: `&File`. This will make it so when you press ALT + F on Windows, it'll open the File menu.

# Tools

* To remove a translation, use the following (where KeyWord is the key):

    find . -type f -print0 | xargs -0 sed -i /^KeyWord/d

* To change a translation key, use something like this:

    find . -type f -print0 | xargs -0 sed -i /^Key/NewKey

* Before you commit, use git diff to check that you don't delete too much
  or some unrelated key with the same prefix.

### Happy translating!

---

### Overview (`docs/emu-api/README.md`)

# PPSSPP Emulator API

`ppsspp_emu_api.h` is a small, header-only C wrapper around the emulator-only tricks PPSSPP exposes
to homebrew through the `"emulator:"` device of `sceIoDevctl` (implemented in `Core/HLE/sceIo.cpp`,
search for `"emulator:"`).

None of this works on real PSP hardware - always check `ppsspp_is_emulator()` before relying on
anything else in the header, and keep a working fallback path for real hardware.

C API documentation: https://www.ppsspp.org/docs/development/ppsspp-internals/emu-api/

Raw `sceIoDevctl` protocol (commands, layouts, quirks): see [protocol.md](protocol.md).

## Usage

Just copy `ppsspp_emu_api.h` into a PSPSDK-based homebrew project and `#include` it. It's entirely
`static inline` functions over `sceIoDevctl`, so there's no library to build or link.

```c
#include "ppsspp_emu_api.h"

if (ppsspp_is_emulator()) {
    ppsspp_send_output_str("Hello from homebrew, running under PPSSPP!\n");
}
```

---

### Overview (`ext/jpge/README.md`)

# jpeg-compressor

This directory contains code from
https://github.com/richgel999/jpeg-compressor/tree/aeb7d3b463aa8228b87a28013c15ee50a7e6fcf3
with the following patches:

* https://github.com/richgel999/jpeg-compressor/pull/18

## License

The license of jpgd.cpp/.h and jpge.cpp/.h is either Public Domain or Apache 2.0. Choose whatever you want.

The license for the optional file jpgd_idct.h (and ONLY this file) is Copyright 2009 Intel Corporation:

Permission is granted to use, copy, distribute and prepare derivative works of
this software for any purpose and without fee, provided, that the above
copyright notice and this statement appear in all copies. Intel makes no
representations about the suitability of this software for any purpose. THIS
SOFTWARE IS PROVIDED "AS IS." INTEL SPECIFICALLY DISCLAIMS ALL WARRANTIES,
EXPRESS OR IMPLIED, AND ALL LIABILITY, INCLUDING CONSEQUENTIAL AND OTHER
INDIRECT DAMAGES, FOR THE USE OF THIS SOFTWARE, INCLUDING LIABILITY FOR
INFRINGEMENT OF ANY PROPRIETARY RIGHTS, AND INCLUDING THE WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Intel does not assume any
responsibility for any errors which may appear in this software nor any
responsibility to update it.

---

### Overview (`ext/naett-lib/README.md`)

# naett /nɛt:/

Tiny HTTP client library in C.

Wraps native HTTP client functionality on macOS, Windows, Linux, iOS and Android in a single, simple non-blocking API.

## Using `naett`

Get the `naett.c` and `naett.h` files and throw them into your project. Check out the [example](./example) for a basic `Makefile` - based setup.

The library needs to be initialized by a call to `naettInit()`. On Android, you need to provide a `JavaVM*` handle in the call to `naettInit()`.
On the other platforms, call with `NULL`.

See `naett.h` for reference docs.

## Platform implementations

`naett` uses the following HTTP client libraries on each platform:

| Platform | Library / component | Build with |
| --- | --- | --- |
| macOS, iOS | NSURLRequest | -framework Foundation |
| Windows | WinHTTP Sessions | -lwinhttp |
| Android | java.net.URL | NDK |
| Linux | libcurl | -lcurl -lpthread |

### Example

```C
#include "naett.h"
#include <unistd.h>
#include <stdio.h>

int main(int argc, char** argv) {
    naettInit(NULL);

    naettReq* req =
        naettRequest("https://foo.site.net", naettMethod("GET"), naettHeader("accept", "application/json"));

    naettRes* res = naettMake(req);

    while (!naettComplete(res)) {
        usleep(100 * 1000);
    }

    if (naettGetStatus(res) < 0) {
        printf("Request failed\n");
        return 1;
    }

    int bodyLength = 0;
    const char* body = naettGetBody(res, &bodyLength);

    printf("Got %d bytes of type '%s':\n", bodyLength, naettGetHeader(res, "Content-Type"));
    printf("%.100s\n...\n", body);
}
```

---

### CodeWarriorMangling (`docs/CodeWarriorMangling.md`)

# Metrowerks CodeWarrior C++ symbol mangling (PSP)

Some PSP titles were built with Metrowerks CodeWarrior rather than the SDK's GCC, so their
symbols aren't Itanium-mangled and `_Z`-based demanglers do nothing with them. PPSSPP
demangles them in `Common/Data/Text/Demangle.cpp` (`DemangleCodeWarrior`), which is what makes
the symbol map readable for those games.

The scheme is a descendant of the AT&T cfront mangling, and the basics match the Macintosh C++
ABI. It deviates in enough places that following that document alone produces wrong answers, so
this is a description of what PSP binaries actually contain.

## How this was worked out

From two PSP executables that shipped with intact symbol tables - one small C++ test program,
one large application of about 10,000 symbols - by demangling every symbol and checking the
result against the disassembly and against the bytes each data symbol points at.

Constructs the corpus did not contain are marked **(inferred)** below. They come from the cfront
scheme, are implemented, and are believed right, but nothing here proves them.

## Overall shape

A mangled symbol is:

```
<basename> __ [<class>] [<cv>] F <parameters> [_ <return type>]
```

with the class, the cv-qualifiers, the `F` and everything after it all optional. A few examples,
building up:

| Mangled | Demangled |
| --- | --- |
| `run_tests__Fv` | `run_tests()` |
| `getDistance__6KzUtilFP7st_unitP7st_unit` | `KzUtil::getDistance(st_unit *, st_unit *)` |
| `what__Q23std9exceptionCFv` | `std::exception::what() const` |
| `count__Q23foo3bar` | `foo::bar::count` |

The last one has no `F`, so it isn't a function at all - it's a static data member, and there is
no parameter list to print. A name with neither a class nor an `F` is not mangled.

### Finding the separator

The `__` separator is genuinely ambiguous. A base name can itself start with underscores
(`__SetupFrameInfo__FP12ThrowContext...`), contain a `__` of its own
(`__TableUnit__SetValue<i,1>__5shTbbF...`), and plain C code in the same binary is full of names
like `I3dCacheManager__GarbageCollect` that are not mangled at all.

There is no way to resolve this from the grammar; PPSSPP tries every `__` in the symbol and keeps
the first split whose right-hand side parses completely. Only if none does will it accept a split
whose parameters didn't decode - and then only when a class qualifier is present, because
otherwise every C name with a double underscore in it decodes as a garbage function signature.

## Names

An identifier is written as its length in decimal followed by its characters: `6KzUtil`.

A qualified name is `Q<count>` followed by that many identifiers: `Q23std9exception` is
`std::exception` (2 components, `3std` and `9exception`). The count is a single digit; ten or
more components are written `Q_<count>_` **(inferred)**.

### Template arguments

Template arguments are written literally between `<` and `>`, *inside* the length-prefixed name -
the length covers the whole thing, brackets included. (The Macintosh ABI document describes a
`__PT` prefix instead. PSP binaries do not use it.)

```
39CList<Q38hlScreen5Brwsr13CContentsUnit>     shList::CList<hlScreen::Brwsr::CContentsUnit>
```

Here `39` is the length of `CList<Q38hlScreen5Brwsr13CContentsUnit>`. Each argument is either a
mangled type, or a plain integer for a non-type parameter, separated by commas:

| Mangled | Demangled |
| --- | --- |
| `15CSimpleChar<24>` | `CSimpleChar<24>` |
| `61ForwardIterator<16TABLE_VAR_MEMBER,21TABLE_VAR_SECT_HEADER,v>` | `ForwardIterator<TABLE_VAR_MEMBER, TABLE_VAR_SECT_HEADER, void>` |
| `47CList<Q26ssTool29tag_<Q26shFont12SysCmdString>>` | `CList<ssTool::tag_<shFont::SysCmdString>>` |

They nest, so a parser must match brackets rather than scanning for the next `>`, and split
arguments only on top-level commas.

A *function* template puts its arguments in the base name instead, and - unlike a normal function -
encodes its return type after a trailing `_`:

```
sort<Pf>__3stdFPfPf_v          void std::sort<float *>(float *, float *)
```

### Special base names

| Base name | Meaning |
| --- | --- |
| `__ct` | Constructor; the name is taken from the class |
| `__dt` | Destructor |
| `__op<type>` | Conversion operator, e.g. `__opCi` is `operator const int()` |
| `__<code>` | Overloaded operator, see below |

Operator codes are the cfront set: `nw` `dl` `nwa` `dla` for `new`/`delete`, `pl` `mi` `ml` `dv`
`md` for arithmetic, `apl` `ami` `amu` `adv` `amd` `aad` `aor` `aer` `als` `ars` for the compound
assignments, `eq` `ne` `lt` `gt` `le` `ge` for comparisons, `aa` `oo` `nt` for logic, `ad` `or`
`er` `co` `ls` `rs` for bitwise, and `as` `pp` `mm` `cl` `vc` `rf` `rm` `cm` for
`=` `++` `--` `()` `[]` `->` `->*` `,`.

So `__as__9ANIMEDataFRC9ANIMEData` is `ANIMEData::operator=(const ANIMEData &)`.

## Types

Type codes are read left to right, each one modifying what follows.

| Code | Type | |
| --- | --- | --- |
| `v` | `void` | |
| `b` | `bool` | |
| `c` | `char` | |
| `s` | `short` | |
| `i` | `int` | |
| `l` | `long` | |
| `x` | `long long` | |
| `f` | `float` | |
| `d` | `double` | |
| `r` | `long double` | **(inferred)** |
| `w` | `wchar_t` | **(inferred)** - a Metrowerks addition to the cfront set |
| `e` | `...` | varargs, always last |
| `P` | pointer to | |
| `R` | reference to | |
| `C` | `const` | |
| `V` | `volatile` | |
| `U` | `unsigned` | |
| `S` | `signed` | **(inferred)** |
| `A<n>_` | array of `n` | **(inferred)** |
| `F<params>_<ret>` | function | |
| `<len><name>` | class | |
| `Q<n>...` | qualified class | |

`PCc` is `const char *`; `CPc` is `char * const`. A pointer to a function or an array needs the
declarator wrapped rather than a `*` appended - `PFi_i` is `int (*)(int)`, and `PPFv_v` is
`void (**)()`.

Two forms back-reference an earlier parameter of the same function **(inferred)**:

| Form | Meaning |
| --- | --- |
| `T<index>` | Same type as parameter `<index>`, 1-based |
| `N<count><index>` | `<count>` more parameters, each the type of parameter `<index>` |

So `foo__FPCcUiN21` is `foo(const char *, unsigned int, const char *, const char *)`.

### cv-qualifiers on the function

A `C` or `V` sits between the class and the `F`, and qualifies the member function rather than a
parameter: `InitRun__Q28shCamera11TStillParamVFv` is
`shCamera::TStillParam::InitRun() volatile`.

## Compiler-generated symbols

CodeWarrior emits a family of symbols for things with no C++ name of their own. These wrap a
mangled name rather than being one, and none of them are described in the ABI document.

| Form | Meaning | Example |
| --- | --- | --- |
| `__vt__<class>` | Vtable | `__vt__Q23std9exception` |
| `__RTTI__<class>` | Typeinfo record | `__RTTI__Q23std9exception` |
| `__sinit_<file>` | Static initializers for a translation unit | `__sinit_hl_app.cpp` |
| `__sterm_<file>` | Static destructors | |
| `@<n>@<symbol>` | `this`-adjusting thunk, `n` bytes | `@12@__dt__3SonFv` |
| `@STRING@<symbol>[@<n>]` | A string literal used inside that function | `@STRING@Get_BGM__Q25hlBHC4SBhcFi@0` |
| `@LOCAL@<symbol>@<var>[@<n>]` | A function-local static | `@LOCAL@sort<Pf>__3stdFPfPf_v@shuffle@0` |
| `@GUARD@<var>$<n>` | Its "already constructed" flag | `@GUARD@app$16079` |
| `@<n>` | An anonymous string constant | `@10046` |

The trailing `@<n>` on `@STRING@` and `@LOCAL@` is a discriminator, present only when a function
has more than one of them - so a parser has to treat it as optional, and must not mistake the
`@` before a `@LOCAL@` variable name for it.

`@<n>@` thunks really are thunks: the one above is 8 bytes of code that does `addiu a0, a0, -12`
and jumps, i.e. adjusts `this` for a secondary base. `@STRING@` symbols point at ordinary string
data - usually the `__FILE__` an assertion expanded to.

## Other decorations

Two more show up inside otherwise ordinary names. Neither needs decoding, but both are worth
recognising:

- `<name>$<digits><file>` - a class or variable with internal linkage, tagged with where it was
  declared: `ClutScreen$11229hl_cplayer_effect_renderer_cpp`. These make for very long symbols,
  since the tag repeats everywhere the type appears.
- `<len>@unnamed@<file>@` - used as a qualifier for file-scope statics, i.e. an unnamed namespace.
  `ARWMENU_MODEL_NAME__34@unnamed@hl_editor_arrow_menu_cpp@` is a data symbol in one.

## The hidden destructor parameter

**The mangling describes the source-level signature, not the generated one.** `__dt__3SonFv`
demangles to `Son::~Son()`, and that is the right answer for a demangler - but the function the
compiler emitted takes a second argument, and anything that turns a demangled name into a
function signature (a decompiler, a debugger's parameter view) will be wrong about it.

CodeWarrior emits one destructor per class rather than the two or three the Itanium ABI clones,
and distinguishes the cases with a flag. On PSP that flag is a **`short` in `a1`**, and the
destructor **returns `this` in `v0`**. So the real signature is:

```c
Son *__dt__3SonFv(Son *this, short flag);
```

Decompiled from a real one, the body is:

```c
Son *__dt__3SonFv(Son *this, short flag) {
    if (this) {                       // destructors are null-safe
        ... restore vtable pointers, destroy members ...
        __dt__3DadFv(this + 12, 0);   // base subobjects get 0
        __dt__3MomFv(this, 0);
        if (flag > 0)                 // "blez" - only a positive flag frees
            __dl__FPv(this);          // operator delete
    }
    return this;
}
```

The values that actually get passed, counted over the call sites in a retail binary:

| Flag | Meaning | Frequency |
| --- | --- | --- |
| `-1` | Destroy a complete object without freeing it - a stack object, a member, an explicit `~T()` call | Most calls |
| `1` | Destroy and free - what `delete` compiles to | A handful |
| `0` | Destroy a base-class subobject | A handful |

`0` and `-1` are a caller-side distinction: the destructors examined here only test the sign, so
both simply mean "don't free", but the compiler still passes `0` specifically when destroying a
base subobject and `-1` everywhere else. Treat `0` as "this call is destroying me as somebody's
base" when reading a call site, even though the callee can't tell.

Constructors in these binaries take only `this` (and also return it).

Note that the vtable slot holds this same function, flag and all, which is how CodeWarrior gets
away without separate deleting and non-deleting destructors: a virtual `delete` passes 1 through
the vtable.

## Hazards

- **A truncated name is unrecoverable.** Some symbol tables cap names (127 characters is a common
  limit), and templates blow past that easily. Once a name is cut, the length prefixes no longer
  match what's left and nothing after the cut can be trusted - better to reject the symbol than
  to print a plausible-looking guess.
- **Not everything with a `__` is mangled.** C code linked into the same binary uses `__` as a
  word separator freely. Requiring a class qualifier before accepting a partial parse is what
  keeps `I3dClut__FlushCache` from turning into `I3dClut(long, ...)`.
- **`e` is a real parameter, not a parse failure.** `Printf__Q26shFont4FontFiiPCce` ends in
  varargs; `(int, int, const char *, ...)` is the correct answer, not a partial one.

---

### DebuggerThreading (`docs/DebuggerThreading.md`)

# Debugger threading model (Core_RunOnCPUThread / g_frameMutex)

Which thread may touch CPU-thread-owned debugger state (breakpoints, symbol map, registers, memory,
kernel threads), and in which order the locks have to be taken. Read this before touching
`Windows/Debugger/*`, `Core/Debugger/WebSocket*` or anything else that reaches into core state from
outside the CPU thread.

CPU-thread-owned debugger state (`g_breakpoints`, `g_symbolMap`, `g_disassemblyManager`, registers,
memory, kernel threads) used to be touched directly from other threads (the WebSocket handler
thread, and the legacy Win32 debugger's message-pump thread) with no synchronization. Two
mechanisms now exist for doing this safely - pick based on whether you're mutating or just reading:

- **`Core_RunOnCPUThread(func)`** (`Core.h`/`Core.cpp`) - queues `func` to run on the CPU thread,
  blocking the caller until it's done. Use for *mutations* (breakpoint add/remove, register/memory
  writes, symbol map edits, stepping requests, thread wake/kill). Drained at the top of every
  `Core_RunLoopUntil()` iteration, so it's reached whether the CPU is running or stepping/paused;
  runs immediately if already called from the CPU thread. Two hard rules learned the hard way:
  never put a modal Win32 dialog call (`MessageBox`, `DialogBoxParam`, `InputBox_GetString`,
  `SomeDialog::exec()`) inside the queued lambda - it would block the CPU thread on user input, so
  split the function into "read/decide", "show modal", "mutate" pieces instead. And never call
  `SendMessage()` targeting one of your own GUI-thread windows from inside the lambda - the calling
  GUI thread is blocked waiting on the CPU thread rather than pumping messages, so a cross-thread
  `SendMessage()` back to it deadlocks; keep such calls outside the lambda instead.
- **`g_frameMutex`** (`Core.h`/`Core.cpp`) - a plain `std::mutex`, held by `NativeFrame()`
  (`UI/NativeApp.cpp`) only across the span where it actually touches that state
  (`g_breakpoints.Frame()` through `g_screenManager->render()` - where `Core_RunLoopUntil()`/actual
  CPU stepping happens - through `runImDebugger`/`renderImDebugger`), not across input handling or
  the present/frame-pacing waits. Use for *reads* invoked very frequently (`WM_PAINT`, a list
  reload triggered on every debugger-state-changed notification) where routing through
  `Core_RunOnCPUThread` would be too slow/heavy. The legacy Win32 debugger windows do this now -
  see `CtrlRegisterList::onPaint`, `CtrlDisAsmView::onPaint`, `CtrlMemView::onPaint`,
  `CtrlBreakpointList::reloadBreakpoints`, `CtrlThreadList::reloadThreads`, etc. in
  `Windows/Debugger/*.cpp`.

Architecture fact that makes `g_frameMutex` correct: regardless of graphics backend, `NativeFrame()`
(and thus CPU emulation via `Core_RunLoopUntil()`, and the Dear ImGui debugger) always runs on the
*same* thread - see `RunGraphicsLoop()` in `Core/EmuThread.cpp`, which every platform goes through
(Windows and headless via `MainThreadFunc()`, Android from its render loop thread). When a backend
needs its own thread for actual graphics API calls (`GraphicsContext::NeedsSeparateEmuThread()` -
true for OpenGL, SDL, headless, libretro, Qt; false for D3D11/Vulkan on Windows), the *original*
thread stays behind purely to pump `graphicsContext->ThreadFrame()` (i.e. just executes queued
graphics API calls), and a *newly spawned* thread takes over `NativeFrame()`/game logic/CPU duty. So `UI/ImDebugger/*.cpp` is always
safe to read/write this state directly, without either mechanism - it's always on the same thread
as `Core_RunLoopUntil()`. The legacy Win32 debugger is different: its dialogs are pumped by the
*original* `WinMain` message-loop thread, which is a genuinely separate OS thread from whichever
thread ends up running `NativeFrame()`/CPU, regardless of backend (this split happens one level
above the `NeedsSeparateEmuThread()` branch) - that's the whole reason it needed this mechanism.

**Update (2026-08-08): both `BreakpointManager`'s and `SymbolMap`'s internal mutexes have been
removed** after auditing every touchpoint across the codebase (WebSocket subscribers,
`Windows/Debugger/*.cpp`, `Windows/MainWindowMenu.cpp`/`Windows/MainWindow.cpp` main-window menu
items, `Core/Core.cpp`'s free-threaded `Core_Break`/`Core_Resume`, `UI/ImDebugger/*.cpp`,
JIT/interpreter backends, `Core/Debugger/MemBlockInfo.cpp`) and confirming each is covered by one of
the two mechanisms above or is already on the CPU/NativeFrame thread. Notably, `Windows/main.cpp`'s
`SortSymbols()` calls (fired from `System_Notify(BOOT_DONE)`/`System_Notify(SYMBOL_MAP_UPDATED)`)
turned out to already be safe without any change - both notifications are only ever fired from the
CPU/NativeFrame thread (`UI/EmuScreen.cpp`, `Core/HLE/sceKernelModule.cpp`), despite an old comment
there claiming reliance on the (now-removed) internal lock. `Qt/mainwindow.cpp`/`Qt/QtMain.cpp`
still poke at `g_symbolMap` directly and unguarded on the Qt UI thread - a pre-existing issue,
deliberately left alone since Qt isn't a maintained backend and is slated for removal; removing the
lock doesn't change `SymbolMap`'s public API, so Qt still builds, just without that safety net.
`GPU/Common/GPUDebugInterface.cpp` (GE debugger expression evaluation) and `Core/MemFault.cpp`
(crash-time diagnostics) also touch `g_symbolMap` and were deliberately not audited this round -
different subsystem / best-effort-by-nature respectively, follow up if they ever come up.

Two more things found while doing this:
- **`Core_RunOnCPUThread()`'s queue is only drained where `Core_RunLoopUntil()` runs, which requires
  a game to be loaded** (it's called from `EmuScreen::render()`). Calling `Core_RunOnCPUThread()`
  while at the main menu with nothing loaded used to hang forever. Fixed by also calling the
  (now public) `Core_ProcessCPUQueue()` directly from `NativeFrame()`, right before
  `g_screenManager->render()`, inside the same `g_frameMutex`-locked span - so it always runs, not
  just while a game is active.
- **Lock-ordering rule**: because `Core_ProcessCPUQueue()` is called from inside `NativeFrame()`'s
  `g_frameMutex`-locked span, any `Core_RunOnCPUThread()` lambda that itself tries to lock
  `g_frameMutex` (directly, or indirectly - e.g. by calling something like
  `CDisasm::NotifyMapLoaded()`, which locks it internally) will deadlock. Keep such calls outside
  the queued lambda, same as the modal-dialog and `SendMessage()` rules above.

## Lock ordering: `g_frameMutex` before `Core_LockAgainstShutdown()`, always

`Core_LockAgainstShutdown()` / `CoreShutdownLock` (a `recursive_mutex`, `g_shutdownLock` in
`Core/Core.cpp`) is held across `CPU_Shutdown()` and `Memory::Reinit()`, i.e. while the core is
going away. Take it on any thread other than the CPU thread before reading core state - emulated
memory, the symbol map, kernel objects - so none of it is freed mid-read. It was called
`Memory::Lock()` and only covered the memory map; the name misled people into thinking it was about
memory access. When a function needs both it and `g_frameMutex`, **take `g_frameMutex` first**.

The CPU thread's order is structural and can't be changed: `NativeFrame()` wraps everything below it
in `g_frameMutex`, and several things under there lock memory - `Core_ProcessCPUQueue()` running a
queued WebSocket handler, and `runImDebugger()` -> `ImMemView` -> `DisassembleRange()`. So the
GUI-thread side is the one that has to match. (Getting it backwards deadlocked for real: a paint
handler held the memory lock and waited for `g_frameMutex` while the CPU thread did the reverse.)

Also: **a `Core_RunOnCPUThread()` callback does not need `Core_LockAgainstShutdown()`** - teardown only happens
on the CPU thread itself (`Memory::Shutdown()` via `CPU_Shutdown()` <- `PSP_Shutdown()`, all callers
on that thread; `Memory::Reinit()` from `Memory::DoState()` on savestate load). Don't add one.

**The general rule behind both of these: never make the CPU thread wait for a thread that is (or may
be) waiting on the CPU thread.** `Core_RunOnCPUThread()` blocks until the CPU thread drains the
queue, so anything the CPU thread might block on must not be held across such a call. The WebSocket
debugger's `lifecycleLock` hit exactly this - it was held across a whole event handler, and the CPU
thread took it in `Core_NotifyLifecycle(STOPPING)`, so stopping a game with a debugger request in
flight hung both threads. That lock is gone now; the rule is what's left of it.

**The WebSocket debugger no longer has any lock guarding it against startup/shutdown, and must not
grow one back.** The invariant instead is: a handler either does its emulator-state access inside
`Core_RunOnCPUThread()` - which serializes it against startup and teardown, since those run on the
CPU thread too - or touches only state that carries its own lock (the log ring buffer, `ctrlMutex`,
`GPUStepping`'s pause-action rendezvous). When adding a subscriber, put the core access in the
queued callback, including the `isAlive()`/`IsValidAddress()` checks: answering those outside it
just means acting on an answer that may already be stale.

`game.*` and `cpu.stepping`/`cpu.resume` are pushed rather than polled - `WebSocketDebuggerTick()`
(called from `Core_ProcessCPUQueue()`) notices the transition on the CPU thread, formats the event
there, and drops it in a per-connection mailbox. Don't add a broadcaster that reads emulator state
from the connection's own thread; produce the event on the CPU thread and push it instead.

The Win32 debugger's GUI-thread readers *do* still need it, so don't "simplify" those away:
teardown is not yet fully inside the `g_frameMutex` span. `EmuScreen::render()`'s `PSP_Shutdown()` is
inside it, but the ones in `EmuScreen::sendMessage()` (`REQUEST_GAME_RESET`, loading a new game) run
from `g_screenManager->sendMessage()` in `NativeFrame()`, which sits *above* where the guard is
taken. Closing that hole - moving those shutdowns inside the span, or deferring them to render time -
is the prerequisite for dropping the shutdown lock from the debugger entirely.

Painting-problem design history, in case a similar tradeoff comes up elsewhere: routing every paint
through `Core_RunOnCPUThread` was rejected as too slow for something invoked continuously. A
per-window snapshot/cache with a per-row-rechecked `Core_IsStepping()` guard was tried first and
worked, but still had a narrow TOCTOU race (the CPU could resume between the check and that row's
reads) and the per-row-recheck pattern itself wasn't liked. Settled on `g_frameMutex` instead -
simpler, and actually race-free rather than just lower-risk. `CtrlRegisterList` shows live values
always now, grayed out by color alone (not cached) while the core is running, since a
constantly-moving value isn't meaningful to read closely anyway.

---

### HLEModules (`docs/HLEModules.md`)

# Adding HLE modules

How the HLE module tables work, the savestate-compatibility rules for adding to them, and the list of
build files a new source file has to be added to.

HLE module implementations live in `Core/HLE/sce<ModuleName>.cpp` / `.h` (e.g. `sceOpenPSID.cpp`, `scePauth.cpp` are good
small examples to copy from). A module is a `const HLEFunction <name>[]` table of
`{nid, &WrapX_YYY<func>, "funcName", retChar, argString}` entries, registered via
`RegisterHLEModule("<name>", ARRAY_SIZE(table), table)` inside a `Register_<name>()` function declared in the header.

- `FunctionWrappers.h` has generic `WrapX_YYY<func>` templates for common signatures (return type X, args YYY) - add new
  wrappers there if you need a new signature.
- A function that needs the CPU (registers beyond its arguments, the PC for a log line, `PARAM()`/`RETURN()`) takes
  `MIPSState *mips` as an extra first parameter, and the wrapper passes in the state the syscall runs on. Use that
  rather than `currentMIPS`. Calling such a function directly, or through `hleCall`, means passing a state yourself.
- Format string legend for the `retmask`/argmask chars: `x` = u32 (shown as hex), `i` = int/s32, `f` = float, `X` = u64,
  `I` = s64, `v` = void.
- For functions of genuinely unknown purpose (only known by NID), name them `<moduleName>_<NID>` and stub them with
  `return hleLogError(Log::HLE, 0, "UNIMPL");` - an established pattern (see `scePauth.cpp`, `sceOpenPSID.cpp`).
- **New modules must be registered at the very end** of the registration function in `Core/HLE/HLETables.cpp` (look for
  the `// add new modules here.` comment near the end of that function) - not inserted alphabetically/logically among
  the existing `Register_*()` calls. Module registration order affects numeric IDs used in savestates, so inserting a
  new module earlier in that list would break save-state compatibility for saves made with older builds.
- **The same rule applies one level down: new entries in an *existing* module's `const HLEFunction <name>[]` table must
  go at the very end of that array too, never inserted alphabetically or next to a "related" existing entry.** A
  resolved import gets written directly into guest RAM as a syscall opcode that encodes the entry's array position
  (`modulenum<<18 | funcindex<<6`, see `GetSyscallOp()` in `Core/HLE/HLE.cpp`), and a savestate captures that opcode
  verbatim - inserting a new entry before an existing one shifts every later entry's index, so loading an older
  savestate after such a change resolves those later entries to the *wrong* function. Adding an all-new table (a new
  module, including a new alias-module `..._driver[]` variant of an existing one) is unaffected, since no old
  savestate could reference indices into a table that didn't exist yet - only *existing* tables need this care.
- Remember to add any new `.cpp`/`.c` file to **seven** places: `Core/CMakeLists.txt`, `Core/Core.vcxproj`,
  `Core/Core.vcxproj.filters`, `UWP/CoreUWP/CoreUWP.vcxproj`, `UWP/CoreUWP/CoreUWP.vcxproj.filters`,
  `android/jni/Android.mk`, and `libretro/Makefile.common`. New `.h` files need the first five (everything except
  `Android.mk`/`Makefile.common`, which are plain compiled-source lists so headers don't go in them). Double check
  each by hand against how an existing neighboring file (e.g. `sceVaudio.cpp`) is listed. Forgetting the UWP entries
  is easy to miss - the CMake and MSBuild (`Core.vcxproj`) builds both succeed silently, and it only surfaces as a
  UWP-only build failure (this has happened for real: `Core/MIPS/InterpreterDispatch.cpp` landed without its UWP
  entries, and the omission wasn't caught until someone actually built the UWP project). Note: New files in the
  unittest project have to be updated in the unittest part in android/jni/Android.mk.

  Both the CMakeLists.txt change (via a Linux/Mac build) and the `Core.vcxproj`/UWP changes (via MSBuild on Windows)
  can actually be build-tested, not just eyeballed - see [Building and testing](building.md) for the main Windows solution,
  and for UWP specifically:
  ```powershell
  $installPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath
  $msbuild = "$installPath\MSBuild\Current\Bin\MSBuild.exe"
  & $msbuild "UWP\PPSSPP_UWP.sln" /t:CoreUWP /p:Configuration=Debug /p:Platform=x64 /m
  ```
  (only `android/jni/Android.mk` and `libretro/Makefile.common` genuinely can't be build-tested here - see
  [Building and testing](building.md) for what verification is possible for those.)

---

### POPS (`docs/POPS.md`)

# PS1 EBOOTs (POPS)

PS1 games on the PSP are EBOOT.PBPs run by the firmware's PS1 emulator. PPSSPP runs the firmware's own
modules for them, so it needs an installed 6.60 or 6.61 firmware (`flash0:/kd/popsman.prx`); without
one, booting a PS1 EBOOT fails with a message saying so. `--unpack-updater DIR EBOOT.PBP` installs one from an
official updater.

The code is `Core/HLE/PopsBoot.cpp` (the boot, the ME, the patches) and `Core/HW/PopsMMIO.cpp` (the
hardware registers the manager uses). The patches are for the 6.60 and 6.61 `popsman.prx` and `pops_0Xg.prx`,
whose code is the same (and the same at the patched places for the PSP-1000 and PSP-2000 models). The
manager's is checked when it's loaded. Offsets below are from the module's base.

## The boot

On hardware the kernel reboots into its POPS configuration, which starts the POPS manager
(`popsman.prx`) and a few user modules, and the kernel's init then calls `scePopsManLoadModule(path, 0)`,
which loads `pops_0Xg.prx` (the emulator) for the PSP model. `Load_PSP_POPS` does the same without the
reboot: it loads the manager, the user modules (`libfont_hv`, `pafmini`, `libpspvmc`) and a helper thread
that makes the init's call. `InitForKernel` reports a memory stick boot in the POPS key configuration.

The emulator draws through the GE and plays through the Media Engine. The manager drives both directly:

- **GE.** The emulator keeps one display list stalled and the manager extends it each frame by writing
  the stall register (`0xBD40010C`), then waits for the FINISH interrupt. These map onto the HLE GPU's
  current list.
- **ME.** A PS1 boot always gets the emulated Media Engine (`Core/MIPS/MeCore.cpp`), which runs the
  manager's code as the PSP does. The manager copies a bootstrap into the boot page (`0xBFC00040`),
  releases the ME's reset through `sceSysreg_driver` and waits for an acknowledgement in the mailbox
  (`0xBFC007F0`). Sony's own ME firmware isn't booted for it. The ME loop (+2F88) sets up the audio block
  (`0xBE000000`) and then, once per sample, sleeps in `halt` until the FIFO has room (interrupt source 12,
  a level: the loop never enables interrupts nor acks them), calls the emulator's mixer (on the ME) and
  writes the sample. The FIFO plays at 44.1kHz in emulated time, and its samples are mixed into the
  output (`MeMMIO::AudioMix`). The loop's calls to the cache functions do nothing on the ME.

Neither the lock the emulator shares with the ME (pops +5C) nor its SPU interrupt pacing is patched: with
two CPUs they should work as on hardware. Vib-Ribbon, which streams its stage music through the SPU's
capture buffers and asserts when those interrupts come at the wrong PS1 time, hasn't been tried.

Unresolved imports remain, mostly `sceSyscon_driver` and power management. The manager carries on
without them.

## Rendering

The emulator keeps the PS1's 1024x512 VRAM at `0x04000000` as one 16-bit framebuffer, and reads and writes
it outside where it draws. It's a fixed framebuffer: created whole, from memory, and uploaded without
filtering. Its 24-bit video is unpacked three bytes per pixel into `0x04100000`, which is kept at 1x. It
draws sprites on pixel centers, which above 1x read their neighbours' texels, so those are moved back half a
pixel. And it builds video frames by ORing passes together, so draws whose logic op is done in the shader
are drawn one at a time.

## Store and converted games

A store game's disc image is a PGD. The manager's license check (+266C) needs the game's license and
activation, so it's replaced: the version key comes from the PGD header, as CFW's popcorn plugin does.
The MACs are checked with the real algorithms (`sceAmctrl_driver`, on libkirk).

A converted EBOOT (popstation and similar) has a plain disc image of DEFLATE blocks and no MACs. As popcorn
does, the emulator's decoder calls go to an inflate, its CD audio index check takes the full-length branch,
and the two MAC checks (+0A28, +0AB8) pass.

For any EBOOT, also as popcorn does: the manual's name check passes, and an ICON0 the emulator can't use
(anything but an 80x80 PNG, such as a missing one or a PSP game's 144x80 one, which crashes it) reads as a
blank 80x80 one.

## Testing

```
PPSSPPHeadless --memstick=MEMSTICK --graphics=vulkan --timeout-wall=60 --timeout-emulated=60 \
    --press=30:0x8,40:0x4000 --screenshot-at=55:shot.png --dump-audio=out.wav EBOOT.PBP
```

The HOME button opens the emulator's menu. Savestates work.

## Not done

- Other firmware versions. CFW's popsloader runs POPS from 3.02 to 6.60, and its tables show what differs
  between them: the offsets patched here, and the kernel NIDs the modules import.
- The anti-LibCrypt word some PAL games need in a converted image (PSISOIMG + 0x12B0). ARK-4's popcorn has
  a table of them by disc ID, switched off. Converting tools can patch the image instead.
- ARK-4's per-game CONFIG.BIN next to the EBOOT, copied over the image's config (PSISOIMG + 0x420).
- A byte popcorn changes in reads of converted images (0x41B, from 0x27 to 0x55 when 0x41B-0x41E read
  27 19 22 41) without knowing why.

---

### PsarFileFormat (`docs/PsarFileFormat.md`)

# The PSAR archive format (PSP firmware updaters)

An official PSP firmware updater carries the whole firmware image in a PSAR archive. PPSSPP can
walk one and extract the files out of it without emulating the updater, which is what
`Core/Util/PSARUnpack.cpp` does. The main use is getting `flash0:/font` - the PGF system fonts -
out of an updater the user already has, since most UMDs carry one.

This document is what was learned reading 43 different firmware versions, from 1.50 to 6.61. It's
a description of the format rather than of the code; the code is the same shape but has the error
handling.

## Where an updater lives

The archive turns up in three shapes, all of them the same bytes once you find them:

| Shape | Where | Notes |
| --- | --- | --- |
| Downloaded updater | `EBOOT.PBP`, subfile 7 (`DATA.PSAR`) | The one you get from Sony's site |
| Disc updater | `PSP_GAME/SYSDIR/UPDATE/DATA.BIN` | Not wrapped in a PBP - the bare archive |
| Loose | any file starting with `PSAR` | Whatever someone already pulled out |

A disc updater directory also holds `EBOOT.BIN` (the updater executable, a bare `~PSP` PRX, of no
use to us) and `PARAM.SFO`. That SFO is worth knowing about: its `TITLE` reads
`"PSP™ Update ver 3.95"`, so **the firmware version can be read without decrypting anything**,
which is cheap enough to check a whole game library with. `DISC_ID` is `MSTKUPDATE` and `CATEGORY`
is `MG`.

Of 589 discs scanned across two libraries, 473 carried an updater.

## Archive header

```
+0x00  u32   "PSAR" magic (0x52415350)
+0x04  u8    version. 1 = "oldschool" (see below), 2 and 3 are the common ones
+0x08  u32   total length of the records
+0x20  u32   0x2C333333 if this archive is already decrypted, otherwise part of the ciphertext
```

The length at +0x08 matters: archives have a few bytes of padding after the last record (16 and 32
bytes in the two checked), and a walk that goes by file size instead will try to decode that
padding as a record and report a spurious failure at the very end.

The marker at +0x20 only appears in archives some other tool has already decrypted. In an original
updater those bytes are ciphertext and will not match.

## Records

The archive is a flat sequence of records starting at +0x10. There is no index - you find entry
*n+1* by decoding entry *n* and adding up its sizes. Each record is:

```
[ 0x150 bytes  encryption header ]     <- absent if the archive is pre-decrypted
[ 0x110 bytes  entry description  ]
[ variable     file contents      ]    <- has its own 0x150 header, same as above
```

Call the header size *overhead*: 0x150 normally, 0 for a pre-decrypted archive.

### Decoding a block

Every encrypted block - entry descriptions and file contents alike - is decoded the same way:

1. **Demangle.** The 0x130 bytes at block+0x20 are AES-128-CBC encrypted on top of everything
   else. Decrypt them with KIRK command 7 (`KIRK_CMD_DECRYPT_IV_0`), key seed **0x55**, IV zero,
   writing the result back over block+0x20. This step is skipped for version 1 archives.

   The point of it is that it hides the PRX tag: you cannot tell how to decrypt the block until
   you have done this.

2. **PRX decrypt.** The block is now an ordinary PRX blob, with its tag at block+0xD0. Run it
   through the normal PRX decrypter (`pspDecryptPRX`). The tag official updaters use is
   **0x0E000000**; `0x06000000` also appears in the wild.

   Note that `0x0E000000`'s key is stored *unscrambled*, unlike almost every other key in
   PPSSPP's tag table, so it needs the `kirk7` pass applied before use - that's what the
   `scrambleKey` flag on `TAG_INFO` is for.

One wrinkle worth knowing: the decrypter reads a little past the end of the block and **uses what
it finds there**, so a decoder should hand it about 16 bytes of slack from the archive. The last
record ends flush with the end of the archive and has no slack to give; zero-filling it there is
fine, but zero-filling it everywhere breaks every archive that does have those bytes.

### Entry description

0x110 bytes once decoded:

```
+0x004  char[]  entry name, NUL padded (see "Naming" below)
+0x100  u32     always zero. If it isn't, you've lost your place in the archive
+0x104  u32     size of the contents block that follows, including its 0x150 header
+0x108  u32     uncompressed size of the file. 0 means this entry is a directory
```

A directory entry still has a contents block to skip over - advance by `+0x104` either way.

### The first two records

The first record (at +0x10) is not a file. Decoded, it holds a text string at +0x10 whose last
comma-separated field is the firmware version, e.g. `...,6.61`. That's the authoritative version
for choosing decryption keys further down.

There is a second record after it whose length isn't recorded anywhere. In practice it is
`overhead + 100`, or `overhead + 144` on 2.7x, or `overhead + u16 at +0x90 of the first record`.
Try them in that order and take the first that decodes. Round the result up to a multiple of 16
before advancing. Version 1 archives don't have this record at all.

## Contents

The contents block decodes to a compressed stream. Sniff the first bytes:

| Magic | Format |
| --- | --- |
| `78 9C` | zlib |
| `KL4E` | KL4E - Sony's own, not implemented in PPSSPP |
| `KL3E` | KL3E - likewise |
| `2RLZ` | LZR |

Across all 43 firmware versions examined, **every file in every archive was zlib**. KL4E does turn
up on the PSP, but a layer further in: two kernel modules (`memlmd_01g.prx` and `loadexec_01g.prx`)
are themselves KL4E-compressed *inside* their own PRX, which is a separate problem from this one.

## Naming

This is the part that changed most across firmware generations, and the part most likely to catch
out an implementation tested against only one updater.

### 1.x and 2.x - real paths

Entries are named outright: `flash0:/vsh/resource/opening_plugin.rco`, `flash1:/registry`,
`ipl:/psp_nandipl.bin`. Nothing else is needed. Note `ipl:` - it is not enough to recognise
`flash0:` and `flash1:`; better to treat any `<device>:/` shape as a real path.

### 3.x - grouped short names

Entries are named `<group>:<5 digits>`, where the group is `com` for files every model gets, or
`01g`, `02g`, ... for one model's. `<group>:00000` is that group's **file list**, and the rest of
that group's entries are looked up in it by their number alone (entry `com:00004` is key `00004`).

### 5.x and 6.x - flat short names with per-model lists

Entries are a bare 5-digit number. The low numbers are file lists, one per PSP model
(`00001` = 01g, `00002` = 02g, ...) and the rest are files that any of the lists may name.

**Which numbers are lists is not fixed.** 6.61 uses 1-11; 6.00 has real files at `00010`-`00012`.
Since a list always decrypts successfully (the PRX layer inside validates a hash), the robust rule
is: try to decrypt a candidate as a list, and if that fails, treat it as an ordinary file.

### File lists

A list decodes to plain text, one `shortname<separator>path` per line. Both the separator and the
path style depend on the generation:

| Generation | Separator | Path written as |
| --- | --- | --- |
| 3.x | `\|` | `flash0/font/ltn0.pgf` |
| 5.x, 6.x | `,` | `flash0:/font/ltn0.pgf` |

Worth normalising to one form early - otherwise a filter like `flash0:/font/` silently matches
nothing on a 3.x archive.

### List encryption

The lists are encrypted on top of everything else, with **DES** - the tables in the reference
implementations are DES's own: a 56-entry PC-1, a 48-entry PC-2, eight 64-entry 4-bit S-boxes, a
32-entry P permutation, and textbook IP/FP bit-shuffling constants.

It is DES-CBC decrypt, with the key assembled from two 32-bit words as `(high << 32) | low` in
big-endian byte order, and an IV alongside them. There is one such set per firmware series, and
which one applies is chosen from the version string in the archive's first record:

| Firmware | Key set |
| --- | --- |
| 1.x, 2.x, 3.0 - 3.7 | 0 |
| 3.8, 3.9 | 1 |
| 4.x | 2 |
| 5.x | 3 |
| 6.x | 4 |

A sixth set exists that nothing selects by version. The values themselves are in `kTableKeys` in
`Core/Util/PSARUnpack.cpp` and aren't repeated here.

Underneath the DES layer is an ordinary PRX blob, so run the result through `pspDecryptPRX` as
well; what comes out of *that* is the text.

A good check while implementing: after the DES pass but before the PRX pass, the u32 at +0xD0
should be a recognisable PRX tag. A wrong key gives a random value there, so that single word
tells you whether the cipher is right before anything else has to work.

## What the archives look like in practice

Three structural eras, visible in the shape of the output:

| Firmware | Naming | Directory entries | Fonts |
| --- | --- | --- | --- |
| 1.50 - 3.52 | real paths | 0-16 | 17-20 |
| 3.71 - 4.05 | `com:`/`NNg:` groups | 50 | 21 |
| 5.01 - 6.61 | flat + per-model lists | 16-17 | 21 |

A 6.61 archive holds 435 records: 411 files, 17 directories and 7 file lists. Among the files are
295 `~PSP` modules, 61 PRF files, 7 encrypted XMB indices (`PSPsysGP`), and 21 files in
`flash0:/font` - 19 of them PGF, plus `gb3s1518.bwfon` and `imagefont.bin`.

The "Fonts" column above counts files in `flash0:/font`, not PGFs specifically.

## Gotchas, collected

- Walk by the length at +0x08, not the file size, or the trailing padding decodes as a bad record.
- Stop when fewer than `overhead + 0x110` bytes remain rather than when the position passes the
  end - the two differ by exactly one spurious record.
- Give the block decrypter its 16 bytes of slack, but don't require them for the last record.
- Don't assume which entry numbers are file lists; find out by trying.
- Don't assume there are file lists at all - 1.x and 2.x have none.
- `ipl:` is a device too.
- A 3.x path has no colon; a 6.x path does.

## See also

- `Core/Util/PSARUnpack.{cpp,h}` - the implementation, and the only consumer of most of this.
- `Core/ELF/PrxDecrypter.cpp` - the PRX layer every block goes through.
- `ext/libkirk` - `kirk7()` is the demangle step.
- `PPSSPPHeadless --unpack-updater=DIR <updater|disc|PSAR>` unpacks one from the command line;
  `--unpack-updater-model=01g..12g` picks a model.

---

### SNSystemsMangling (`docs/SNSystemsMangling.md`)

# SN Systems (SNC / ProDG) C++ symbol mangling (PSP)

A number of PSP titles were built with SN Systems' compiler rather than the SDK's GCC. Its C++
symbols look nothing like the Itanium mangling, and nothing like the cfront-derived one
CodeWarrior uses either - they are much more compact, and encode lengths as letters rather than
digits. PPSSPP demangles them in `Common/Data/Text/Demangle.cpp` (`DemangleSNSystems`).

## How this was worked out

From a single PSP executable that shipped with its symbol table intact: 4,662 mangled symbols
out of 11,338, covering an application, an STL, a JPEG decoder and a sound library. Everything
below is attested there except where marked **(inferred)**, which means the code implements it
by analogy with the codes that are attested and nothing in that binary proves it.

The description is complete enough to decode 4,661 of the 4,662. The one holdout is an STL
symbol whose template argument is a reference to a member of another template.

## Overall shape

```
__0 <kind> <name…> <parameters> [_ <return type>] [<qualifier>]
```

The kind character says what the symbol is, and - importantly - how many name components to
read, since nothing else delimits them:

| Kind | Meaning | Name components |
| --- | --- | --- |
| `f` | Member function | class, then function |
| `F` | Free function | function |
| `o` | Member operator, constructor or destructor | class, then a two-letter code |
| `O` | Global operator | just the two-letter code |
| `d` | Data | class, then variable |

```
__0fLCHeapMemoryFAlloci          CHeapMemory::Alloc(int)
__0FHGetRandv                    GetRand()
__0oLCHeapMemorydtv              CHeapMemory::~CHeapMemory()
__0OnwUi                         operator new(unsigned int)
__0dLCHeapMemoryG__vtbl          CHeapMemory::__vtbl
```

## Names

An identifier is a **letter giving its length**, then that many characters. `A` is 0, so `F` is
5 and `L` is 11; lowercase continues the run, with `a` = 26 up to `z` = 51.

```
LCHeapMemory   ->  L = 11  ->  "CHeapMemory"
FAlloc         ->  F = 5   ->  "Alloc"
bMatrixScaleRotationXZYTrans  ->  b = 27
```

A `5` before a component makes it an enclosing scope, and any number of them can stack up
before the component the kind character called for:

```
__0f5FsoundMCSoundPlayerPGetPlayerObjectUi
       ^ namespace "sound", class "CSoundPlayer", function "GetPlayerObject"
   sound::CSoundPlayer::GetPlayerObject(unsigned int)
```

### Template arguments

`7` opens a template argument list and `_` closes it. Each argument is a type, or `4` and an
integer for a non-type parameter. They nest.

```
6KTFixString748D512_                 TFixString<512>
6FTRect7f_                           TRect<float>
6KTFixVector76KTFixString7400Y_4g_   TFixVector<TFixString<128>, 32>
```

`9<index>A` refers back to one of the enclosing name's template arguments, 1-based, so `9BA` is
the first. The trailing letter appears to be a nesting level and is always `A` in practice.

```
__0FDmax7f_RC9BATB_RC9BA    const float & max<float>(const float &, const float &)
```

### Integers

Non-type template arguments and array bounds use a compact encoding: a run of `0`s, each worth
52, followed by a letter for the remainder.

| Written | Value | |
| --- | --- | --- |
| `Q` | 16 | |
| `g` | 32 | (lowercase, so 26 + 6) |
| `0M` | 64 | 52 + 12 |
| `00Y` | 128 | 104 + 24 |
| `0000w` | 256 | 208 + 48 |

Values that would need too many `0`s switch to an escape: `8`, a length letter, then that many
decimal digits. `8D512` is 512 and `8E1024` is 1024.

### Operators

For kinds `o` and `O` the name ends with a two-letter code instead of a length-prefixed
identifier. `ct` and `dt` are the constructor and destructor - the class name isn't repeated,
so it has to be taken from the qualifier. `nw` and `dl` take an optional trailing `a` for the
array forms.

Attested: `ct` `dt` `as` `eq` `ne` `ls` `nw` `nwa` `dl` `dla`. The rest of the usual set
(`pl` `mi` `ml` `dv` `md`, the compound assignments, comparisons, `cl`, `vc`, `rf`, …) is
implemented by analogy **(inferred)**.

## Types

| Code | Type | |
| --- | --- | --- |
| `v` | `void` | |
| `b` | `bool` | |
| `c` | `char` | |
| `s` | `short` | |
| `i` | `int` | |
| `l` | `long` | |
| `x` | `long long` | |
| `f` | `float` | |
| `d` | `double` | |
| `r` | `long double` | **(inferred)** |
| `w` | `wchar_t` | **(inferred)** |
| `e` | `...` | varargs |
| `P` | pointer to | |
| `R` | reference to | |
| `C` | `const` | |
| `V` | `volatile` | **(inferred)** |
| `U` | `unsigned` | |
| `S` | `signed` | **(inferred)** |
| `A<n>` | array of `n` | |
| `F<params>_<ret>` | function | |
| `M<class>` | pointer to member of `<class>` | |
| `6<name>` | class, struct or enum | |
| `9<index>A` | template argument `<index>` | |

Note that a class type in a parameter is introduced by `6`, while the symbol's own name isn't -
the kind character has already said a name is coming.

Declarators need wrapping rather than suffixing: `PFv_v` is `void (*)()`, `PPA0Ms` is
`short (**)[64]`, and `PM<class>F…_…` is `ret (Class::*)(args)`.

### Parameter back-references

Two forms repeat an earlier parameter of the same function, both indexed 1-based with letters:

| Form | Meaning |
| --- | --- |
| `T<index>` | Same type as parameter `<index>` |
| `N<count><index>` | `<count>` more parameters, each the type of parameter `<index>` |

```
__0FKRadixSortSPUiNCBUi
   RadixSortS(unsigned int *, unsigned int *, unsigned int *, unsigned int)
```

`NCB` is "two more of parameter 1". Note that `T` is also the static marker below, so a `T`
whose following letter isn't a valid parameter index ends the list rather than repeating one.

## Return types and qualifiers

A `_` after the parameters introduces the return type. Only templates carry one - the same rule
as the other manglings, since two instantiations can differ in return type alone.

After that, a trailing `K` marks a const member function. A trailing `T` marks a **static**
member function: it appears only on kinds `f` and `o`, never on a free function (which wouldn't
need marking), and every member `operator new`/`operator delete` in the corpus carries it,
which is exactly the set of member functions that are implicitly static.

One more marker, `__S`, turns up between the name and the parameters on a couple of template
members. What it means is unknown; it doesn't affect the name, so the demangler skips it.

## Compiler-generated symbols

| Form | Meaning |
| --- | --- |
| `__TID_<type>` | Type id - a one-byte object whose *address* is the type's identity |
| `__T_<type>` | Typeinfo record - 20 bytes, pointing at the name and the base class |
| `__sti__<file>_cpp` | Static initializers for a translation unit |
| `<class>::__vtbl` | Vtable, as an ordinary `__0d` data symbol |

`__TID_` and `__T_` are the two halves of the RTTI for one class, and which is which is
inferred from their sizes rather than from anything that names them. Both take a type rather
than a symbol, and - unlike everything above - spell an enclosing namespace as just another
component instead of marking it with a `5`:

```
__TID_DstdIbad_cast    type id for std::bad_cast
__TID_v                type id for void
```

Secondary vtables for multiple inheritance use a different scheme again, with decimal lengths:
`__vtbl___10CRefObject__5FsoundNCSoundRequest`. Only a handful exist and PPSSPP leaves them
alone.

## Hazards

- **A lot of C runtime code links into the same binary**, and much of it starts with a double
  underscore (`__adddf3`, `__malloc_lock`, `__sti`). Rejecting a symbol that doesn't parse
  completely is what keeps those intact.
- **The kind character is the only thing that says where the name ends.** There is no separator
  between the last name component and the first parameter, and uppercase letters are both
  lengths and type codes, so a parser that guesses will read the wrong thing and still consume
  a plausible number of characters.
- **`T` is overloaded** - a parameter back-reference in the middle of the list, and the static
  marker at the end.

---

### VSHBootInvestigation (`docs/VSHBootInvestigation.md`)

# VSH Boot Investigation

Notes on booting the PSP's Visual Shell (VSH / `vshmain.prx`) in PPSSPP via the direct HLE boot
path (`--vsh`), using a real Sony firmware dump (not checked into the repo - placed locally under
`assets/flash0/`, with `assets/flash1/` alongside it if you have one). This is exploratory
reverse-engineering of Sony's shipped, undocumented System Software, not a supported PPSSPP
feature - treat conclusions here as best-effort, not verified against hardware.

## Start here

One command boots VSH, runs to a fixed point, and tells you where it got. It starts the emulator,
finds the debugger port, connects, and cleans up after itself - no wrapper script needed:

```bash
printf 'cpu.runUntilTime us=6000000\ncpu.status\n:quit\n' | \
  Tools/wsdbg/target/release/wsdbg.exe --sync --compact --quiet \
    --launch Windows/x64/Debug/PPSSPPHeadless.exe --vsh --cpu=interpreter --graphics=vulkan --loglevel=3 2>emu.log
```

That takes a few seconds. Protocol replies come back on stdout, the emulator's log on stderr.
Put more commands in the script to inspect state once it stops. Three things to know:

1. **Don't add `:wait cpu.stepping` after `cpu.runUntilTime`** (or any resume/step command).
   `--sync` already waits for it; a second one never arrives and burns the whole timeout. This is
   the single most likely reason a run seems to take minutes - emulation itself is ~real time.
2. **`--cpu=interpreter`** is what this path is known to work with, and breakpoints are most
   reliable there.
3. Boot needs a real firmware dump in `assets/flash0/` (and optionally `assets/flash1/`), neither
   of which is in the repo.

## Why it opens on a disc error

The shell reaches the interactive XMB, but first shows "This disc cannot be started. The region
code is not correct." Cancel it with circle and the menu is there and usable.

Two separate things:

- **It thought a disc was inserted**, because nothing in PPSSPP modelled an empty drive. Now a shell
  booted without a game starts with an empty drive: `sceUmdActivate` tells the shell's drive callback
  (`SceVshMediaDetectUMD`) there's no disc, and `sceIoDevctl` `0x01F20001` (get disc type) answers `0`,
  as JPCSP does. Activation used to report a readable disc regardless, so the shell asked for its type,
  got nothing it knew, and showed "The disc could not be read."

  A disc goes in with the WebSocket debugger's `game.umd.insert` (`path`), which mounts the image
  and calls the drive callback the way a PSP does when a disc is inserted, and comes out with
  `game.umd.eject`. The shell then lists the disc in the Game column with its icon, background and
  animated preview, and launching it boots the game through `sceKernelLoadExecVSHDisc` (Daxter on
  6.61, to its title menu). In the app, the pause screen offers "Insert UMD" and "Eject UMD" while
  the XMB is running.

  UMD Video discs work too: the disc type comes from what's on the disc (`PSP_GAME`, `UMD_VIDEO`,
  `UMD_AUDIO`), the shell lists a video disc in the Video column, and selecting it hands it to the
  firmware's own player (`video_plugin`, `video_main_plugin`, with `mpeg_vsh.prx`). Bourne Supremacy
  and the Spider-Man 2 trailer on the EUR Video Demo Disc play with sound. What that player needed
  from our Media Engine HLE (per-picture SEI records, keyframe flags, field conversion in
  `sceMpegBaseCscAvc`) is in pspautotests `video/mpeg/avcdetail` and `video/mpeg/cscavc`.
- **It then asked whether the region matched and we didn't answer.** `sceIoDevctl` `0x01E18030` is
  the region check, and unusually it answers through the *return value* (1 matches, 0 doesn't)
  rather than an output buffer. It was unimplemented, so the VSH read the failure as a mismatch and
  retried 12 times. Implemented now - PPSSPP has no region-locked discs, so anything it can load is
  something it should run - and the call drops to one.

## Around the XMB (6.61)

- **Game**: the memory stick list shows PSP games, PS1 EBOOTs and homebrew with icons and info.
  Homebrew and PS1 games launch from it; PSN titles stop at "The copyright protection information
  is invalid (8010850E)", since there's no licence (`PSP/LICENSE/*.RIF`). A game that exits goes
  back to the XMB, as on a PSP. The Saved Data Utility lists saves: it needed the raw stick device
  `msstor0p1:/` (pspautotests `io/msstor/msstor`) and `sceSdGetLastIndex` without a key.
- **Photo**: folders under `PICTURE/` list with thumbnails, and JPEG and PNG open full screen (R
  for the next one).
- **Music**: MP3 plays. SensMe stops at 8002013A.
- **Settings**: the menus open. System Information shows "-" for the MAC address: wlan.prx reads it
  from IdStorage key 0x44, and IdStorage is left failing on purpose (see below).
- **Network**: the browser and the online manuals reach their Network Connection screen once
  `sceNetApctl_lib` is there, then stop at 8002013A on `sceHttp` functions only the NetFront browser
  uses (06488A1C, 2C3C82CF, 3A67F306, 739C2D79, 77EE5319, 8046E250, A461A167, B0257723).

## Current status

`--vsh` boots VSH and renders real XMB frames. Measured on the GE command stream after 25
emulated seconds: 8 draw commands (two `DRAW SPLINE 16x9`, a `DRAW BEZIER 4x4`, four textured
`DRAW PRIM TRI_STRIP` - the wave background and icon quads), 41 texture binds, and full
PROJECTION/World/VIEW transform setup. `opening_plugin_module`, `sceMpegVsh_library` and
`impose_plugin_module` load and start.

**It reaches the interactive XMB.** Up to 2026-08-19 it showed the logo and wave animation and then
a full-screen red error screen, which turned out to be the XMB index failing to decrypt (below).
With that fixed, and with the disc region check answered (above), the shell opens on a dismissable
disc error and the menu behind it works.

Note that framebuffer readback does not work under headless on either backend -
`gpu.buffer.screenshot` and `gpu.buffer.renderColor` both answer "Could not download output" - so
checking what is actually on screen means running the app build. From headless, the useful proxy is
the per-frame display list signature (see the red error screen section): a screen that is finished
changing repeats byte-identically, and a live menu does not.

## Which firmware versions work

**All 40 of them.** Every version that ships on a UMD - 1.50, 1.52, 2.00, 2.50, 2.60, 2.71, 2.80,
2.81, 2.82, 3.03, 3.11, 3.30, 3.40, 3.50, 3.51, 3.52, 3.71, 3.72, 3.73, 3.80, 3.90, 3.95, 3.96,
4.01, 4.05, 5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10, 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60 -
plus the download-only 6.61, reaches an interactive XMB with no error on screen.

An earlier pass through this also concluded "all of them", but what it measured was that the shell
*renders*, and several versions did that while showing nothing but the wallpaper or a full-screen
error. The check now is a picture: boot with `--vsh --graphics=software --screenshot-save` and look
at it. Two things about that:

- **`--timeout` is wall-clock seconds, not emulated ones.** A shell drawing a full XMB runs far
  slower than one stuck on a flat background, so a short timeout makes a working version look
  stuck. 120 seconds is comfortable for all of them.
- **Judge by the image, not the file size** - though the failure modes do have recognisable sizes,
  and none of them is subtle.

### What was in the way, in the order it was found

Five distinct causes, each of which stopped a whole band of versions:

- **Three kernel calls under NIDs we didn't have.** `sceKernelLoadModuleVSH` is how the shell loads
  its own plugins, so without it nothing drawable ever loads and the screen stays black; it has six
  NIDs across the range. Same story for `sceKernelGetModel` (seven) and `sceImposeSetStatus` (five).
  An unresolved GetModel meant the shell read a garbage model number and went looking for PSP-3000
  resources on a dump that has none.

  Finding them is mechanical, and worth writing down because it generalises. For each firmware,
  disassemble the module that exports the function and look for the body you already know from
  6.61: `sceKernelLoadModuleVSH` is the `modulemgr.prx` export whose callees are
  `sceKernelIsIntrContext`, `sceIoOpen`/`Ioctl`/`Close` and `sceKernelGetUserLevel`;
  `sceKernelGetModel` is whatever `vshbridge.prx` wraps in a `sceKernelGetUserLevel() < 4` check
  and re-exports. Several exports share that shape, so cross-check against the boot log: the one
  that matters is the import the shell actually calls, which the "Unknown syscall (run)" lines name.

- **Missing `sceResmgr` keys.** `flash0:/vsh/etc/index_XXg.dat` is the index of what the XMB shows
  and is encrypted; a shell whose key is missing loads every resource successfully and still has
  nothing to put in the menu, so it gives up with the red error screen. Each generation has its own
  tag, one per PSP model from 5.03 on and a single unsuffixed `index.dat` before that. 1.50 through
  2.50 don't use it at all.

  The keys are in each firmware's own `mesg_led*.prx`, in a table of 24-byte entries - 4-byte tag,
  16-byte key, 4 bytes of padding. Search the decrypted module for the tag as a little-endian word
  and read the next 16 bytes. **Validate the hit before believing it**: locate a tag that
  `PrxDecrypter.cpp` already has a key for and check it matches byte for byte. Extracting the 6.6x
  triple this way reproduces `keys_9DC14891_1/2/3` exactly, which is what established the layout in
  the first place. Don't try to walk the table blind - guessing the grid alignment produces
  plausible-looking garbage out of ordinary MIPS code, and the 2.5x-era table isn't on that grid
  at all.

- **`sceImposeGetParam(0x40000000)`.** A real setting in every `impose.prx` from 1.50 to 5.55, which
  we rejected as not a parameter at all. 3.11's shell read the error back, blanked the display with
  `sceDisplaySetFrameBuf(0, 0, 0)` and never turned it on again - so the screen kept whatever
  uninitialised VRAM held while a perfectly healthy frame loop ran behind it. 2.00, 3.03 and 3.11
  all turned on this one value.

- **`category_version` in the registry.** Our `sceReg` serves a compiled-in snapshot of a 6.6x PSP's
  registry, `/REGISTRY/category_version` included. Every shell checks it and treats a version higher
  than the schema it knows as a corrupt registry, offering to reset your settings instead of
  booting - which is what 1.50 through 5.50 did. The check is one-directional, older is always
  accepted, and nothing tried to migrate anything, so we report 1.

- **`sceRegCloseRegistry` dropping categories another opener still owned.** See `sceReg.cpp`; the
  VSH's alarm scan nests registry opens and this cost it its own open category.

### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60

Not offsets - NIDs. A function PPSSPP HLEs under its 6.6x `*_driver` NID is unrecognized on an
older build, so the import resolves to the **real firmware module** instead, and the real module
goes places the emulator can't follow.

| Function | 6.60/6.61 | 6.31-6.39 | 6.00-6.20 | 5.03-5.55 | 3.95-4.05 | 3.72-3.90 | 3.71 | 1.50-3.51 |
|---|---|---|---|---|---|---|---|---|
| `sceRtc_driver` `sceRtcSetAlarmTick` | `E09880CF` | `54B9C589` | `68AED59A` | `ADAF231F` | `55AC1C23` | `827BCB3F` | `329E8E3A` | `7D1FBED3` |
| `sceHprm_driver` `sceHprmReadLatch` | `E9B776BE` | `A3A87975` | `5FC5E53B` | `605DEA7A` | `A6E8D4F0` | `8C728076` | `F0AA1FB9` | `40D2F9F0` |

The rtc one is the interesting failure. Without the HLE, the VSH's alarm call ran the real
`rtc.prx`, which called on into `syscon.prx` and blocked forever on a `SceSysconSync` semaphore.
The symptom was a boot where every thread was parked and `idle0` was running, and the tell was a
**fourth** `SceSysconSync` waiter that a healthy boot doesn't have (there are three, one each for
sceSYSCON_Driver, sceRTC_Service and SceWlanMac - that's their normal idle state).
`hle.backtrace thread=<SCE_VSH_GRAPHICS>` named the whole chain: vsh_module -> vshbridge -> rtc ->
syscon -> wait. The hprm one is only a performance bug, but a loud one: the VSH reads the latch
once a frame, so an older firmware's 12-second boot logged ~20000 lines of the same import.

Two more of the same shape, for 1.50-2.xx: `sceImpose_driver` exports `sceImposeGetParam` as
`0x531C9778` and `sceImposeChanges` as `0xB415FC59` there, with no user-mode alias. Changes runs
once a frame too. And `ModuleMgrForKernel` numbers `sceKernelLoadModuleVSH` `0xA4370E7C` on 1.x -
that's how the VSH loads its own plugins, so unresolved it got module id 0 back and the
`sceKernelStartModule` after it failed, exactly the way `0xD5DDAB1F` did on 6.61 before it was
implemented.

**How to map a NID between versions.** Disassemble the same module from both firmwares with
`--re-module` and match by address. Don't compare raw addresses - the builds move code - anchor on
the plain user-mode export whose NID never changed (`sceRtc/0x7D1FBED3` for SetAlarmTick,
`sceHprm/0x40D2F9F0` for ReadLatch) and read off the `*_driver` NID at the same address. Below
3.95 `sceRtcSetAlarmTick` isn't in the user-mode library at all, so anchor on a neighbour instead:
it is the driver export immediately below `sceRtcIsAlarmed`. Where even that fails, match the
function body - that is how the two impose calls and the 1.x LoadModuleVSH were identified.

`sceRtcIsAlarmed` also had to be implemented (it returns 0, as in JPCSP). Left as a null entry it
returned `SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED`, and the 3.0x-3.5x VSH read that as "ask the
hardware instead" and went back to blocking on syscon.

### The scePaf heap pool, and the order modules start in

scePaf's allocator wants a pool base already written into one of its BSS slots. The module that
owns the allocator fills that slot in itself, from its own module_start. The kernel's bootstrap
starts modules one at a time and waits for each module_start to return, so on hardware that has
happened long before vshmain runs. We used to start all the modules as threads side by side, and
vshmain's start thread ran first: the pointer was still null and the shell wrote through it. For a
while the slot was pre-filled with an 8.3MB block (the compiled-in default size) to get past that.

The pre-fill made the boot work but not the game icons. vshmain's start asks paf for one block
for game_plugin's surface pools, 12MB when `sceKernelTotalMemSize` says more than 36MB (8MB
otherwise). paf's own start would have set up a 0xC50000 long-life heap that holds it, but hadn't
run yet, so the request went to the 8.3MB pre-filled heap and failed. game_plugin then made
surfaces with no memory behind them, decoded every ICON0 and PIC1 correctly, and threw them away
(white tiles on 3.80-5.05, grey placeholders from 5.50).

`LoadVshKernelModules()` now starts the shell's own modules (heaparea1 where it exists, vshbridge,
paf, common_gui, common_util) one at a time, each when the previous module_start returns, and
vshmain waits for the last. The kd/ drivers still start side by side: syscon's module_start blocks
on a semaphore its hardware interrupt would signal. With the order right the pre-fill went away,
and every version from 1.50 to 6.61 shows game icons.

Which module owns the allocator moved. **Up to 4.05 it is a separate `flash0:/vsh/module/heaparea1.prx`**,
which paf imports as `scePafHeaparea` and cannot allocate a byte without; from 5.01 it is compiled
into paf.prx and heaparea1 is gone. Loading heaparea1 when it's present was the missing piece for
every 3.x and 4.x version - without it `scePafHeaparea_ACCE25B2` was an unresolved import, paf
built a heap out of an uninitialized stack pair, and vshmain died storing the null the allocator
handed back.

In both modules the slot is filled by the one function that calls `sceKernelTotalMemSize`: a
`> 0x2400000` test picks 0xA00000/0xC50000 pool sizes over the compiled-in defaults, then two
`sceKernelCreateFpl` + `sceKernelTryAllocateFpl` pairs write to adjacent slots.

### 1.50 declares no module attributes

`g_runningVSH` was set from `PSP_MODULE_VSH_MODE` in the module's attribute word. 1.50's
vshmain.prx has attribute `0000` - Sony only started setting the flag in 1.52 - so the entire VSH
bootstrap was skipped and the shell ran with none of its support modules loaded. The check also
accepts the module *name* `vsh_module` now.

### The vsh_module alarm-category patch only 6.6x needed

6.60 and 6.61 used to crash in vsh_module's alarm-task scan on a "count" in rodata that nothing
initialized, and a patch zeroed it (only when the word was the `0x3F666666` it was derived from).
It was another effect of the start order above: with the shell's modules started in order, 6.60
and 6.61 reach the XMB without it, and it's gone.

### Kernel modules with per-model builds

`LoadVshKernelModules()` asked for `memlmd_01g.prx`, `loadexec_01g.prx` and
`wlanfirm_01g.prx` by name. A firmware unpacked for one model ships only that model's build, and
PPSSPP's own updater unpack defaults to 02g, so all three failed to load. `ResolveVshModelModule()`
now substitutes the emulated model's suffix when that file exists, falling back to `_01g` for a
dump unpacked with model `any` (which has every model's). Firmwares older than about 3.60 predate
the PSP-2000 and have no per-model split at all, so they are unaffected.

### 5.55 needed two PRX keys

Its `flash0:/kd` modules are tagged `0x4C941AF0`/`0x4C941BF0`, and those two were the only entries
of JPCSP's PRX tag table PPSSPP was missing. The shell came up anyway - vshmain and paf are user
modules - but with not a single driver behind it.

## The red error screen

What is known, all measured from a 40-emulated-second `--vsh` boot:

- **Nothing fails to load.** Every `sceIoOpen` in the whole boot succeeds: `flash0:/font/*.pgf`,
  `opening_plugin.rco`, `system_plugin.rco` / `_bg` / `_fg`, `topmenu_plugin.rco`,
  `topmenu_icon.rco`, `impose_plugin.rco`, and `flash0:/vsh/etc/index_02g.dat`. The only I/O
  failure in the entire trace is `flash1:/registry/init.dat`, which is correct (see above). So VSH
  genuinely gets as far as building the top menu before giving up.
- **Where it freezes.** Grouping `sceGeListUpdateStallAddr` by display list id gives a per-frame
  signature of what is being drawn. It grows 6 -> 10 -> 11 -> 15 -> 19 -> 33 -> 34 -> 38 stall
  points (the dialog and its text being composed) and then repeats **byte-identically for the
  remaining ~2016 frames**. That is the error screen locking in; everything before it is animation.
- **Nothing logs an error in the run-up.** The window before the freeze is just the startup jingle
  decoding (`sceAtracDecodeData` + `sceAudioOutputPannedBlocking`), `vshCtrlReadBufferPositive`,
  `scePowerIsSuspendRequired`, `sceKernelGetUserLevel` and `sceImposeChanges`. So the trigger is a
  *wrong return value* we hand back, not a missing call - which makes it much harder to spot than
  the `sceKernelLoadModuleVSH` bug was.
- **`sceImposeChanges` is not a symptom of the error.** It runs once a frame regardless; the
  ~10000 calls per boot are just the indicator refresh, and they start *after* the frame loop is
  already steady. Don't read into it.

### What actually triggers it: the XMB index can't be decrypted

Found by raising log channels for a narrow window around the transition (5.8s to 6.7s emulated,
~12K lines - `log.channel.set`, not a full-boot log). The frame loop's calls before and after the
transition differ sharply: before it there is resource loading, module starting and registry
access, after it only the clock and battery indicators. Inside that window:

```
sceIoOpen(flash0:/vsh/etc/index_02g.dat)   -> fd 8
sceIoRead(8, 092a2d40, 496)                -> the whole file
sceIoClose(8)
unresolved import sceResmgr/9dc14891, called from 'vsh_module'    <- traps here
sceKernelExitDeleteThread(1)               <- the ScePafJob building the menu gives up
```

`index_02g.dat` is the index of what the XMB shows, and it is encrypted (it starts `PSPsysGP`, not
the `release:` of an already-plaintext one). `sceResmgr_9DC14891` is what decrypts it. With that
import unresolved the call trapped, the index stayed encrypted, and the job thread quit - hence a
shell that has loaded every resource successfully and still has nothing to show.

**Fixed.** `Core/HLE/sceResmgr.cpp` implements the call, the three tags it needs
(`0x0B2B90F0/91F0/92F0`, keys and code `0x5C` from JPCSP) are in `Core/ELF/PrxDecrypter.cpp`, and
`pspDecryptType9()` there decrypts it - 496 bytes in, 159 out, starting `release:`, which is the
check `sceResmgr` logs on every call.

Type 9 turned out to be type 6 with three differences, all because a type 9 file carries a real
ECDSA signature at `0x104..0x12C` where type 6 has nothing:

- The "this region must be empty" check has to stop at `0x104` rather than `0x10C`, or the first
  8 bytes of the signature fail it (this was the original `-2`).
- The signature is excluded from the hashed header rather than fed into it - JPCSP zeroes
  `buf2[0x34..0x5C)`, which is the same range after its header rearrangement, so `PRXType9` simply
  leaves that field zero.
- `ecdsa_hash` in the KIRK CMD1 header stays 0. Type 6/7 set it; the branch type 9 takes writes
  only the mode word. Setting it was the final `-4`.

The SHA1 check inside the decrypt is a good progress signal while porting: reaching `-4` means the
header reconstruction and hash inputs were already right and only the KIRK call was wrong.

## The root cause of the long-standing blank screen

**`ModuleMgrForKernel/0xD5DDAB1F` (`sceKernelLoadModuleVSH`) was unimplemented.** That is how VSH
loads its own plugins: the XMB's UI lives in `flash0:/vsh/module/*_plugin.prx` and `vshmain`
pulls those in through this kernel call, not the user-mode `sceKernelLoadModule`. The import went
unresolved, vshmain got no module id back, and the `sceKernelStartModule` that followed was called
with **id 0** and failed with `0x8002012E UNKNOWN_MODULE`. No plugin ever ran, so the scene had
its containers but nothing drawable in them - which is exactly the "render state set up 6 times
per frame, zero draws" symptom this investigation chased for a long time.

Implemented along with `0xD86DD11B` `sceKernelSearchModuleByName`, the other unresolved
`ModuleMgrForKernel` import.

Found by scanning a whole boot for *any* HLE call returning an error. Exactly one mattered, and it
had been missed on two earlier passes because it logs as `: error 8002012e` rather than as a
`8xxxxxxx=sceSomething(` return value. **Grep both shapes.**

## Solved along the way

- **Real kernel driver modules running as genuine MIPS code** needed scratchpad RAM kernel-mode
  address mirrors (`Core/MemMap.cpp`), dummy COP0 instructions (`Core/MIPS/Interpreter.cpp`), and
  a GPIO + Syscon serial MMIO model (`Core/HW/GpioMMIO.{h,cpp}`) so `kd/syscon.prx`'s real
  handshake completes instead of spinning.
- **`vsh_module` alarm-task SIGSEGV**: was a targeted data patch zeroing an "alarm category count"
  field nothing else initialized. Removed once the shell's modules started in order, which made
  it unnecessary.
- **`SCE_KERNEL_ERROR_NO_MEMORY` in the app build**: the 4MB kernel pool was exhausted by PPGe's
  ~2MB overlay texture (now skipped when booting VSH) plus 11 driver modules each asking for a
  256KB thread stack (now given 32KB).
- **`sceHprm_driver/sceHprmReadLatch` (0xE9B776BE)** implemented and its output struct actually
  zeroed, matching JPCSP. Correct, but did not affect the blank screen.
- **flash1 is now mounted** when a dump sits next to the flash0 one. `sceReg` serves a compiled-in
  snapshot of some real PSP's registry (`Core/HLE/sceReg.cpp`) rather than reading the volume, and
  a genuine dump has only `registry/system.dreg` and `.ireg`. **`flash1:/registry/init.dat` must
  stay absent** - an earlier note here said creating one "changes nothing", which is wrong. VSH
  stats it twice during startup, and *finding* it means "reinitialize the registry": it then calls
  `sceRegRemoveRegistry` and `sceIoDevctl("flashfat1:", 0x5802)`, both `UNIMPL`, and the boot dies
  without starting a single plugin module. Its absence is the healthy path, and the resulting
  `FILE NOT FOUND` in the log is expected, not a lead.
- **A diagnostic-tooling bug that produced a phantom lead.** The "unresolved import" reporter
  encoded identity in a 55-slot ring buffer, but a VSH boot registers ~1000 unresolved imports, so
  the ring wrapped ~18 times and every report named whatever most recently reused that slot. It
  made `sceMgVideo_driver` look like one massively hot call when it was the misattributed sum of
  many. Fixed by recovering `stubAddr` via `g_lastSyscallPC` and looking identity up in
  `module->importedFuncs`, which already tracked exactly this (interpreter-only, matching the
  original limitation). **Lesson: when identity has to be recovered later by address, check
  whether something already tracks it before adding a cache.** Breakpoints were never at fault -
  if one doesn't fire, suspect the address first.

## Imports that must stay unresolved

Counterintuitive but repeatedly measured: implementing more of what the VSH imports can make the
boot *worse*, because a correct implementation lets a real flash0 driver finish its init and walk
on into hardware PPSSPP does not emulate. Leaving the import unresolved makes the driver fail early
and the boot continues. All three are commented in the source at the point where they'd be added:

- **`ThreadManForKernel` mutex NIDs** (`0xB7D098C6` `sceKernelCreateMutex`, `0xB011B11F`
  `sceKernelLockMutex`, `0x6B30100F` `sceKernelUnlockMutex`) - we implement all three for user
  mode already. Resolve them and the NAND and ID storage drivers init successfully, then
  `sceIdStorage_Service` polls the NAND controller at `0xbd101300` forever (470k reads in 25
  emulated seconds), and only 4 modules start. `sceKernelAllocateFpl` (`0xD979E9BF`) used to be on
  this list, but it's the mutexes that do it: with only the FPL resolved, `sceIdStorage_Service`
  allocates from its pool and the boot still reaches the XMB.
- **`InterruptManagerForKernel` interrupt registration** (`0x58DD8978`, `0xF987B1F0`, `0x4D6E7305`,
  `0xD774BA45`) - JPCSP can honour these because it emulates the interrupt controller as MMIO. We
  dispatch the few interrupts we emulate ourselves (`__RegisterIntrHandler`), so success is a lie
  the drivers act on. Measured: 31 such calls (interrupts 4, 12, 15-18, 20-24, 31), then a stall in
  GE list execution with no plugin module started.

73 unresolved import hits over 37 distinct module/NID pairs remain in a boot. Most of the rest are
`sceSysEventForKernel`, `sceSuspendForKernel` and `*_driver` modules that would need real hardware
behind them. Note the *runtime* ones log as `Unknown syscall (run) at ...: unresolved import ...`
(`HLE.cpp:905`) - a different string from the load-time `Unknown syscall from known HLE module`,
so grep for both or you will undercount badly.

## How this boot path works

Real hardware boots VSH through the kernel's own module bootstrap, launched from
`flash0:/reboot.bin`. PPSSPP doesn't emulate that chain. `--vsh` loads
`flash0:/vsh/module/vshmain.prx` through the normal PRX loader, and
`LoadVshKernelModules()` (`Core/HLE/sceKernelModule.cpp`) approximates the missing
bootstrap by loading and starting `vshbridge.prx`, `paf.prx`, `common_gui.prx`, `common_util.prx`
and 11 real `kd/` drivers first, in the order JPCSP's own `--vsh` shortcut uses.

JPCSP's `--vsh` turns out to use the same shortcut rather than a real reboot.bin LLE path, so this
is a legitimate if approximate route to a working VSH.

Known differences from JPCSP's `--vsh` that have **not** been tried: it preloads
`PSP_MODULE_AV_VAUDIO`, `AV_ATRAC3PLUS` and `AV_AVCODEC`, and sets the io filepath to
`ms0:/PSP/GAME`. Its trick of forcing the root thread to kernel mode at priority 0x7E **was**
tried and reverted - it reorders scheduling enough that `sceVshBridge_Driver` runs before a
precondition is ready. Firmware version already matches (660 both sides).

## Thread structure

There is no `vsh_main` thread and there shouldn't be. `vshmain.prx`'s `module_start` runs on the
`root` thread, creates and starts `SCE_VSH_GRAPHICS` (entry `08818d14`) and returns from module -
visible in the log as `Context switch: root -> ... (returned from module)` right after the
`sceKernelStartThread`. **`SCE_VSH_GRAPHICS` is VSH's main thread**, and its entry is the
outermost frame of every render backtrace.

Steady state is 8 threads: `SCE_VSH_GRAPHICS`, `ScePafThread`, `SceWaveMain`, `SceWlanMac`,
`sceRTC_Service`, `sceSYSCON_Driver`, `idle0`, `idle1`, plus transient `ScePafJob` threads that
load the `.rco` resources and exit. The frame loop is healthy: poll `ScePafSyncCall`,
`vshCtrlReadBufferPositive`, render, `sceDisplaySetFrameBuf`, wait `SceVblankSync`.

## The render call chain

Still worth having if individual widgets misbehave. Traced with a **write memcheck on a display
list word** (`memory.breakpoint.add address=... write=true`), which stops with a structured hit
and lets `hle.backtrace` name the whole chain - minutes of work, and the technique to reach for
first next time:

```
08818d14   SCE_VSH_GRAPHICS thread entry
  08895544   6-iteration loop
    08963cfc   container: sets base render state, walks children at [obj+0x550], count [obj+0x554]
      08950754   per-child render
        089a1cdc / 089a36e4 / 089a38c0 ...   GE state helpers
```

`08950754` gates on `z_un_089501cc(child+0x450) == 1` and on `[child+0x438] != 0`, then computes
the scissor by intersecting `[child+0x43c..+0x448]` with a parent clip rect.

## Tools and methodology

- Everything useful works on a live session: `hle.thread.list`, `hle.eventflag.list`,
  `hle.backtrace`, `memory.disasm`, `memory.readString`, `gpu.displaylist.disasm`, `cpu.evaluate`
  (expressions like `[s0+0x438]` read guest memory), and memory breakpoints. See
  `docs/WebSocketDebugger.md` for the catalog.
- `cpu.runUntilTime us=N` is what makes a script reproducible - it stops on the requested emulated
  microsecond, so every run reaches the same instruction. Ask for the time you need and no more;
  the red screen locks in around 6 emulated seconds.
- Raise log channels for a *short* window only - `sceGe`/`G3D` at debug produce ~200K lines in
  three seconds of boot. `log.channel.set` is session-only, never persisted. `--loglevel=3` is a
  good default for the whole run: bare `--log` on a VSH boot writes ~2.3GB, level 3 writes ~66KB.
- Diffing against JPCSP (`../JPCSP`, Java source, NIDs searchable by hex) repeatedly beat guessing
  at missing HLE functions. `assets/flash0` has the decrypted PRXs if a NID needs disassembling.
- Headless is preferred over the app build - the app's Debug config blocks on a Windows crash
  dialog with no console output.

**Emulation is not the slow part.** A VSH boot runs at roughly real time even in a Debug build
(1 emulated second in a 2.5s run that includes process startup). If a scripted run takes minutes,
it is waiting, not emulating - and the usual cause is an explicit `:wait cpu.stepping` after a
`cpu.runUntil*`/`cpu.resume`/`cpu.step*`, which `--sync` already waits for. The redundant one waits
for a second `cpu.stepping` that never comes and burns the whole `--sync-timeout`. See
`Tools/wsdbg/README.md`.

## Not included on this branch

The separate `--vsh-reboot` experiment (loading `flash0:/reboot.bin` for a real LLE kernel boot,
with NAND/KIRK/system-control MMIO emulation) lives on `vsh-experiments`. The direct HLE path here
reaches much further without any of it.

---

### WebSocketDebugger (`docs/WebSocketDebugger.md`)

# PPSSPP WebSocket Debugger

PPSSPP has a JSON/WebSocket-based debugger and automation API, served from the
same HTTP server used for "Remote ISO" disc sharing and file upload. It lets
an external tool (a script, a web page, another editor/IDE) inspect and
control a running emulation session: read/write memory, set breakpoints,
step the CPU, read GE/GPU state, send fake input, tail the log, etc.

This doc is a local reference, the user-facing documentation is on the website.

## Where the code lives

- `Core/WebServer.cpp` / `Core/WebServer.h` - the shared HTTP server (also
  used for Remote ISO and file upload). It owns the listening socket and
  dispatches `/debugger` requests.
- `Core/Debugger/WebSocket.cpp` - upgrades the HTTP request to a WebSocket and
  runs the per-connection event loop (`HandleDebuggerRequest`).
- `Core/Debugger/WebSocket/*.cpp/.h` - one "subscriber" or "broadcaster" per
  feature area (CPU, memory, GPU, HLE, input, breakpoints, ...). Each file's
  top comment documents its events in detail - this doc gives the overview
  and an index into those files.
- `Core/Debugger/WebSocket/WebSocketUtils.h` - shared `DebuggerRequest`
  helper (parameter parsing, response/error helpers) and `DebuggerSubscriber`
  base class.
- `Common/Net/WebsocketServer.h/.cpp` - the low-level WebSocket framing.

## Transport

- Runs on the same port as Remote ISO sharing (`g_Config.iRemoteISOPort`; `0`
  means "pick a free port automatically" - the actual bound port is written
  back to that config value and logged: `Listening on port N`).
- URL path: `/debugger`.
- WebSocket subprotocol: `debugger.ppsspp.org` (required - a plain HTTP GET
  to `/debugger` without a websocket Upgrade just redirects to the bundled
  web UI at `/debugger/index.html`).
- Messages are JSON, both directions, always shaped as `{"event": "NAME", ...}`.
- One WebSocket connection = one client; PPSSPP does not limit the number of
  simultaneous debugger connections.
- The debugger only actually does anything while `WebServerFlags::DEBUGGER`
  is enabled (see "Enabling it" below) - the HTTP server itself may also be
  running for other reasons (Remote ISO, upload).

## Message protocol

Requests you send:
```json
{ "event": "cpu.status" }
```
Optionally include a `"ticket"` field (any JSON value) - PPSSPP echoes it
back verbatim in the response/error, so you can correlate requests and
responses when firing several at once. `Tools/wsdbg` (see below) assigns an
incrementing integer ticket automatically.

Responses use the *same* event name as the request:
```json
{ "event": "cpu.status", "ticket": 1, ... }
```

**The ticket convention**: send one whenever you care about the answer. Since
a response reuses the request's event name, a ticket is the only thing that
distinguishes *your* answer from an unsolicited broadcast of the same name, or
from the answer to an identical request you sent a moment earlier. Conversely,
for a request that doesn't answer immediately (below), leaving the ticket off
says you aren't waiting for anything.

Responses are not always immediate. `cpu.resume`, `cpu.stepInto`,
`cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime`,
`cpu.nextHLE`, `cpu.stepping` and `gpu.stats.feed` send nothing back at the
time of the request; what follows later is the broadcast that reports the
actual outcome (`cpu.stepping` / `cpu.resume`), which carries no ticket.
`input.buttons.press` is the odd one out - it answers with the request's own
event name *and* ticket, but only once the button has been held for the
requested number of frames.

If you would rather not track which those are, ask to be told explicitly:

```json
-> { "event": "client.config.set", "acknowledgeDeferred": true }
-> { "event": "cpu.resume", "ticket": 7 }
<- { "event": "deferred", "for": "cpu.resume", "ticket": 7 }
<- { "event": "cpu.resume" }
```

With that on, every request draws exactly one immediate reply - a response, an
`error`, or a `deferred` - so a client can correlate without a hardcoded list,
including for events added in future versions. It is off by default and must
stay that way: an extra message would break a client that correlates purely by
ticket, and it can't reuse the request's event name because for
`input.buttons.press` that is exactly what the real, later answer looks like.

Errors look like this:
```json
{ "event": "error", "message": "...", "level": 2, "ticket": 1 }
```
`level` is a `LogLevel` (1=NOTICE, 2=ERROR, 3=WARN, 4=INFO, 5=DEBUG, 6=VERBOSE).

PPSSPP also sends unsolicited ("broadcast") events with no request - see
below.

By convention, send a `version` event right after connecting (see
`WebSocket/GameSubscriber.cpp`):
```json
{ "event": "version", "name": "my-tool", "version": "1.0" }
```
PPSSPP responds with its own name/version, and remembers yours (currently
just for internal bookkeeping/future logging). The response also carries
`pid` (OS process id) and `path` (the executable/disc currently loaded, or
`null`), so an automation client can confirm it attached to the instance it
meant to - a port alone doesn't prove that, since a leftover process may still
be holding the port you asked for.

## Broadcast (unsolicited) events

Sent without you asking, whenever the underlying state changes:

| Event | Sent when | Source |
|---|---|---|
| `log` | A new log line is emitted | `LogBroadcaster.cpp` |
| `game.start` | A game finishes booting | `GameBroadcaster.cpp` |
| `game.quit` | The game is closed/reset | `GameBroadcaster.cpp` |
| `game.pause` / `game.resume` | User opens/leaves the pause menu | `GameBroadcaster.cpp` |
| `cpu.stepping` | CPU enters a stepping/break state | `SteppingBroadcaster.cpp` |
| `cpu.resume` | CPU resumes from stepping | `SteppingBroadcaster.cpp` |
| `cpu.breakpoint.hit` | Any breakpoint trips, whether or not it stops the CPU | `WebSocket.cpp` |
| `input.buttons` | Any emulated button changes state | `InputBroadcaster.cpp` |
| `input.analog` | An analog stick position changes | `InputBroadcaster.cpp` |

A client can opt out of specific broadcast categories with
`broadcast.config.set` (`{"disallowed": {"logger": true, "game": true, "stepping": true, "input": true, "breakpoint": true}}`),
see `ClientConfigSubscriber.cpp`. `gpu.stats.feed` (see below) works the same
way for periodic GPU stats. `client.config.set` in the same file carries
per-connection settings that aren't about broadcasts - currently just
`acknowledgeDeferred`, described under "Message protocol" above.

### Source line info

Where a game shipped an unstripped ELF, PPSSPP decodes its DWARF `.debug_line`
and can map an address to a source file and line. `cpu.breakpoint.hit` and
`cpu.stepping` carry `file`/`line` in the `hit` object, and `hle.backtrace`
carries them per frame - which is where it pays off most:

```
08841f98  move sp,fp          mesh.zig:163
0883afa4  li v0,0x0           MenuState.zig:821
088260d8  andi at,v0,0xFFFF   State.zig:40
0882a27c  andi at,v0,0xFFFF   engine.zig:468
```

Both fields are `null` when there's no line info for that address, which is the
common case: **PRX conversion strips every `.debug` section**, so this never
applies to a commercial game. In practice it means homebrew that ships its
`app.elf` next to the EBOOT - the same file the companion symbol loader uses -
or a plain `.elf` you built yourself. It follows `bAutoSaveLoadSymbols` along
with the symbols.

DWARF 2, 3 and 4 are decoded; version 5 units are skipped with a log line rather
than mis-parsed, since it re-encoded the file table. Nothing targeting the PSP
emits it today (psp-gcc produces 2, Zig 4).

### Emulation speed

`game.speed.set` drives two independent things:

```json
-> { "event": "game.speed.set", "fastForward": true }        // unlimited
-> { "event": "game.speed.set", "percent": 200 }             // double speed
-> { "event": "game.speed.set", "percent": 25 }              // quarter speed
-> { "event": "game.speed.set", "percent": null }            // drop the override
<- { "event": "game.speed.set", "fastForward": false, "percent": 200, "limitFps": 120 }
```

Percentages are relative to 60 FPS, matching how the in-app settings present the
alternative speeds - so `200` means the same thing in both places. `percent`
must be at least 1; use `fastForward` for unlimited rather than `0`, so there's
only one way to say it. `fastForward` wins while it is on, and `percent` is
remembered underneath it.

`limitFps` in the response is the frame rate throttling is actually aiming for
once everything - fast-forward, this override, the user's own alternative-speed
hotkeys - has been taken into account, with `0` meaning unlimited. It comes
straight from the function the frame timing itself consumes, so prefer reading
it over inferring the result from the other two fields.

This is a **separate channel** from the user's own alternative speeds. It
deliberately never touches `g_Config`, whose speed settings are persisted per
game, so a debugger session can't permanently change what the user configured.
Clearing the override also only stands down from a limit set through this event,
never from one the user set themselves. It resets on every game boot.

The request **fails** rather than being silently ignored when something else
owns the speed: RetroAchievements hardcore mode, or being connected to a network
game without "allow speed control while connected".

Headless sets fast-forward at startup and never turns it off on its own, so
`game.speed.set` works there too - turning fast-forward off makes headless
throttle to real time, which is occasionally useful for a wall-clock repro.

### Breakpoint hits

`cpu.breakpoint.hit` fires every time a breakpoint's condition passes and it has
some action set - including **log-only breakpoints, which never stop the CPU**.
That's what makes them usable for automation: before this event existed, a
log-only breakpoint's only trace was a line in the log stream.

```json
{
  "event": "cpu.breakpoint.hit",
  "sequence": 1,
  "hit": {
    "kind": "exec",
    "pc": 142876568,
    "address": 142876568,
    "hits": 1,
    "logged": true,
    "paused": false,
    "condition": null,
    "symbol": "rendering.mesh.Mesh(rendering.Vertex.PspVertex).draw",
    "breakpoint": { "start": 142876568, "end": 142876568 }
  }
}
```

The same `hit` object is attached to `cpu.stepping` when a breakpoint is what
stopped the CPU, so both can be parsed the same way. It is **absent** when the
break came from something else (the user pausing, a savestate load, an
exception), so test for its presence rather than for a `kind`.

Fields common to every kind:

| Field | Meaning |
|---|---|
| `kind` | `"exec"`, `"memory"` or `"register"` |
| `pc` | The instruction responsible |
| `address` | Exec: the instruction. Memory: the address **actually accessed** |
| `hits` | Total times this breakpoint has tripped, matching `*.list` |
| `logged` / `paused` | Which actions it had - `paused` false means the CPU kept running |
| `condition` | The condition expression, or `null` |
| `symbol` | Symbol at `address`, or `null` - resolved here to save a round trip |
| `file` / `line` | Source location of `pc`, or `null`. See "Source line info" below |
| `breakpoint` | `{start, end}` identifying which breakpoint fired. Absent for `"register"`, whose identity is the register, not an address |

Extra fields for `"memory"`:

| Field | Meaning |
|---|---|
| `size` | Bytes accessed |
| `access` | `"read"` or `"write"` |
| `source` | Who performed it - `"interpret"`, `"CPU"`, `"HLE"`, or an allocation tag such as `"ThreadFillStack"` |

Extra fields for `"register"`: `register` (GPR index) and `registerName`
(e.g. `"a0"`).

Note `address` and `breakpoint.start` are **not** the same thing for a memory
breakpoint watching a range - the first is the byte touched, the second is the
range being watched. On `cpu.stepping` the legacy `relatedAddress` field keeps
reporting the range start; `hit.address` is the accurate one.

`sequence` counts hits *produced*, not delivered. A connection whose queue backs
up (easy to do with a log-only breakpoint in a hot loop - one can produce tens
of thousands of hits per second) drops hits rather than growing without bound,
so a gap in `sequence` tells a client exactly how many it missed. Game and
stepping events are never dropped. Turning the
`breakpoint` category off via `broadcast.config.set` avoids the traffic
entirely.

## Request/response event catalog

Full details (parameters, response shape) are documented as comments above
each handler in the corresponding `Core/Debugger/WebSocket/*Subscriber.cpp`
file - this is just an index.

| Category | Events | File |
|---|---|---|
| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game, applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree; all but this one are refused in a session booted into the XMB, whose discs come and go), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `version` | `GameSubscriber.cpp` |
| CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` |
| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` |
| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |
| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write`. The numeric ones report the result as both `value` and `uintValue` - the latter is what `cpu.getReg`/`cpu.getAllRegs` call it, so a client can read either without caring which event answered | `MemorySubscriber.cpp` |
| Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` |
| Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` |
| Disassembly | `memory.base`, `memory.disasm` (add `compact=true` for plain-text lines instead of full per-field objects), `memory.searchDisasm` (add `findAll=true` for every match instead of just the first - e.g. "every caller of this address"), `memory.assemble` | `DisasmSubscriber.cpp` |
| GE display list disassembly | `gpu.displaylist.disasm` - like `memory.disasm` but for GE command words (`CLEARMODE`, `PRIM`, etc.) instead of CPU instructions; also supports `compact=true` | `GPUDisasmSubscriber.cpp` |
| HLE | `hle.thread.list/wake/stop`, `hle.func.list/add/remove/removeRange/rename/scan`, `hle.func.hash` (the code hash and size of the analyzed function at an address, as `hardcodedHashes` in `MIPSAnalyst.cpp` needs them, and the name of the entry already matching it if any), `hle.module.list`, `hle.module.saveSymbols/loadSymbols` (save/load one module's symbols to/from its standard `PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym` file, shared across any game that loads the same module - see `SymbolMap::GetModuleSymbolsPath`), `hle.game.saveSymbols/loadSymbols` (the same for symbols that aren't inside any module - heap, stack, scratchpad, hardware registers - which describe one game's memory layout and so go to a per-game `PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym` instead; see `SymbolMap::GetGameSymbolsPath`), `hle.backtrace` | `HLESubscriber.cpp` |
| Data symbols | `hle.data.list/add/remove/rename` - label discovered data (structs, tables, buffers) with a name/type, same idea as `hle.func.*` but for `ST_DATA` symbols | `HLESubscriber.cpp` |
| Kernel objects | `hle.object.list` (every live kernel object of every type at once, with an optional `type` filter - uid/type/name/one-line summary only); `hle.eventflag.list/info`, `hle.mutex.list/info`, `hle.semaphore.list/info`, `hle.msgpipe.list/info`, `hle.callback.list/info` (per-type full detail, including waiting-thread lists) - all read-only, never mutate kernel state | `HLEKernelObjectSubscriber.cpp` |
| GPU stats | `gpu.stats.get`, `gpu.stats.feed` | `GPUStatsSubscriber.cpp` |
| GPU recording | `gpu.record.dump` | `GPURecordSubscriber.cpp` |
| GPU buffers | `gpu.buffer.screenshot`, `gpu.buffer.renderColor/renderDepth/renderStencil`, `gpu.buffer.texture`, `gpu.buffer.clut` | `GPUBufferSubscriber.cpp` |
| Input injection | `input.buttons.send`, `input.buttons.press`, `input.analog.send` | `InputSubscriber.cpp` |
| Replay | `replay.begin/abort/flush/execute/status`, `replay.time.get/set` | `ReplaySubscriber.cpp` |
| Client config | `broadcast.config.get/set`, `client.config.get/set` | `ClientConfigSubscriber.cpp` |
| Log channels | `log.channels.list`, `log.channel.set` - query/change a log channel's level (string: `notice`/`error`/`warning`/`info`/`debug`/`verbose`) and/or enabled state; the `log` event itself (the passive message stream, unaffected by this) keeps its existing numeric `level`, see `LogBroadcaster.cpp` | `LogConfigSubscriber.cpp` |

## Enabling it

- **UI**: Settings > Tools > Developer Tools > "Allow remote debugger"
  checkbox (`UI/DeveloperToolsScreen.cpp`). The "Local Server Port" slider on
  the Networking screen sets the port (shared with Remote ISO sharing; `0` =
  auto-pick).
- **Config**: `RemoteDebuggerOnStartup=true` in `ppsspp.ini`
  (`g_Config.bRemoteDebuggerOnStartup`) starts it automatically on launch
  (`UI/NativeApp.cpp`).
- **Command line** (both application and headless builds): `--debugger=PORT`
  (`0` = pick a port automatically) - a shared auto-param in
  `Core/CmdLine.cpp`/`.h` (`CmdLineMode::Both`). `ApplyToConfig()` sets
  `iRemoteISOPort`/`bRemoteDebuggerOnStartup` for that run without persisting
  them to the config file.
  - A **non-zero** `PORT` is treated as mandatory (`WebServerSetRequireExactPort()`):
    if it can't be bound, the server does *not* silently fall back to some other
    free port the way the "Local Server Port" preference does, because a client
    was told to connect there. Headless exits non-zero; the application build
    logs an error and shows an OSD message but keeps running. Use `--debugger=0`
    and read the actual port from the `Listening on port N` log line if you'd
    rather not care which port you get.
  - On the **application** build this is exactly like ticking "Allow remote
    debugger" - the game boots and runs normally, debugger listening
    alongside it.
  - On the **headless** build (`headless/Headless.cpp`) it additionally
    forces `coreParameter.startBreak = true`, so the CPU halts before
    running anything - useful for setting breakpoints before launch.

## Discovery

For LAN auto-discovery (mainly useful for mobile), the server periodically
reports its `(local ip, port)` to `report.ppsspp.org/match/update` (see `RegisterServer()` in
`Core/WebServer.cpp`). Clients can query `report.ppsspp.org/match/list` to
get a list of candidate endpoints on the same network and try connecting to
each in turn.

## The bundled web-based JS debugger

`assets/debugger/` is a git submodule
(`https://github.com/unknownbrackets/ppsspp-debugger.git`, `bundled` branch -
see `.gitmodules`) containing a prebuilt React app. PPSSPP serves it directly
at `/debugger/` (`Core/WebServer.cpp`'s `HandleFallback`/`ServeAssetFile`),
so opening `http://<ip>:<port>/debugger/` in a browser gets you a full GUI
debugger for free. The actual editable source lives in a different branch of
that same repo (the `bundled` branch only holds the built output that gets
checked in here).

From reading the minified bundle (`assets/debugger/static/js/main.*.js`),
it connects like this:

- Manual connect: `new WebSocket("ws://ip:port/debugger", "debugger.ppsspp.org")`.
- Auto connect: `fetch("//report.ppsspp.org/match/list")` for a list of
  `{ip, port}` candidates (as registered by `RegisterServer()` above), then
  tries each with the same WebSocket call until one succeeds.

## Talking to it yourself

- `scripts/websocket-test.py` - old minimal Python one-shot script (needs the
  `websocket-client` pip package).
- `Tools/wsdbg/` - a small Rust CLI/REPL client for this session's work (see
  `Tools/wsdbg/README.md`): connects, does the `version` handshake, and lets
  you fire off events by hand or from a one-shot command line, printing
  responses and broadcasts as they arrive. Built and smoke-tested against a
  live PPSSPP instance while writing this doc.

---

### Android Device Testing (`docs/android-device-testing.md`)

# Testing an Android build on a device

Notes from driving real devices over adb. Most of what follows is not about Android being hard, but
about ways the tooling quietly answers a question you did not ask - you measure the wrong build, or
measure garbage that has not been collected yet, and get a confident wrong answer.

## Install the build you think you are testing

`versionCode` is derived from the git commit count (see `gitVersionCode` in
`android/build.gradle.kts`), so a feature branch has a *lower* code than a longer branch. Installing
one over the other fails:

    adb: failed to install ...: Failure [INSTALL_FAILED_VERSION_DOWNGRADE]

`-r` does not permit downgrades; `-d` does. Use `adb install -r -d <apk>`.

Do not pipe the install through `tail -1` or `grep Success` and move on. The failure is a single
line and easy to swallow, and the device then keeps running whatever was there before - every
measurement afterwards describes the wrong binary, including A/B comparisons where both arms end up
being the same build and therefore agree with each other.

Verify after every install, not just when something looks odd:

    adb shell dumpsys package org.ppsspp.ppsspp | grep versionName

A second, independent check is worth it when a result is surprising: grep the log for a symbol that
exists only on the branch under test (a renamed class or log tag, say).

The version string itself can also lie. The Android build regenerates `git-version.cpp` but does not
always recompile it, so the APK can carry the previous build's string while containing current code.
When the version matters:

    find android/.cxx -name git-version.cpp.o -delete

## Forcing an activity recreate without touching the device

- `adb shell settings put system font_scale 1.15`, then back. `fontScale` is not in the activity's
  `configChanges`, so each change destroys and recreates the activity. Independent of orientation.
- `adb shell wm density <n>` also recreates - but only while `density` is absent from
  `configChanges`.
- `adb shell settings put system user_rotation 1` does **not** rotate PPSSPP. The activity follows
  the sensor, so this needs a physical rotation (or a cooperative device).

Put back whatever you changed: `font_scale`, `accelerometer_rotation`, `user_rotation`.

`wm density reset` is actively wrong on a device with a resolution override - a Galaxy S8 running
1080x2220 on a 1440x2960 panel resets to the *physical* panel's density and leaves the UI oversized.
Read the original with `wm density` first and set it back explicitly.

## Measuring retained activities

`adb shell dumpsys meminfo <pkg>` prints an Objects block with `Activities` and `AppContexts`.

The counts include objects that are garbage but not yet collected. Read it straight after N
recreates and you will see roughly N whether or not anything leaks, which looks exactly like a leak.
Force collection instead:

    adb shell am send-trim-memory org.ppsspp.ppsspp COMPLETE

then wait around eight seconds and read. Repeat until two readings agree - three or four rounds is
normal, and a single trim is not enough.

The protocol that gives an answer you can trust: cold start and read the baseline, do N recreates,
trim until stable, compare. A sound build returns to the baseline; a leaking one sits at baseline+N
and stays there.

Worked example, Galaxy S8 on Android 9, same device and protocol, both builds confirmed by
`versionName`. Before the activity handover fix: 13 recreates left 13 Activities and 18 AppContexts,
stable across repeated trims. After: the same 13 recreates settled back to 1 and 6.

A JNI global reference is a GC root, so an activity pinned by one shows up here. This is a usable
check for JNI ownership bugs, not only Java ones.

## logcat on a noisy device

Filter to the process, since the tag alone is not enough:

    adb logcat -d --pid=$(adb shell pidof org.ppsspp.ppsspp)

On a device with a chatty system - a Quest especially - the ring buffer can rotate the app's lines
away within seconds, which reads as "logging stopped" rather than "logging was lost". Start a stream
*before* launching the app instead:

    adb logcat -c && adb logcat -s PPSSPP:V PpssppActivity:V   # in the background, then start the app

## VR (Quest)

- The VR flavor is a separate package: `./gradlew :android:assembleVrDebug`, installing as
  `org.ppsspp.ppssppvr`. It can sit alongside the normal build.
- `adb exec-out screencap` returns solid black for an immersive app - it captures the 2D panel
  layer, which such an app does not have. Use the runtime's own line instead: a logcat entry like
  `VrApi: FPS=73/72 ... App=2.67ms` proves frames are being submitted and how much GPU time the app
  spent on them. Zero app time means the render loop is not producing.
- `am start` makes the activity `topResumedActivity`, but the shell keeps focus, so the app never
  gets a surface and the render loop does not restart. Re-entering an immersive app has to be done
  from inside the headset. A fresh `am start -S` after a `force-stop` does work.
- `debuggerd -b <tid>` needs root, so no native stacks from a retail headset.

## Check which paths the device actually exercises

Before concluding that a test covered something:

    adb shell grep -E '^(MemStickDirectory|GraphicsBackend)' /storage/emulated/0/PSP/SYSTEM/ppsspp.ini

A plain filesystem path for the memstick means the SAF/content-URI code
(`Common/File/AndroidStorage.cpp`, `ContentUri.java`) is never entered, so a run on that device says
nothing about it - a recents list full of plain paths says the same. The VR flavor reads
`ppssppvr.ini`, not `ppsspp.ini`.

---

### Async Savestates (`docs/async-savestates.md`)

# Async savestates

How savestate compression and file I/O were taken off the emulation thread, so saving and loading
don't stall the game long enough to glitch the audio. The worker and the operation queue are in
`Core/SaveState.cpp`, rewind in `Core/SaveStateRewind.cpp`, and the file format code in
`Common/Serialize/Serializer.cpp`. Written 2026-10-19; file references will drift.

**Keep memory use low.** A state is about 38 MB uncompressed, and much of the hardware we run on is
short of memory. So don't waste it: no full-size buffers beyond what an operation needs, freed when
it's done. Within reason, though; a brief extra buffer in a rare case is fine. See [Memory](#memory).

## Where the time goes

Measured with Release headless on desktop x64, local disk, three commercial games:

| | Emu thread today | Of which can move to a worker |
|---|---|---|
| Save | 49-75 ms | compress (zstd, default level) + write: 47-70 ms. The snapshot itself is 2-5 ms |
| Load | 28-40 ms | read + decompress: 21-30 ms. Applying the state is 6-10 ms |
| Load from a slot | the save and the load above | `LoadSlot` first saves `load_undo.ppst` in full |

The waits at the start of `SaveStart::DoState` (utility IO, sas mix, async IO, pending GPU drawing)
were under 0.2 ms. Phones are several times slower, and their storage can be much slower again, so
the worker column is the part that hurts. The emu thread keeps only the snapshot and the apply.

## Structure

Each operation splits into the part that touches emulator state (emu thread) and the part that
doesn't (worker).

- **One savestate worker thread**, with a FIFO job queue. Not `g_threadManager`: file work on a slot
  has to happen in order. On Android it holds an `AndroidJNIThreadContext` for its lifetime, since
  storage access can need JNI.
- **Callbacks still run on the emu thread**, from `SaveState::Process()`. The worker posts results to
  a completion queue that `Process()` drains, so callers see the same threading as before, only
  later.
- An operation goes through: queued, emu phase, worker phase, completion, callback. A load does its
  worker phase (reading) before its emu phase (applying).

## Save

1. Emu thread, in `Process()`: `SaveStart::DoState` into a fresh buffer (the snapshot). The waits
   and `FlushPendingDrawing()` stay here, and so does the `saveStateGeneration` bump.
2. Worker: compress (see [Memory](#memory)) and write the file, to `<file>.tmp` for a slot save, then for a slot save the rotation that `SaveSlot`'s
   `renameCallback` does today (old file to `.undo`, `.tmp` to the real name), including the
   `g_Config.Save()` for the undo bookkeeping. Then set `g_filesStale`.
3. Completion: the callback, with "Saved State" or the failure. **The message appears when the file
   is on disk**, not at snapshot time, so a failed write is reported honestly. A failed write leaves
   the previous file in place (it only ever wrote the `.tmp`).

The thumbnail screenshot is already asynchronous and doesn't change.

**A save to a path that already has a save queued, and not yet snapshotted, is a duplicate
keypress.** It isn't queued again. Its callback is attached to the queued save and called with that
save's status and an empty message (so it shows nothing, and the queued save's own callback shows
the result once). Once the snapshot is taken, another save to the path is a new save: the game may
have moved on, or a load been applied, since.

A save to a different path is queued, and takes its snapshot right away like any other. **At most two
save snapshots exist at once**: a third save, while two are still being written, waits for the
older of them before taking its snapshot. Two quick saves to different slots are rare and briefly
cost one extra buffer; more than that isn't worth holding 38 MB each for.

## Load

1. Worker: read the header and check it, then stream the file through the zstd decompressor into
   the state buffer, while the game keeps running.
2. Emu thread, in the first `Process()` after the read completes:
   - For a slot load, snapshot the current state for load undo. This has to happen here, at the
     moment the game actually leaves that state, not when the load was asked for.
   - Apply the loaded state (`LoadPtr`), with everything `Process()` does around it today
     (`hasLoadedState`, `Core_ResetException()`, AV dump restarts, `g_lastSaveTime`).
3. Worker: compress and write the load undo snapshot to `load_undo.ppst`, and update
   `sStateLoadUndoGame`. This happens even when the apply failed: that's when the undo is most
   useful, since a failed load resets the game.
4. Completion: the callback, after the apply.

The game runs a frame or two longer before the loaded state takes over. That isn't observable,
since the load replaces all of it.

## Ordering

Operations still take effect in the order they were asked for:

- **Emu phases run in queue order.** A save queued after a load waits until that load is applied,
  and snapshots the loaded state, as it does today when both run in the same `Process()`. A load
  waiting for its file holds up only the emu phases queued behind it.
- **Worker jobs run in queue order**, so a save to a slot followed by a load of it writes before it
  reads.
- **UI-thread file operations on slots wait for the worker** (`WaitForWorker()`, for all jobs, since
  it's rare and short): `UndoSaveSlot` and `DeleteSlot`. `UndoLoad` is a load, so it's queued behind
  the load undo write like anything else. `LoadSlot`'s existence check counts a slot with a save
  pending as existing. The thumbnail screenshot isn't the worker's, so its rotation doesn't wait.

## What stays synchronous

There's one code path. Waiting for it is `WaitForWorker()`, which simply blocks: worker jobs never
need the emu thread, so it can't deadlock.

- **libretro**: `retro_serialize`/`retro_unserialize` are in memory with no file I/O, and stay as
  they are. So does the freeze-frame feature (`SaveToRam`/`LoadFromRam` every frame).
- **Verify** stays synchronous.
- **Shutdown and game switch** (`SaveState::Shutdown()`, from the kernel's shutdown, which app exit
  goes through too): wait for the worker, so a save whose snapshot was taken is written. Then run the
  completions' bookkeeping (the undo slot in the config) without their callbacks, which can refer to
  an EmuScreen that's going away, and drop whatever is still queued, as before. A save still queued
  at that point (asked for in the pause menu, which doesn't run `Process()`, then quitting) is lost
  like it always was: by then most of the kernel has shut down, too late to take a snapshot.
- Loads at boot (auto-load, `--state`, opening a `.ppst`) and the debugger's `game.state.*` with
  `wait` already work through callbacks, and need no special handling. The game already ran at least
  a frame before a boot-time load applied.

## Rewind

Rewind snapshots used to compress on a `std::thread` per snapshot, which the next snapshot joined
(stalling if it was still running), and the whole restore ran on the emu thread: wait for the
compressor, decode (rebuild the full ~38 MB state from the base and the changed blocks), apply. The
snapshot itself (2-5 ms, mostly copying RAM and VRAM, already a `ParallelMemcpy`) stays on the emu
thread.

Now:

- The emu thread owns the ring itself (which slots hold what); compressing and decoding are jobs on
  the savestate worker, and the emu thread leaves the buffers a job uses alone until it's done.
- **Compression runs on the worker.** If the previous snapshot's job is still running, the snapshot
  is skipped and retried next frame rather than waited for: snapshots are best effort.
- **The deltas get zstd on top** (level 1: higher levels shrink them a little for a lot more time).
  The changed blocks still compress about 3x, so the ring takes about a third of the memory.
- **Restore is an async load from memory**: `StartRestore()` picks the state and has the worker
  decode it (queued behind the newest snapshot's compression, if that's still going), and
  `FinishRestore()` applies it on the emu thread. No snapshots are taken in between. No prefetching
  (see [Memory](#memory)).

## Memory

What each operation holds at its peak, against today:

| | Today | Async |
|---|---|---|
| Save | snapshot (38 MB) + `ZSTD_compressBound` output buffer (about 38 MB) | snapshot (38 MB) + the compressed output as it grows (10-12 MB). The snapshot is freed before writing |
| Load | whole compressed file (about 12 MB) + state (38 MB) | state (38 MB) + a small streaming input buffer |
| Load from a slot | the save, then the load | at apply: the loaded state + the load undo snapshot (76 MB), briefly. The loaded state is freed right after the apply, before the undo is compressed |
| Rewind restore | a `static` 38 MB decode buffer, never freed | the same buffer, freed after the apply |
| Rewind ring | block diffs | the same, zstd-compressed: about a third |

Rules:

- **No pools and no buffers kept between operations.** Allocate a buffer when the operation starts
  and free it when it ends. A 38 MB allocation costs far less than the stall this design removes,
  and an idle emulator then holds nothing for savestates (rewind aside, which is opt-in and labelled
  a memory hog in the settings).
- **At most two save snapshots at a time** (see [Save](#save)), and one load buffer at a time: a
  second load queued behind a first doesn't start reading until the first is applied.
- **Stream compression in both directions.** Saving, `ZSTD_compressStream2` into a buffer that grows
  with its output, instead of allocating the worst case up front. It isn't streamed straight to the
  file, since the header holds the compressed size and seeking back to patch it in isn't safe on
  every kind of storage (Android's content URIs). Loading, `ZSTD_decompressStream` reads the file in
  chunks, so the compressed data never sits in memory in full. The file format doesn't change.
- **Rewind adds nothing.** The worker reuses rewind's existing buffers. There's no prefetching
  (decoding a likely target before the rewind is pressed), which would need a spare full-size buffer.

## Later, if phone measurements call for it

- `MeasureAndSavePtr` traverses the state twice (measure, then write). Writing into a buffer sized
  from the previous measurement plus slack would skip the measure pass, but needs bounds checks in
  the write pass.
- The apply is mostly `DoState` logic rather than copying; leave it unless it shows up.

## Results

Measured the same way as above (Release headless, desktop x64), on the emu thread:

| | Before | After |
|---|---|---|
| Save | 49-75 ms | 2.4-2.5 ms (the snapshot) |
| Load | 28-40 ms | 12-15 ms (the apply, plus the warnings and callback around it) |
| Rewind restore | decode + apply | 12-13 ms (the apply) |
| A frame waiting on the worker | | under 0.05 ms |

Rewind deltas went from 4-13 MB to 1.3-3.8 MB each, at 10-24 ms of worker time.

## Testing

Headless, wsdbg and the `game.state.*` debugger commands cover all of it. `game.state.save` and
`game.state.load` take a `slot` instead of a `path`, which goes through the slot code like the save
and load keys: undo rotation, load undo, repeats. Use a scratch `--memstick` for that, not the app's,
or the tests overwrite real slots. wsdbg's `--sync` moves on when a request is acknowledged, not when
a `wait` result arrives, so put a `:sleep` between steps whose order matters, and one at the end.

---

### Building (`docs/building.md`)

# Building and testing PPSSPP

Agent-oriented notes on the various build systems and test suites. This is the long version of the
"Build and validation" / "Testing" sections in [AGENTS.md](../AGENTS.md).

## Build and Validation

To verify that things build on Linux/Mac, use ./b.sh --debug. For Windows, use the Visual Studio solution in the Windows subdirectory
(`Windows/PPSSPP.sln`) - always build through it, even if a stray CMake-generated `build/` directory exists at the repo root (e.g.
left over from WSL/MSYS2 experimentation); that directory is not the supported Windows build path and may not have a working
compiler toolchain wired up.

An agent can drive the VS solution non-interactively with `MSBuild.exe` instead of opening the `devenv` GUI. Locate it via
`vswhere.exe` (same tool/gotchas as described in the libretro section below) and build a specific project with `/t:`, e.g.:

```powershell
$installPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath
$msbuild = "$installPath\MSBuild\Current\Bin\MSBuild.exe"
& $msbuild "Windows\PPSSPP.sln" /t:UnitTest /p:Configuration=Debug /p:Platform=<platform> /m
```

(swap `/t:UnitTest` for `/t:PPSSPPWindows` or another project name as needed; drop it entirely to build the whole solution).

On an ARM64 host, use `$installPath\MSBuild\Current\Bin\arm64\MSBuild.exe` instead. The MSBuild in
`Bin\` itself runs emulated and picks the 32-bit x86-hosted compiler (`bin\HostX86\arm64\CL.exe`),
which can't always map the precompiled headers into its address space. Builds then fail
intermittently, every file of a project at line 1, with `C3859: Failed to create virtual memory for
PCH` and `C1076: compiler limit: internal heap limit reached`. That looks like running out of
memory, but lowering `/m` or `CL_MPCount` doesn't help, and `/p:PreferredToolArchitecture=arm64`
has no effect. The build log's `CL.exe` path shows which host compiler ran.

`<platform>` is `ARM64` or `x64` - whichever the machine actually is, so look it up rather than
picking a default. The output directory follows it (`Windows\<platform>\<configuration>\`), which
makes building one and running the other an easy mistake. It is easiest to make on Windows-on-ARM,
where an x64 build runs anyway under emulation: everything appears to work, but it is slower than
the native build and any performance measurement from it describes the emulator rather than the
code. `platform.machine()` in Python reports the host; `$PROCESSOR_ARCHITECTURE` reports the shell,
which is `AMD64` in an emulated shell even on an ARM64 machine.

In addition to the pspautotests runner (test.py), there is a separate binary with C++ unit tests
in the /unittest subdirectory. After substantial changes (at the end of a chunk of work, not
necessarily after every edit), run these too:

- Windows: build the `UnitTest` project (unittest/UnitTests.vcxproj), then run `Windows/<platform>/Debug/UnitTest.exe all` (`<platform>` being `ARM64` or `x64`, whichever you built)
- Linux/Mac: configure with `-DUNITTEST=ON`, then run `build/PPSSPPUnitTest all`

This runs all tests in `availableTests` in unittest/UnitTest.cpp. You can run one or more
specific tests by passing their names instead of `all` (space-separated, e.g. `UnitTest.exe
CmdLine Path Utf8`); no arguments lists the available tests.

After a `git stash`/`git stash pop` that touches a **header**, do a `/t:Rebuild` rather than trusting
the incremental build. Stashing rewrites files with timestamps that can leave objects looking newer
than the header they were compiled against, so adding or removing a class member produces a binary
where translation units disagree on the object layout. That shows up as `UnitTest.exe` segfaulting
before it prints anything, for every test including ones unrelated to the change - which looks like
a catastrophic code bug and is not one. If a build starts crashing inexplicably right after a stash
cycle, rebuild before investigating anything else.

More generally, **when you are bisecting a behavioural change, confirm the binary actually changed
before you believe the result** - check the executable's mtime, or have the code you just added log
something you can grep for. A stale binary is indistinguishable from a real regression, and it lies
consistently, so a bisect on top of one produces a confident, entirely fictional answer. This cost
several hours once: a link that silently failed left a stale `PPSSPPHeadless.exe` in place, every
subsequent "revert this and retest" step reported the same failure, and a change was blamed that a
later clean rebuild proved innocent. The `LNK1168: cannot open ... for writing` case (a still-running
instance holding the exe, see [Debugging](debugging.md)) is the most common way to get there, which
is why killing leftover processes before building is worth doing unconditionally rather than only
when a build complains.

Known environment-specific issue: in at least one sandboxed dev environment, the `Jit` test
(`unittest/JitHarness.cpp`) hangs indefinitely specifically during the `CPUCore::JIT_IR`
phase - confirmed unrelated to source changes (reproduces identically on unmodified checkouts)
and not a memory-access fault (`Memory::HandleFault` is never entered). Root cause wasn't
pinned down further (would need a native debugger attached to the hung process, not available
in that environment) but is very likely specific to that sandbox rather than a real PPSSPP
bug, since CI runs the equivalent of `UnitTest.exe all` on every commit across multiple
platforms without apparent issue. If `all`/`Jit` hangs in your environment, run every other
test by name instead (skip `Jit`) to still get real coverage.

## UWP build

UWP has its own solution, `UWP\PPSSPP_UWP.sln`. Build the app with `/t:PPSSPP_UWP` (or a single
library such as `/t:CoreUWP`), same MSBuild setup as above:

```powershell
& $msbuild "UWP\PPSSPP_UWP.sln" /t:PPSSPP_UWP /p:Configuration=Debug /p:Platform=<platform> /m
```

## Android assets

The APK's `assets/` directory is the repo-root `assets/` directory, wired up by
`assets.directories.add("../assets")` in `android/build.gradle.kts`. There's no copy step, so a file dropped
in `assets/` ships as-is. `ndk-build` doesn't read assets at all, and nothing populates `android/assets/` -
if you have one, it's a leftover from the old Ant/Eclipse build and is ignored.

## Legacy Android build (android/jni)

There is a legacy Android build using the raw NDK build system (`android/jni/Android.mk` + `ndk-build`), separate from the
gradle build in `android/`. It's hooked up on CI (see `.github/workflows/build.yml`, the `android` matrix entries) and is
useful for quick test builds (it can build `ppsspp_headless` and the unit tests for Android). You do not need to build it
by default, but if you want to test-build it locally:

- The NDK path is hardcoded in `android/ab.cmd` (Windows) or passed via the `NDK` env var to `android/ab.sh` (POSIX).
  It should match the `ndkVersion` in `android/build.gradle.kts`. The scripts just run ndk-build with a core count
  derived from the machine (nproc / %NUMBER_OF_PROCESSORS%).
- Example (POSIX): `cd android && NDK=/path/to/ndk ./ab.sh APP_ABI=arm64-v8a HEADLESS=1`
- The `ppsspp_headless` executable ends up in `android/libs/<abi>/`.

## tvOS

The tvOS app is the iOS app with the iPhone-only parts compiled out, checked with
`PPSSPP_PLATFORM(TVOS)` (tvOS also gets `PPSSPP_PLATFORM(IOS)`). The CMake side is the iOS toolchain
with `-DIOS_PLATFORM=TVOS`, which sets `TVOS`.

- `./b-tvos.sh` builds an unsigned app into `build-tvos-xcode/Release-appletvos/`.
  `DEVELOPMENT_TEAM=<team> DEVICE=<udid> ./b-tvos.sh` signs it, registers the Apple TV with the team
  if needed, installs it and launches it with the console attached.
- To pair an Apple TV, put it on Settings > Remotes and Devices > Remote App and Devices. It doesn't
  show in `xcrun devicectl list devices` until paired; find it with
  `dns-sd -B _remotepairing-manual-pairing._tcp` and `xcrun devicectl device info details --device <name>`,
  then `xcrun devicectl manage pair --device <identifier>` and enter the code it shows.
- `./b.sh --tvos --libretro` (and `./b.sh --ios --libretro`) build the libretro cores. tvOS has no
  `sigaltstack`, among other things, so this is the build that catches it.
- There's no tvOS simulator build: FFmpeg has no tvOS simulator libraries.
- tvOS apps have no persistent storage except about 500 KB of `NSUserDefaults`. Anything else may be
  purged, so the memory stick is in Caches for now.

## libretro core build (Windows)

Canonical instructions are in `libretro/README_WINDOWS.txt` - read that first, this is a summary plus
agent-specific gotchas. The libretro core (`ppsspp_libretro.dll`) is built with a real `make`, not the
Visual Studio solution, even on Windows - it uses `cl.exe`/`link.exe` as the compiler/linker (via
`platform=windows_msvc2019_desktop_x64`), but orchestrated through GNU Make running inside an MSYS2
shell (a plain MSYS2 install, not "Git Bash" - typically at `C:\msys64`, needs `pacman -S make`).

```sh
cd libretro
make DEBUG=1 platform=windows_msvc2019_desktop_x64 -j32
```

(drop `DEBUG=1` for a release build; `-j` count doesn't need to match logical CPUs exactly). To test the
result, copy `ppsspp_libretro.*` into wherever the local RetroArch install reads cores from (e.g. its
`cores/` directory) and load it from within RetroArch.

An agent can drive this non-interactively by invoking `C:\msys64\usr\bin\bash.exe -lc "..."` directly
as a subprocess (the `-l` login-shell flag matters - it's what sets up MSYS2's own `PATH`, `make`,
`cygpath`, etc. correctly). In a sandboxed/agentic invocation (as opposed to a normal interactive MSYS2
terminal a human opens), two Windows environment variables the Makefile's VS-detection logic depends on
may not be inherited by the spawned process - `COMSPEC` (breaks the `cmd //c "bash VSWhere.sh ..."` call
used to locate Visual Studio) and `ProgramFiles(x86)` (which `VSWhere.sh` itself needs to find
`vswhere.exe`). If VS auto-detection fails this way, skip it by overriding `VsInstallRoot` directly on
the `make` command line (GNU Make command-line variables take precedence over the Makefile's own `:=`
assignment of the same name):

```sh
make VsInstallRoot="/c/Program Files/Microsoft Visual Studio/<year>/<edition>" DEBUG=1 platform=windows_msvc2019_desktop_x64 -j32
```

(path in MSYS2/cygpath POSIX form, not a raw Windows path; find the real value via `vswhere -latest
-property installationPath` if unsure of `<year>/<edition>`). This is a real full compile+link - prefer
it over trying to syntax-check libretro-specific files with a standalone `cl.exe /Zs` invocation, which
can miss real bugs (e.g. an include-order issue that leaves a platform macro like
`VK_USE_PLATFORM_WIN32_KHR` undefined before `vulkan.h`'s first, include-guarded inclusion, since a
narrower manual include-path/define set used for a syntax-only check may not reproduce the actual build
step's ordering).

## Headless and unittest builds

We have additional PPSSPPHeadless and unit test builds (/headless and /unittest), that have their own separate
main functions (and also stub out most of the System_ functions as needed). Take these into account
when making cross platform changes.

New unit tests are added by listing them in availableTests in unittest.cpp. If they are large, put them in
separate files in the unittest subdirectory. A new file has to be listed in three build files, not two:
`CMakeLists.txt`, `unittest/UnitTests.vcxproj` (and its `.filters`), and `android/jni/Android.mk`, which
builds a unit test executable of its own. The Android one is the easiest to forget, since missing it builds
fine everywhere you are likely to try it and only fails on Android CI.

A unit test is often the first thing to call a given function from outside its own .cpp, which makes the
`ppsspp_unittest` target in the legacy Android build (`android/jni/Android.mk`, see above) the strictest check
we have: MSVC links an `inline` function defined in a .cpp anyway, clang correctly does not. So a test can
build and pass on Windows and fail to link only on Android CI, with an undefined symbol pointing at a header
line. Fix it by dropping the bogus `inline` from the definition, not by avoiding the call.

The compilers also disagree about floating point contraction, which matters for any test asserting that a JIT
is bit-identical to its C++ reference. Clang folds `a * b + c` into a single fused multiply-add by default;
MSVC never does, under `/fp:precise`, in Debug or Release. So on arm64, where the JITs emit `FMLA`, a
reference written as `a * b + c` matches on Mac, Linux and Android and is off by one ULP on Windows on ARM.
Don't leave it to the compiler: write `fmaf(a, b, c)` when the fused result is wanted (MSVC compiles it to a
single `fmadd`), and `a * b + c` when it isn't. `PrescaleUV` in `GPU/Common/VertexDecoderCommon.cpp` picks per
architecture, matching what each JIT does. x86 doesn't have the problem, since the SSE2 baseline has no FMA
instruction to contract into.

pspautotests are a large set of tests of the PSP OS's API surface, and thus tests our HLE implementation.

**To check for regressions, run them exactly the way CI does** (see `.github/workflows/build.yml`):

```bash
python test.py -g --graphics=software
```

**The `-g` matters.** `test.py` keeps two lists: `tests_good` (the regression set - these pass and must keep
passing, ~314 of them) and `tests_next` (work-in-progress tests that are *expected* to fail, i.e. the to-do list).
`-g` runs only `tests_good`; with no flag you get `tests_next + tests_good` and around a hundred failures that mean
nothing is wrong. Don't go hunting those, and don't report them as regressions - the only meaningful result from
`-g` is `0 tests failed`. (`-b` runs only `tests_next`; `-m` prefix-filters whichever list is selected.)

Note the runner prints a debug-CRT "Detected memory leaks!" dump after the summary line on Windows debug builds.
That's normal and not a test failure - read the `N tests passed, N tests failed` line, which comes before it.

See docs/pspautotests.md for a workflow for running pspautotests and improving PPSSPP with the results.

### LoongArch64 and RISC-V JITs under qemu

CI tests these JITs by cross-building headless and running pspautotests under qemu-user, and you can do the
same on Linux (packages: `gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu qemu-user`, or the riscv64
equivalents):

```bash
./b.sh --loongarch64 PPSSPPHeadless     # builds into build-loongarch64/
mkdir -p build-qemu
printf '#!/bin/bash\nexec qemu-loongarch64 -L /usr/loongarch64-linux-gnu "$(dirname "$0")/../build-loongarch64/PPSSPPHeadless" "$@"\n' > build-qemu/PPSSPPHeadless
chmod +x build-qemu/PPSSPPHeadless
python3 test.py -g --graphics=software --cpu=jit-ir --timeout=60 --known-failures=loongarch64
```

`test.py` runs the most recently modified `build*/PPSSPPHeadless`, so `touch` the shim after rebuilding the real
binary, or it runs your native build instead. The full suite takes a couple of minutes.

qemu reports LSX and LASX as present, so CI only exercises the LoongArch vector paths.
The scalar fallbacks never run here, so passing tells you nothing about them.

The 32-bit ARM JIT works the same way with `./b.sh --armhf` (into `build-armhf/`) and `qemu-arm -L
/usr/arm-linux-gnueabihf`. The cross packages are `gcc-14-arm-linux-gnueabihf g++-14-arm-linux-gnueabihf`, and
`sudo cmake/scripts/setup-cross.sh armhf` adds the GL and EGL stubs the link needs. Without root, `apt-get download`
the packages, `dpkg-deb -x` them into one directory, and point `CROSS_ARMHF` at it: the toolchain file then takes
the sysroot from `$CROSS_ARMHF/usr/arm-linux-gnueabihf`. The binutils need `LD_LIBRARY_PATH` to include
`$CROSS_ARMHF/usr/lib/x86_64-linux-gnu`, glibc's `libc.so` linker script names absolute paths that have to be
rewritten to the new root, and the stubs are copied in by hand. For a debugger, run `qemu-arm -g PORT` and attach
`gdb-multiarch` to it.

`hash/hash` dies with a bus error under qemu-arm on every CPU core, so it's not a JIT problem, but it takes the rest
of its batch down with it: pass `--known-failures=armhf`.

Without a cross toolchain (on macOS, say) two things still help:

- **A syntax check of the other JITs.** The RISC-V and LoongArch backends (`Core/MIPS/RiscV`,
  `Core/MIPS/LoongArch64` and their emitters) have no architecture guards, so the host compiler can check
  them: `clang++ -fsyntax-only` per file, with the `-I`/`-D`/`-std`/`-isystem` flags of any Core file from
  `compile_commands.json`, plus `-I<repo> -I<repo>/ext -I<repo>/Common` (the database's include paths are
  relative to the build directory). Check that a copy with a deliberate syntax error does report errors: zsh
  doesn't word-split a `$FLAGS` variable, which once made a broken check look clean. This proves the code
  compiles, nothing about its output.
- **The portable software-renderer paths.** riscv64 and loongarch64 run the software renderer's non-SSE,
  non-NEON C++ paths, so their test failures reproduce natively: wrap the two NEON defines in the
  `__aarch64__` block of `ppsspp_config.h` in `#ifndef PPSSPP_TEST_NO_SIMD`, configure a separate
  `build-nosimd` with `-DHEADLESS=ON` and `-DPPSSPP_TEST_NO_SIMD` in `CMAKE_C_FLAGS` and `CMAKE_CXX_FLAGS`,
  and build `PPSSPPHeadless` there. Delete it and revert `ppsspp_config.h` afterwards, since `test.py` keeps
  picking the newest binary. A failure that's the same on both architectures points at the portable path, not
  the JITs.

## Quick rebuild on Linux

You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut:

```bash
cd build ; make -j32; cd ..
```

---

### Command Line (`docs/command-line.md`)

# Command-line parsing

All command-line parsing for both the main app and the headless build belongs in `Core/CmdLine.cpp` /
`Core/CmdLine.h` (`CommandLineOptions`), not in the platform entry points (`Windows/main.cpp`,
`headless/Headless.cpp`, `UI/NativeApp.cpp`, etc.). Don't re-parse `argv` in those files - add a field
to `CommandLineOptions` instead.

## Declaring an option

Most options are declared in the `g_autoParams` table in `CmdLine.cpp` as:

```cpp
{offsetof(...), type, longName, shortName, docString, mode}
```

`mode` gates the option to `CmdLineMode::Application`, `::Headless`, or `::Both` (the default if the
field is omitted from the initializer).

The same long name can be reused for both modes with different types and meanings, because a given
`Parse()` call only matches params whose mode is `Both` or equal to the current mode. `--log` is the
example to know: a `String` "log to FILE" option in Application mode, a `Bool` "full log output" option
in Headless mode. They don't collide.

Options that can be repeated (e.g. `--ignore TESTNAME`, collected into a `std::vector<std::string>`), or
that don't fit the generic single-value table, need manual handling in the `else if` chain inside
`CommandLineOptions::Parse()` - the same way `--graphics=` and the `boot` filenames are handled.

## Getting a parsed option into the emulator

`ApplyToConfig()` is where parsed options are pushed into `g_Config` / `g_logManager`. Prefer wiring a
new option through there, so every platform gets it for free, rather than reading `CommandLineOptions`
fields ad-hoc at each call site.

`NativeInit()` in `UI/NativeApp.cpp` still takes `argc`/`argv`, because several platform entry points
pass them in, but it shouldn't read them directly: by the time `NativeInit()` runs,
`CommandLineOptions` should already hold everything.

---

### Cpu Equivalence (`docs/cpu-equivalence.md`)

# CPU core equivalence

The goal: every CPU core (interpreter, IR interpreter, JIT, IR JIT) produces the same bits on every
host (x86-64 and ARM64), NaN payloads (and possibly NaN signs) aside. Where the cores disagree, the
hardware decides which one is right, so several items below need a test on a real PSP before they
can be fixed.

## The fuzzer

`UnitTest CPUEquivalence` (in `unittest/JitHarness.cpp`) generates random code (ALU, immediate,
FPU, VFPU, loads and stores, optionally forward branches) and a random start state, runs it on every core, and
compares the full end state against the interpreter: GPRs, FPRs, VFPU registers and control
registers, hi/lo, fcr31 (bit 23 aside, which some cores sync from fpcond and some don't), fpcond,
llBit, 1KB of scratch memory, and the cycles charged up to the terminating syscall. NaNs compare
equal to each other. Loads and stores go through `k0`, which points at the scratch memory and is
never written.

Each differing case is minimized (instructions dropped while it still differs, or turned into nops
when the code has branches, so none moves its target) and printed with its inputs. It ends with a count of the reported cases by their last instruction, which is usually the
one at fault. By default only a crash fails the test, as known differences remain.

Environment variables:

| Variable | |
|---|---|
| `PPSSPP_CPUFUZZ_COUNT` | number of cases (default 2000) |
| `PPSSPP_CPUFUZZ_SEED` | seed of the first case; case i uses seed + i |
| `PPSSPP_CPUFUZZ_CLASSES` | weighted string of `a` (ALU), `i` (immediate), `f` (FPU), `v` (VFPU), `m` (memory), `r` (a run of vdots the IR batches into one `Vec4VfpuMultiDot`); `b` anywhere adds forward conditional branches |
| `PPSSPP_CPUFUZZ_CODE` | comma-separated hex instructions to run instead of the generated code |
| `PPSSPP_CPUFUZZ_CODE_SEED` | apply `PPSSPP_CPUFUZZ_CODE` to this seed only |
| `PPSSPP_CPUFUZZ_SKIP` | cores to skip, by digit: 1 IR interpreter, 2 JIT |
| `PPSSPP_CPUFUZZ_REPORTS` | how many differences to minimize and print (default 20) |
| `PPSSPP_CPUFUZZ_VERBOSE` | print every case before running it, to find one that crashes |
| `PPSSPP_CPUFUZZ_DUMP` | write every core's end state of every case to a file |
| `PPSSPP_CPUFUZZ_STRICT` | fail on any difference, not only on a crash |
| `PPSSPP_CPUFUZZ_OPS` | comma-separated instruction name prefixes to generate only; `dot` is the dot products and the prefix ops |
| `PPSSPP_CPUFUZZ_HOSTEXT` | `+` or `-`: turn the JIT's optional host extensions on or off (RISC-V Zba/Zbb/Zbs/Zcb/Zfa/Zfh/Zicond, and V with `-`, LoongArch LSX/LASX); the `VFPUDotJit` test follows it too |
| `PPSSPP_CPUFUZZ_BLOCKS` | print the IR and host code of every block the JITs ran, after each case |
| `PPSSPP_CPUFUZZ_UNKNOWNPREFIX` | compile as after a block left a prefix uneaten |

For quick rounds while working on some instructions, fuzz just those, on every core of the machine:
`PPSSPP_CPUFUZZ_OPS=dot Tools/cpu_fuzz_parallel.py build/PPSSPPUnitTest --count 2000` splits the seeds
over several processes and sums up the results.

Once a block ends with a prefix uneaten, the IR frontend stops assuming that blocks start with the
default prefixes (`startDefaultPrefix`), for as long as the JIT exists. In a run, the first case that
leaves a prefix does this, so whether a later case compiles one way or the other depends on the
cases before it, and a difference may not reproduce with that seed alone.
`PPSSPP_CPUFUZZ_UNKNOWNPREFIX` puts every core into that mode first.

Always set `CI=1` when running it from a script on Windows. Otherwise a JIT assert opens a message
box and the run hangs until someone clicks it.

`Tools/cpu_fuzz_reduce.py` runs the fuzzer and, if a case crashes (usually a JIT emitter assert),
finds it and reduces it to the instructions still needed for the crash. A crash that only happens
after other cases have run usually means uninitialized or stale state, not the reduced code.

To compare hosts: run with the same seed and count and `PPSSPP_CPUFUZZ_DUMP` on each, and diff the
files. Each line starts with the core and the seed. A Windows-on-ARM machine can run the x64 build
under emulation, which is enough for the host-dependent float behavior of the cores. It isn't for
timing, of course.

## Fixed

Crashes (JIT emitter asserts) and wrong code found by the fuzzer. Entries for the "ARM64 JIT", the
"x86 JIT" and "both JITs" are about the legacy JITs, which have since been removed. Every JIT is now
IR-based (the "IR JIT" and "native IR backend" entries).

- ARM64 JIT: `ext` with pos + size past 32, and `ins` with msb below pos, asserted in UBFX/BFI.
- ARM64 JIT: `ApplyPrefixST` read an uninitialized register for a swizzle naming a lane past the
  op's size. The ARM64 JIT now gates its VFPU ops on prefixes the way the IR frontend does (vdot,
  vcmp, vi2f, vi2x, vocp, vhdp, vcmov, the matrix inits), and the x86 JIT got the gates it was
  missing too (vdot, vhdp, vcmp, vf2i, vh2f, vi2f, vi2x, vocp, vsgn, the color conversions, the
  matrix inits, vmmov, vmscl).
- ARM64 JIT: vdot, vhdp, vmmul and vqmul used fused multiply-add, which rounds differently from
  every other core. They now multiply and add separately, in the interpreter's order.
- vcmov in both JITs ignored a T prefix, which the interpreter applies to D (read as T).
- IR: vmmul, vmscl, vmidt/vmzero/vmone and vmmov on a 1x1 matrix flipped the transposed bit, which
  for 1x1 selects a different register. vmmul's fallback never wrote a 1x1 result at all.
- IR: vmmul, vtfm, vmscl, vmmov and the matrix inits took a matrix that isn't a plain Vec4 layout to
  be a transposed one, so a 4x4 matrix with a row/column offset (like `E723` or `M212`) produced
  Vec4 ops on unaligned registers. Native IR backends asserted, the IR interpreter computed garbage.
- IR: with every lane write-masked by the D prefix, the destination lanes were all D temps, which
  looked like a Vec4, and the mask blend then went into an unaligned destination.
- x86 JIT: `sc` into `$zero` wrote its result to an immediate and asserted. vmmul's SIMD path
  crashed on `vmmul.s` and on matrices that wrap around (`MatrixWrapsAround()`), which now go to
  the interpreter and the scalar path.
- Native IR backends (all four): `ll` into `$zero` mapped `$zero` for writing and asserted.
- ARM64 and x86-64 IR JITs: `sc` didn't mark rt dirty, so its 0/1 result was lost.
- IR interpreter: never applied the guest's rounding mode or flush-to-zero (fcr31) on block entry,
  only after a `ctc1` in the same block. It now does what the native dispatchers do, and leaves the
  host's mode in place around syscalls and replacement functions.
- JITs: the mode was only taken into account once a `ctc1` had been compiled (`js.hasSetRounding`),
  so a mode from a savestate made on another core, a core or thread switch, or an interpreted
  `ctc1` was ignored, and syscalls then ran in the guest's mode. `UpdateFCR31()` now sets the flag
  and throws away the blocks compiled before. Ops left to the interpreter run in the guest's mode,
  except syscalls, and an interpreted `ctc1` takes effect at once. `CPURoundingMode` tests all of
  this on every core, with and without `JitDisableFlags`.
- ARM64 JIT: `mfhi`/`mflo` into `$zero` wrote the host register holding zero.
- x86-64 IR JIT: helper calls (vsin, vrsq and the like) didn't flush RCX and RDX outside Windows,
  where the System V ABI doesn't preserve them.
- MIPSAnalyst: `ctc1`/`cfc1` of a control register other than 31 counted as touching the FPU
  condition, so the x86 JIT's register cache dropped a dirty fpcond before one.
- x86 JIT: fpcond held the compare's all-ones mask instead of 1.
- ARM JITs: `vmfvc` of a control register past the defined ones mapped the dest without
  `MAP_DIRTY`, so its 0 was never written.
- IR: `vmmov` between transposed matrices whose rows wrap around (like `E020`) produced unaligned
  Vec4 moves, which crashed the x86 IR interpreter.
- RISC-V and LoongArch64 JITs: see below.
- The legacy JITs' `Comp_Generic` ran the interpreted instruction in the host's default rounding
  mode instead of the guest's.
- IR: vmscl and the matrix inits of two transposed matrices flipped the transposed bit to get
  Vec4s, which for a matrix with an offset (one that wraps around) names other registers. vmscl
  also took its Vec4 path before checking that the matrices don't partly overlap.
- IR: vc2i and vuc2i read their source as a single, not as the first lane at the size encoded.
- IR: vf2i and vi2f wrote each lane straight to a destination that could hold a later lane's source.
- IR: vcmov under a saturating D prefix left lanes that didn't move with whatever the D temp held.
- IR: vmtvc to a control register that masks its value read the VFPU register numbered like the
  control register.
- IR `OptimizeFPMoves`: propagated a copy into a multi-lane result covering the copied register,
  and into the src3 of ops where it's also the destination (FCmovVfpuCC), moving the result.
- IR `OptimizeLoadsAfterStores`: compared offsets as unsigned, so a store at -16 of 16 bytes didn't
  cover a load at -12, which reused the value from before the store.
- Native IR backends: flushing a host register that cached `$zero` left it mapped, so after a call
  that clobbered it (the dot product routine), a store of `$zero` stored garbage.
- The IR frontend continues past conditional branches, so a block can now leave with a prefix
  pending at a mid-block exit. It doesn't continue then, as only the end of a block checks that.
- Hardware answers 1, 2 (host agreement), 5, 7 (the interpreter and `vcmov`), 8, 9 and 10 below.

Timing, which decides when interrupts, thread switches and other events happen, so a difference
sends a game down a different path on each core:

- IR: `j`, `jal`, `bc1t` and `bc1f` didn't charge their delay slot.
- IR: a syscall charged itself before running, or, in a delay slot, never. It now sees every
  earlier instruction charged and is charged once it returns, as in the interpreter.
- Interpreter: a skipped likely delay slot cost 1 cycle, the IR cores charge what it would cost.
- Interpreter: it checked downcount after every instruction, the IR cores only between blocks. It
  now runs to the same block ends (branches and jumps after their delay slot, syscalls, breaks,
  replacements), so events happen at the same instruction. Its fast loop also charged a branch
  and its delay slot together after both, so a syscall in the slot didn't see the branch.

The interpreter also follows the IR frontend on past a conditional branch that isn't taken
(`IR_CONTINUE_BRANCHES`), with the same limits, except in one case it can't see: the IR takes a
prefix register written by `mtvc` or `vmtvc`, or by a VFPU op it interprets that doesn't consume
prefixes, as unknown, where the interpreter goes by the value. Games set prefixes with `vpfx`.
The fuzzer only compares cycles at the end, so it doesn't see where a block ends; a trace of
syscalls and thread switches from a game does (God of War, Tekken 6 and Daxter agree on all three
cores).

## Open differences

The goal, in order: every core gives the same result on both hosts, and then that result is the
PSP's. Where matching the PSP is expensive, agreeing with each other comes first.

### Answered on hardware

Each of these has a pspautotests test recorded on a PSP (6.61). Distinct outputs across the eight
configurations (four cores, ARM64 and x86-64 under Rosetta), and lines that differ from the PSP.
The `jit` columns were measured with the legacy JITs, since removed; `jit` is now `jit-ir` on both.

| Test | Distinct outputs | ARM64 interp / ir / jit / jit-ir | x86-64 interp / ir / jit / jit-ir |
|---|---|---|---|
| `cpu/cpu_alu/shift_encodings` | 1 | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 |
| `cpu/cpu_alu/allegrex_encodings` | 1 | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 |
| `cpu/fpu/denormal` | 2 | 54 / 54 / 54 / 54 | 54 / 54 / 54 / 54 |
| `cpu/vfpu/fcr31` | 2 | 50 / 50 / 50 / 51 | 50 / 50 / 50 / 51 |
| `cpu/vfpu/signed_zero` | 4 | 14 / 37 / 37 / 37 | 14 / 37 / 36 / 37 |
| `cpu/vfpu/nan_gen` | 7 | 51 / 51 / 57 / 57 | 51 / 51 / 51 / 54 |
| `cpu/vfpu/undefined` | 4 | 0 / 6 / 3 / 6 | 0 / 6 / 9 / 6 |
| `cpu/fpu/fpu_nan` | 2 | 0 / 0 / 0 / 0 | 4 / 4 / 4 / 4 |

Before the fixes, `shift_encodings` had 2 outputs and `fcr31` 7. `nan_gen`, `fpu_nan` and
`denormal` still come out as each host's own NaNs (item 6). What's left of `fcr31` is items 3 and
vdot's summation, and of `undefined` the horizontal ops and matrix registers with an offset that doesn't fit the size. Rosetta behaves as an x86 CPU here: the x86-64 interpreter made
`0xFFC00000` NaNs and flushed denormal results but not inputs under FS (FTZ without DAZ).

The fuzzer agrees: integer, FPU and memory code (`PPSSPP_CPUFUZZ_CLASSES=aaiiffm`) gives the same
end state on every core and on both hosts in 20000 cases, apart from item 11 below. With VFPU code
mixed in, the interpreter differs between the hosts in 7 of 20000 cases, each a host NaN out of a
horizontal op (vmmul, vtfm, vdot, vdet) that vmin, vmax or vf2i then orders or converts by its
sign.

1. **srl/srlv with stray bits** (`shift_encodings`, fixed). Only the low bit of the unused field
   counts: `srl` rotates when bit 21 is set and shifts when it is clear, whatever bits 22-25 hold,
   and `srlv` the same with bit 6. sll/sra/sllv/srav ignore the field. The Allegrex bit ops
   (`allegrex_encodings`, fixed) decode only part of their op field too, so every value is one of
   wsbh, wsbw, seb, seh and bitrev (see `AllegrexBitOp()`), and special3 function 0x18 is a no-op.
2. **Flush-to-zero (FS)** (`denormal`). With FS clear the PSP handles denormals in full, which
   both hosts match. With FS set it flushes denormal results to zero, with tininess decided before
   rounding (a product that rounds up to FLT_MIN is still flushed), but never flushes inputs: sqrt,
   div and the compares see a denormal as it is. One exception: add/sub with a zero operand
   returns the other operand, denormal or not. x86 FTZ gets all of it but that exception (16
   lines). ARM64's FZ also flushes inputs (54 lines). The x86 cores now set DAZ along with FTZ, so
   both hosts flush inputs and agree, 54 lines away from the PSP. Matching the PSP needs either
   the x86-like mode where ARM64 has it (FEAT_AFP: FPCR.AH with FIZ clear, not on every device),
   or flushing results in software when FS is set.
3. **fcr31 doesn't affect the VFPU** (`fcr31`). All six fcr31 settings give identical results.
   The VFPU always rounds to nearest, and always flushes denormal inputs and outputs to zero
   (`vf2iu` of a denormal is 0, a denormal times 1 is 0). The cores should run VFPU ops in
   round-to-nearest without guest FS, and flush denormals themselves, rather than inheriting the
   FPU's mode. Some of the remaining lines are vdot's exact summation (`1.5u + 1.5u + 3 - 3` is 0
   on the PSP). The cores agree with each other here (they all follow fcr31), so this is about
   matching the PSP. A check on every VFPU op would cost too much. Since games run VFPU ops in
   clumps, switching the host into round-to-nearest with flush-to-zero (inputs too) around a clump
   and back afterwards may be cheap enough; not tried yet. The ops that move to the accurate dot
   product get this right by themselves.
4. **Signed zero from sums** (`signed_zero`). Every summing op returns +0 for a zero sum,
   whatever the signs of the products: vdot, vhdp, vfad, vavg, vdet, vcrsp, vqmul, vmmul, vtfm,
   vhtfm, at every size, including vfad of all -0. vcrs (a product per lane) and plain vadd keep
   -0. The interpreter is right apart from vcrsp, vqmul and vtfm/vhtfm.
5. **vmin/vmax and denormals** (`fcr31`). Denormals compare equal to zero, a tie returns the
   second operand, and the result keeps the operand's bits (a denormal comes out unflushed). Fixed
   on every core with `vfpu_minmax_key()`, an integer key compared instead of the floats
   (`minmax_zero` and `minmax_tie` pass everywhere).
6. **Generated NaNs** (`nan_gen`). VFPU arithmetic never keeps a payload: every NaN it produces is
   `0x7F800001` or `0xFF800001`. The sign is + for add, sub and every summing op, the XOR of the
   operand signs for mul, div and vscl, the input's sign for vrcp, vrsq, vsin and vasin, and + for
   vsqrt, vlog2 and vexp2. vsat0/vsat1, vabs and vneg pass NaN bits through. The cores make the
   host's NaN instead, so x86-64 and ARM disagree. The
   branch `cpu-equiv-4` fixes the single-lane ops (vadd, vsub, vmul, vdiv, vscl, vcrs, vsat0/1, vbfy,
   vocp, vsocp) on every core, through four new IR ops and checks in the legacy JITs, and is held
   back until its cost on every multiplication is measured. The horizontal ops wait for the
   accurate dot product.

   The FPU makes the positive quiet NaN `0x7FC00000` from operands that aren't NaN
   (`cpu/fpu/fpu_nan`), which ARM64 does and x86 doesn't for add, sub and div. Also fixed on
   `cpu-equiv-4`.
7. **Undefined VFPU encodings** (`undefined`). The interpreter matches the PSP on all of them
   (single-lane vdot/vhdp, vdet at every size, vcrsp/vqmul at .s/.p, matrix registers that wrap
   around, vtfm/vhtfm size mismatches, vmfvc past register 15 reading 0, vcmov with bit 20 set),
   now including `vcmov` with imm3 = 7, which tests a CC bit that is always clear: vcmovt never
   moves and vcmovf always does (fixed on every core). The JITs still differ on the single-lane
   dot products and vcrsp/vqmul at .s/.p (for the accurate dot product). The IR cores now match on
   matrix registers whose offset doesn't fit the size (see Fixed).
8. **Loads and a pending D prefix** (`prefix_load`, fixed). lv.q, lv.s, lvl.q, lvr.q and mtv write
   every lane, ignoring the D prefix's mask and saturation, and leave it pending. The interpreter
   applied the mask to lv.q, lvl.q and lvr.q (and the IR cores, which hand lvl.q and lvr.q to it).
9. **vfim** (`vfim`, fixed). The half float converts like vh2f: denormal halves give a signed zero,
   inf and NaN keep their mantissa bits in place. The interpreter halved denormals, the IR made
   them IEEE.
10. **bltzal and friends with rs = ra** (`branch_link_ra`, fixed). The branch tests ra's value from
   before the link. Every core linked first.

### Known, no hardware test needed

11. **Divide-by-zero flags.** Only the interpreter sets the cause and flag bits for `div.s` by zero
   (see `Int_FPU3op`). The other cores leave fcr31 alone. Making the others match needs a new IR op
   or an interpreter fallback for div.s, and the latter costs every division.

### ARM32

Measured on a Galaxy S8+ and A21 with the armeabi-v7a build (`android/ab.sh APP_ABI=armeabi-v7a HEADLESS=1
UNITTEST=1 SKIPAPP=1`, run over adb). These measurements predate the IR-based ARM32 JIT, when
`CreateNativeJit` returned the legacy `ArmJit` (since removed). The IR JIT gives the IR interpreter's
end state in every fuzz case under qemu-arm (`./b.sh --armhf`, see building.md).

- The interpreter gives the same end state as on ARM64 in all 20000 integer/FPU/memory fuzz
  cases, and in all but 3 of 20000 mixed ones (horizontal-op NaNs into vmin/vmax, as on x86-64).
- All three cores match the Mac's same core on every `cpu/` pspautotest, apart from horizontal ops
  and NaNs: `signed_zero` (vdet, vqmul in the JIT), `nan_gen`, and in the JIT the NaNs out of vsat
  (`prefix_sat`) and vbfy (`specials`). Those wait for the accurate dot product and item 6.
- What it took: the interpreters apply the guest's FPSCR (rounding mode and FZ); the IR
  interpreter's NEON Vec4 paths are ARM64-only, since ARMv7 NEON always flushes and rounds to
  nearest; ARM32's libm `ceilf`/`floorf` don't flush a denormal input, so `FlushForConversion()`
  does that under FS; and the ARM JIT got the other JITs' prefix gates, mtvc masking, and fixes to ins/ext/max/min, round.w.s of NaN, and vsgn. `vh2f` and `vasin` go to the
  interpreter (the hardware half conversion quiets NaNs).
- Before these JIT fixes, the fuzzer on the S8+ had the JIT differ from the interpreter in 1298
  of 20000 mixed cases (the IR interpreter in 1446), not broken down. It crashes on the A21 in its
  memory setup, so it hasn't been rerun since.

### RISC-V and LoongArch64

Run under qemu-user on x86-64 Linux (cross toolchains from Ubuntu's `gcc-15-riscv64-linux-gnu` and
`gcc-15-loongarch64-linux-gnu`, configured with `-DHEADLESS=ON -DUNITTEST=ON`). Each has three
cores, and the JIT is the IR JIT. RISC-V detection only reports Zba/Zbb/Zbs/Zicond for two SoCs and
never Zcb or Zfa, so the fuzzer forces them with `PPSSPP_CPUFUZZ_HOSTEXT=+` (under
`QEMU_CPU=max`); LoongArch runs both with LSX (qemu's la464) and without (`-`).

Fixed:

- RISC-V emitter: `c.lbu`/`c.sb` (Zcb) had their two offset bits swapped, so byte loads and
  stores at offset 1 or 2 went to the wrong byte.
- RISC-V emitter: `CanFLI` took any NaN for FLI's canonical one, so `vfim` of a NaN lost its
  sign (Zfa).
- LoongArch JIT: `vsat0`/`vsat1` turned a NaN into the bound (FMIN/FMAX return the non-NaN
  operand), and `vcmp NE` was false for a NaN.
- Both JITs: `Vec4Dot` (vdot, vhdp, vtfm, vmmul) used fused multiply-add. Without SIMD it now
  multiplies and adds in the IR interpreter's order; LoongArch's LSX path adds pairwise, like
  the x86 and ARM64 JITs.
- Interpreter: `vsgn` of a negative NaN gave 1 on RISC-V, whose subtraction always makes the
  positive canonical NaN.
- RISC-V emitter: `vmv.s.x` and `vfmv.s.f` were encoded as `vmv.x.s` and `vfmv.f.s`, writing the
  scalar register instead of the vector (nothing used them before the dot product routine).

The accurate dot product (`Vec4VfpuDot`) runs through a routine both JITs generate at startup and
call, like the ARM64 and x86-64 ones (`RiscVVfpuDot.h`, `LoongArch64VfpuDot.h`). RISC-V has a
scalar version, which uses Zbb and Zicond where it has them, and an RVV one used when the V
extension is detected. LoongArch has an LSX version and a scalar one. Inf and NaN go to
`vfpu_dot_reference()`. In the interpreters, `vfpu_dot()` is `vfpu_dot_scalar()` on RISC-V and
`vfpu_dot_lsx()` on LoongArch. The `VFPUDotJit` unit test runs every variant the host can run
against the reference on `VFPUDot`'s inputs. Whether the RVV version beats the scalar one on real
hardware hasn't been measured: it moves values between the vector and scalar units three times,
which costs more on some cores than on others.

Neither JIT has `Vec4VfpuMultiDot`, so the frontend leaves runs of dots as single
`Vec4VfpuDot`s for them (`IROptions::vfpuMultiDot`). Both have the Vec4 vmin, vmax, vslt, vsge and
vi2f ops (`IROptions::moreVec4Ops`), lane by lane on RISC-V and on LoongArch without LSX.

Before the accurate dot product, the JIT gave the same end state as the IR interpreter in 20000
VFPU-heavy cases (`vvvf`), 20000 mixed and 10000 integer/memory ones, on RISC-V with and without
the extensions and on LoongArch without LSX. What was left were host NaNs out of a horizontal op
that something then orders by sign (item 6), and with LSX the summation order of the dot product.
With it, the JIT matches the IR interpreter bit for bit in 20000 default cases on LoongArch with
and without LSX, and in 6000 on RISC-V with RVV, without V and the extensions, and with the
extensions. The same holds in at least 4000 dot product cases (`PPSSPP_CPUFUZZ_OPS=dot`) in each of
those, and with unknown prefixes.

Against the other hosts:

- Neither ISA can flush denormals, so FS does nothing (`cpu/fpu/fpu` and `cpu/fpu/denormal` fail,
  63 lines from the PSP where x86 and ARM64 have 54). Both are in `known_failures` in `test.py`.
- RISC-V never propagates a NaN operand: every NaN it makes is `0x7FC00000`, which matches the
  PSP's FPU (`cpu/fpu/fpu_nan` passes) but loses a NaN's sign and payload (`cpu/fpu/roundmode`).
  LoongArch propagates operands like x86 and makes the positive default NaN like ARM64, and also
  passes `fpu_nan`.
- Every core gives the same results as on x86-64 in 20000 integer/FPU/memory cases but one, a
  `div.s` of a denormal under FS. With VFPU code, the interpreters differ by host NaNs from
  horizontal ops into vsbn, vmin, vmax or vscmp.

### Not looked into yet

- In 32000 default cases, the IR cores now differ from the interpreter only in item 11, on ARM64.
- Backward branches and loops: the generated branches only go forward. Prefixes across blocks only
  as far as a forward branch carries them.

## Unrelated

Headless used to take `-r` for the IR interpreter, but `--root` (whose short form is `-r`) won and took
the next argument as a directory. The short cpu core flags are gone now; use `--cpu=ir`.

---

### Debugging (`docs/debugging.md`)

# Debugging PPSSPP (agent notes)

How to drive PPSSPP's debugger facilities from a script or agent session, and the traps around them.
The WebSocket protocol reference itself is in [WebSocketDebugger.md](WebSocketDebugger.md); the
threading rules for debugger code are in [DebuggerThreading.md](DebuggerThreading.md).

## WebSocket debugger

PPSSPP has a JSON/WebSocket debugger and automation API (connect, read/write memory, search memory for values or byte
patterns, set breakpoints, step the CPU, label data symbols, read GPU state, inject input, tail logs, etc.), served on
the same port as Remote ISO sharing at `/debugger` with subprotocol `debugger.ppsspp.org`. Implementation is in
`Core/Debugger/WebSocket.cpp` and `Core/Debugger/WebSocket/*Subscriber.cpp` (one file per feature area, each
documented at the top). Enable it via Settings > Tools > Developer Tools > "Allow remote debugger",
`RemoteDebuggerOnStartup` in the config, or `--debugger=PORT` on the command line (`0` = pick a port automatically) -
works on both the application and headless builds. On headless it also forces a break at start (`startBreak`), so the
CPU halts before anything runs. The bundled web GUI at `/debugger/` comes from the `assets/debugger` submodule
(`unknownbrackets/ppsspp-debugger`, `bundled` branch).

The `bundled` branch only holds built output, so `assets/debugger/static/js/main.*.js` in this tree is minified -
don't try to answer "does the web GUI use this event/parameter?" by reading it, the identifiers are mangled and you
will guess wrong. **The unminified source is at https://github.com/unknownbrackets/ppsspp-debugger** (default branch,
not `bundled`) - fetch or grep that when you need to know what the official client actually sends, e.g. before
changing or removing part of the protocol.

**Before touching this interface, read `docs/WebSocketDebugger.md`** - it has the full protocol reference and event
catalog (including which events are read-only vs. require `cpu.stepping` first). Don't guess event names or
parameters from memory; the doc (and each `*Subscriber.cpp` file's per-handler comments) is the source of truth, and
new events get added over time (e.g. `memory.search`, `hle.data.*`).

When adding new commands, don't forget to update `docs/WebSocketDebugger.md`,

To quickly get a live session going for manual testing (e.g. after adding/changing an event): build `PPSSPPWindows`
(see [Building and testing](building.md)), then run it with `--debugger=PORT` and something that keeps running/looping so the
CPU stays alive, so requests get a response instead of "CPU not started"/"CPU not active" errors. Any homebrew or
game works; PSP homebrew isn't checked into this repo, so if you don't already have something installed under
`memstick/PSP/GAME/`, ask the user for a `.iso`/`.cso`/`.elf`/`EBOOT.PBP` to boot, or to install one via the in-app
Homebrew Store. Watch the log output (`--log=somefile.log`) for the line `Listening on port N`, then point
`Tools/wsdbg/` at that port (`cargo run -- N <event> [key=value...]` for one-shot, or `cargo run -- N` for a REPL).
Most mutating events (`hle.func.*`, `hle.data.*`, memory writes while paused, etc.) require the CPU to be stopped
first - send `cpu.stepping` and `cpu.resume` to pause/unpause.

Alternatively use the headless build, Windows/{arch}/Debug/PPSSPPHeadless.exe or build/PPSSPPHeadless on CMake-based
platforms. Where arch is x64 or ARM64.

### Driving a headless debugger session (gotchas)

A working invocation, and the traps around it:

```bash
./Windows/x64/Debug/PPSSPPHeadless.exe --cpu=interpreter --debugger=34567 --timeout-wall=100000 --graphics=software --log \
    --root pspautotests/tests/../ pspautotests/tests/cpu/cpu_alu/cpu_alu.prx > hl.log 2>&1 &
# wait for "Listening on port" in hl.log, then:
./Tools/wsdbg/target/release/wsdbg.exe 34567 --sync --sync-timeout 15 < script.txt
```

- **The timeouts apply to the whole session**, not per test. `--timeout-wall=N` is real seconds and
  `--timeout-emulated=N` is emulated ones; both default to infinity, both can be set at once, and whichever
  is reached first ends the run (`TIMEOUT` or `TIMEOUT (emulated)`). With `--state=`, emulated time counts
  from when the state has loaded: until then the game boots for however long the load takes on the host, so
  counting from boot made runs end on different frames. `--timeout` is the old name for
  `--timeout-wall`. As soon as you pass one it applies to your whole interactive debugging session too, so
  pass something huge (`--timeout-wall=100000`) or the process exits out from under you mid-session. The
  wall-clock check is skipped while `IsDebuggerPresent()`; the emulated one doesn't need that escape hatch,
  since sitting at a native breakpoint burns no emulated time.
- **Which one you want depends on the question.** Wall-clock is what stops a hang from hanging the machine.
  Emulated is what you want for "has the game had long enough to get somewhere" - a heavy scene runs many
  times slower than real time and a near-idle one much faster, so the same wall-clock budget means very
  different amounts of game time. Booting a firmware VSH to its XMB is a good example: 10 emulated seconds
  is about 25 real ones on 6.61 and about 7 on 2.00.
- **Prefer `--debugger=0` and scrape `Listening on port N` from that run's own log** over hardcoding a port. Also
  `taskkill //F //IM PPSSPPHeadless.exe` between runs for hygiene (Git Bash here has no `pkill`) - leftover
  instances are easy to accumulate when a script leaves the CPU stopped at a breakpoint.
- **On Linux, kill leftovers with `pkill -x PPSSPPHeadless`, never `pkill -f PPSSPPHeadless`.** `-f` matches full
  command lines, and the shell running your `pkill` has "PPSSPPHeadless" in its own command line, so it kills
  itself (exit 144) and the emulator instances you meant to clear survive into the next run.
- **Attaching gdb to a running instance fails under WSL/Ubuntu** (`ptrace_scope`: "Could not attach to process"),
  so a hang has to be caught by starting the run under gdb. Interrupting a batch-mode gdb from another shell with
  `pkill -INT` never got as far as the backtrace in the 2026-09-22 attempts. Before reaching for gdb, first rule out
  the cheap explanations below (backend, flags) by diffing a good and a bad command line.

  Some history, because it silently produced a round of bogus results before it was fixed: `Common/Net/HTTPServer.cpp`
  used to set `SO_REUSEADDR`, which on Winsock means "allow binding a port someone else is already listening on"
  (unlike POSIX, where it only covers TIME_WAIT). Two instances would *both* bind the same explicit port and *both*
  log `Entering web server loop. Listening on port 34567`, with the winner of any given connection undefined - so a
  client aimed at a fixed port could end up driving a leftover process running a different binary, CPU backend, or
  game. It's `SO_EXCLUSIVEADDRUSE` on Windows now, so the second bind fails honestly, and a non-zero `--debugger=PORT`
  that can't be honored is fatal in headless (exit 1) instead of falling back to a random port. If you still suspect
  you're talking to the wrong process, the `version` response carries `pid` and `path` - check them.
- **The headless build defaults to JIT** (`Headless.cpp`, `CPUCore cpuCore = CPUCore::JIT`), despite
  `g_Config.iCpuCore` being force-set to INTERPRETER just above - `ApplyToConfig()` has the final say. Pass
  `--cpu=interpreter` for the interpreter. (`-r` is `--root`, which eats the next argument as the root path.)
- When a test finishes, headless exits and the WebSocket connection closes (`CloseFrame { code: Away }`). So "the
  connection just closed" after a `cpu.resume` normally means **the breakpoint you were counting on never tripped**
  and the game ran to completion - not a transport problem.
- Some events deliberately never respond while the CPU is stepping, so `--sync` will burn its full timeout on them:
  `gpu.stats.get` and `gpu.stats.feed` (documented - they answer after the next flip), `gpu.record.dump`, and
  `input.buttons.press` (waits for N frames). Send them with wsdbg's `:nowait`, and let the next line
  (a `cpu.runUntilTime`) run the CPU:
  `:nowait input.buttons.press button=circle duration=4`. `duration` counts vblanks, not seconds.
- **Don't send a bare `cpu.resume` in a `--sync` script.** `--sync` waits for the CPU to stop again, which
  a free run never does, so it burns the whole `--sync-timeout` while headless runs unthrottled, which can
  be hundreds of emulated seconds. Use `cpu.runUntilTime`.
- Log broadcasts drown scripted output. Pass `--quiet` to wsdbg, which turns them off.
- **Nested parameters work in wsdbg's `key=value` shorthand**: values are parsed as JSON, and single quotes keep
  the inner double quotes, e.g. `input.buttons.send buttons='{"cross":true}'`. Prefer that over a raw JSON line,
  which gets no ticket (see below).
- Keep wsdbg scripts in files and pipe them in, rather than building JSON inline in a shell command - inline
  `{"event":...}` in a bash heredoc trips Claude Code's command analyzer ("brace with quote character") and forces a
  manual approval prompt for every single invocation.
- **`--sync` can only match a response to a request that carries a ticket**, and wsdbg only assigns tickets to its
  `key=value` shorthand. A raw JSON line without a `ticket` isn't waited for at all, so use the shorthand (nested
  values included, see above). Hex works there: `memory.disasm address=0x08804000`.
- **To see the screen from a script, use wsdbg's `:screenshot <file.png>`** while the CPU is stopped, e.g. after a
  `cpu.runUntilTime`. With headless, run `--graphics=software` for it: headless Vulkan has no output image to read
  back and asserts. Give a native path on Windows (`C:/...`, not `/c/...`).
- **Headless reports `SYSPROP_HAS_DEBUGGER` as false** (only `Windows/main.cpp` implements it), so anything gated on
  it does nothing there - `LoadSymbolsIfSupported()` in `Core/System.cpp`, for instance, doesn't load `.ppmap`/`.sym`
  at all under headless. Gate new debugger-adjacent features on their own config flag, not on that property.
- Headless defaults its memstick to `memstick` next to the executable (`Headless.cpp`). Pass `--memstick=DIR` to
  point it at a real one instead - e.g. the same directory the app build uses - rather than copying a game in.
- Kill leftover instances (`taskkill //F //IM PPSSPPHeadless.exe`) before building - a running one makes the link
  step fail with `LNK1168: cannot open ... for writing`, which looks like a build problem and isn't.
- Don't wrap a script that starts headless in `timeout` - when it fires it takes the emulator down with it, and if
  the emulator was stopped at the crash you were investigating, that state is gone. Let the launcher exit and leave
  the process running; wsdbg can reconnect to the same port as many times as you like.
- Response field names are not uniform: `memory.read_u32` answers with `value`, while `cpu.getReg` answers with
  `uintValue`. A parser defaulting a missing key to 0 will quietly report zeroes - read the handler's comment in
  `Core/Debugger/WebSocket/*Subscriber.cpp` rather than guessing.
- `broadcast.config.set` accepts all five categories now (`logger`, `input`, `game`, `stepping`, `breakpoint`);
  it used to reject `game` and `stepping` until each had happened to fire once.
- **A script has to keep the connection open long enough for what it asked for to happen.** `cpu.runUntilTime`
  followed immediately by `:quit` disconnects before the run even starts, and it looks exactly like the feature
  not working. End with a `:wait cpu.stepping <seconds>`.
- **Exception and crash messages do not reach the log in headless.** It registers its own debug-output listener
  (`SendDebugOutput` in `headless/Headless.cpp`) that `fwrite`s to stdout, which is block-buffered when you
  redirect it to a file - so the output sits in the CRT buffer while the process runs, and `taskkill //F` throws
  it away rather than flushing. To actually read a crash trace, give that run a short `--timeout-wall` and `wait` for
  the process to exit on its own.
- **`0xFFFFFFFF` is not an invalid instruction** - it decodes to `vflush`, a real Allegrex VFPU op, so writing it
  over code to test illegal-instruction handling just runs it. Check what an encoding actually is with
  `memory.disasm` before assuming it's garbage; the interpreter raises `ExecExceptionType::ILLEGAL` only when
  `MIPSGetInstruction` has no interpreter for it (`tge`/`tlt`/`teq` and friends).
- **To line input injection up with a wall-clock repro, use `cpu.status`'s `us` field** (emulated microseconds), not
  `ticks`. The PSP's clock frequency is changeable and games do change it - CrossCraft Classic runs at 333MHz, so
  `ticks / 222000000` is off by a factor of 1.5. `clockHz` is reported alongside.
- **`--graphics=software` cannot see "the screen stops updating" bugs.** When HLE writes pixels straight into a
  display buffer - video decoding, `sceJpeg`, `sceMpegAvcCsc` - the hardware backends only find out because the HLE
  calls `gpu->PerformWriteFormattedFromMemory()`. The software renderer reads that memory directly and so renders
  the frame whether or not anything was notified. A missing notification therefore looks perfect under
  `--graphics=software` and shows up as a frozen screen on every real backend, while the decode logs keep scrolling
  past as if all were well. Reproduce display bugs on a hardware backend (see "Choosing a GPU backend" below;
  `directx9` is not a valid value) and compare `--screenshot-save=` output, not the log.
- **`wsdbg --launch` only works with the headless build.** The app build is a GUI-subsystem exe with no stdout, so
  the `Listening on port N` line never reaches the launcher and it gives up. Start it yourself with an explicit
  `--debugger=PORT` and point wsdbg at that port. Note also that `--debugger-run` is `CmdLineMode::Headless`; the
  app takes plain `--debugger=PORT`.
- **`--launch` needs `--log` to find the port.** Without it, headless prints only emulated printfs, so the
  `Listening on port N` NOTICE never appears and `--launch` fails with "PPSSPP never reported a debugger port".
  `--log --loglevel=3` keeps the port line while cutting the debug flood - full `--log` at the default level costs
  a lot of emulation speed (a 25-second run of a game playing video wrote 450k lines and ran several times slower
  than real time, which on its own looks like the stall you're hunting).

## Measuring a commercial game with headless

Headless will happily run a game and print nothing, or run *a different configuration than the one you asked
for*, and both look like a clean pass if you are counting log lines. Four traps, each of which silently
produced a round of bogus results here:

- **`--log` is required for any log output at all**, and the log goes to **stderr**. Without it you get only
  headless's own lines (`Loaded State`, `TIMEOUT`); `-d`/`-v` set the level but don't turn the printf logger
  on. So folding stderr in isn't optional if you're grepping, which is what makes the third trap bite.
- **Headless's memory stick is not the app's.** It defaults to `<exe dir>/memstick` (`headless/Headless.cpp`),
  so `Windows/<platform>/<config>/memstick`, while the app derives its own from `installed.txt` or Documents
  (`InitMemstickDirectory` in `Windows/main.cpp`, which carries a TODO about sharing the derivation). Headless
  *creates* that directory, empty `PSP/NAND/flash0` and all, so firmware installed through the app is invisible
  and every LLE module quietly falls back to HLE - at which point you are measuring the HLE you were trying to
  compare against. Pass `--memstick=` explicitly; it resolves relative to the CWD, not the exe.
- **Check the exit code.** An unrecognised parameter prints `Error: ...` to stderr and exits 1, which is
  correct and easy to throw away: fold stderr into stdout (which the first trap forces), count grep hits, and a
  run that never started reports the same all-zero line as a clean one.
- **Zero is not a pass.** Measuring by log-grepping needs a positive precondition asserted separately ("did
  this run reach the code at all"), or "0 errors" also means "0 anything" - which is equally what a failed
  boot, a savestate that never reaches the cutscene, and a silent HLE fallback produce.

The last three compound: the fix is to treat the run's exit code and a positive "we got here" counter as
preconditions, and only then believe the error counts.

Rather than silently falling back to HLE, headless refuses the run: an explicit
`--disable-hle=` whose firmware module isn't there names the module, prints the `flash0:/kd` and memory stick
it looked in (usually enough to spot that it's the one beside the exe), and exits 1. Only an *explicit*
`--disable-hle` binds - sceMpeg and sceMp4 are LLE by default and still fall back quietly, or every run on a
machine with no firmware would fail. So when a measurement depends on the real module, pass the flag
explicitly even though it's on by default, and the run will tell you if it didn't get it.

`--force-hle=` is the other direction, and the one to reach for when deciding whether something is
our fault: it puts our HLE back for libraries that now run the real module by default, so the same
repro can be run both ways and the logs diffed. That is how the leftover warnings in Tekken 6 were
sorted - three appeared identically with `--force-hle=16`, which made them the game's own, and the
fourth only under the real module, which made it ours. Neither `--nand=` pointing somewhere empty
nor `--appendconfig` does this job: the firmware gets found anyway and the setting is per-game.

### Choosing a GPU backend

- **`--graphics=software`** (the default) needs no GPU at all, and matches the GPU pspautotests' reference
  screenshots (taken on a PSP) best. It's slow for games, though: it runs display lists synchronously inside
  `sceGeListEnQueue`, which then dominates any profile of the emulator thread.
- **`--graphics=vulkan`** renders offscreen, into images of its own with no window or swapchain, so it needs no
  display. On macOS it goes through MoltenVK.
- **`--graphics=opengl`** renders offscreen on macOS (a CGL context with a framebuffer object of its own), and
  through a hidden SDL window elsewhere.
- **`--graphics=d3d11`** (Windows) renders through a hidden window.

For game runs, prefer a hardware backend: 30 emulated seconds of God of War take 3-4 seconds instead of a minute.
The pspautotests pass on Vulkan and OpenGL except for 17 GPU tests, whose references are hardware screenshots
that the hardware backends don't match exactly.

To smoke-test a CPU or GPU change on real games, run the old and the new binary on the same games with
`--timeout-emulated` and `--screenshot-save=`, and compare the screenshots. Most games are deterministic from
run to run, so a different screenshot means different behaviour. God of War: Chains of Olympus isn't: its menu
animation follows wall-clock time, so even the same binary lands on different frames.

To check that a game keeps running after `--state=` loads, pass `--frame-stats`: at the end of the run it prints
the flips in each emulated second since the load. A stuck or crashed game shows as zeros or a short list, and a
steady game as a flat 30 or 60. The state's load result is printed too, and a failed load exits 1.

To get past menus, `--press=SECONDS:MASK[:DURATION],...` holds buttons at emulated times (`MASK` is a sceCtrl
mask, `0x8` START and `0x4000` CROSS, held 0.15 seconds by default), and `--screenshot-at=SECONDS:FILE,...` saves
screenshots along the way.

For audio, `--dump-audio=FILE.wav` records the mixed output (before the game volume setting) for the whole run.
An audio glitch you can hear usually shows in the waveform: a click is a jump between neighbouring samples, and
a repeated or dropped mixer block is two identical 64-sample runs in a row.

Pass `--graphics` explicitly even when you want the default, so a copied command line doesn't depend on it. If a
run goes silent with no CPU use, a host thread is blocked, and neither `--timeout-wall` nor `--timeout-emulated`
will end it, as both are only checked when the emulation loop comes around.

### Measuring speed

`Tools/headless_bench.py` compares the speed of headless binaries on savestates or frame dumps. It alternates
the binaries from round to round and reports medians of four numbers per binary:

- **Wall time** is what a player sees, but it swings by several percent between identical runs (heat, the
  scheduler moving threads between performance and efficiency cores).
- **CPU time** (all threads) says how much work was done. A change that keeps it but cuts wall time spreads
  the work better, and one that cuts it while wall time stays has left the threads waiting.
- **Instructions retired** vary by well under 0.1% between identical runs, so they show changes too small for
  wall time.
- **Cycles** also count stalls (a division, a missed cache line) that instructions don't.

The last two come from `/usr/bin/time -l` on macOS and `perf stat` on Linux. The tool checks for mains power
first: on battery (or in macOS low power mode) the clock speed varies and the numbers aren't comparable. Stop
anything else that uses the CPU, including a forgotten `usbhostfs_pc`, which can spin a core indefinitely. A run
that exits with an error, misses `TIMEOUT (emulated)` or doesn't load its savestate stops the tool rather than
passing as a fast run.

To measure the CPU emulation alone, use `--graphics=null` (headless only, `GPU/Null/GPU_Null.cpp`). It runs
display lists, bounding boxes, matrix loads and block transfers, moves the vertex and index pointers past
each draw, and charges the same GE time per draw, but draws nothing and creates no draw context. Games flip
the same frames as with Vulkan, in roughly half the host instructions. Nothing is rendered, so screenshots
and anything that reads a framebuffer back through the GPU (rather than from PSP memory) get nothing.

Headless decodes vertices with the JIT like the app, where the host can (`--vertex-jit=false` for the C++
decoder, which the tests don't need either).

### Profiling the CPU cores

`--jit-profile=FILE` (app and headless) samples the emulator thread's host PC every 50 µs and, when the JIT
goes away or clears its cache, appends a report to FILE:

- the share of samples in JIT blocks, in the fixed routines they call (like the dot product routines), in
  the dispatcher, and outside JIT code by host function (with symbols where `dladdr` finds them);
- the 30 hottest blocks, each with its MIPS code, its IR, and its host code with the share of samples per
  host instruction, the IR instruction each comes from, and the samples in routines charged to the call;
- the hot fixed-code routines, per instruction.

It costs nothing when off. The native JITs (macOS, Linux and Android by signal, Windows by
`SuspendThread`; RISC-V and LoongArch on Linux only) sample the host PC; the IR interpreter instead times every block, which used to be the
`IR_PROFILING` define. Short runs from a savestate include compiling everything after the load, which
shows as emitter and IR pass functions; profile 20 emulated seconds or more to see the game itself.
To compare two builds by where the time goes, profile one game at a time: several at once wait on each
other's GPU work, which moves the shares.

## Debugging and breakpoint considerations

It might be worth trying the interpreter - all types of breakpoints are the most reliable with this CPU backend.
The JITs are much, much faster and in theory also support breakpoints, and we're trying to make the JITs
as reliable, but are maybe not quite there.

Concretely, as measured against the headless build (2026-08-16), per CPU backend:

| | interpreter | JIT / IR JIT |
|---|---|---|
| `cpu.breakpoint.*` (exec) | works | works |
| `cpu.stepInto/Over/Out`, `runUntil`, `nextHLE` | works | works |
| `memory.breakpoint.*` (memchecks) | works | **only for constant addresses** |
| `cpu.regBreakpoint.*` | works | never trips (as documented) |

## Native debuggers and the memory fault handler

With fast memory on, a bad guest access from JIT code is a real host SIGSEGV/SIGBUS (an access violation on
Windows), which `Memory::HandleFault` (`Core/MemFault.cpp`) catches and turns into a clean emulator-side
exception. A native debugger sees the fault first and stops there, so a run that would have reported
`Read Word: SIGSEGV at ...` looks like a crash instead. Continuing hands the fault to the handler, like a
first-chance exception in Visual Studio. To keep the debugger out of the way when the faults are expected:

- **lldb (macOS/iOS)** needs both of these, verified 2026-09-28 against `pspautotests/tests/cpu/crash`. The
  first skips the stop on the Mach-level EXC_BAD_ACCESS, the second the stop on the signal it turns into:

  ```
  settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_ACCESS
  process launch --stop-at-entry
  process handle SIGSEGV SIGBUS -s false -n false -p true
  continue
  ```

  (`process handle` needs a live process, hence the `--stop-at-entry`.) The catch is that a genuine host crash
  then kills the process without stopping in the debugger.
- **gdb (Linux/Android)**: `handle SIGSEGV nostop noprint pass`.

The `pspautotests/tests/cpu/crash/crash_*.prx` binaries each make one bad access (read, write, float, bad jump),
which makes them the quickest check that the handler works on a platform: run one with `--cpu=jit` in headless. They
should print the guest exception and exit 0, not die with signal 11.

## Debugging a game that works on hardware but not in PPSSPP

First, turn on `bAutoSaveLoadSymbols` (`--auto-save-load-symbols` in headless): when homebrew ships its
unstripped ELF next to the EBOOT (`app.elf` alongside `app.prx`, common for Zig/Rust/SDK homebrew), PPSSPP loads
the function and data names out of it, so the disassembly reads `world.init_empty` instead of `z_un_088c00f0`.
prxgen strips the symbol table on the way to the PRX, which is why the loaded module has none of its own.

The same flag also loads DWARF line info from that ELF (`Core/Debugger/LineInfo.h`), so addresses turn into
`mesh.zig:163` in backtraces, crash traces, breakpoint hits and log lines, both call stack views, and the
disassembly status bar. Availability is narrow and worth knowing before relying on it: **PRX conversion strips
every `.debug` section**, verified across all 437 pspautotests `.prx` and CrossCraft's own `app.prx`, and of 24
installed homebrew EBOOTs *none* carry debug info - CrossCraft only does because it ships `app.elf` separately.
So it's there for homebrew you're developing (or a plain `.elf` you built), never for a commercial game. DWARF 2
through 4 are decoded (psp-gcc emits 2, Zig 4); v5 re-encoded the file table and its units are skipped with a
warning rather than mis-parsed.

That same file is also a ground-truth oracle for anything the loader computes. It still has the symbol table (so
an address can be turned into a
function name) and the full `.rel.*` sections *with symbol indices*, which the PRX format throws away. That makes it
possible to check the emulator's work exhaustively offline - for the HI16/LO16 relocation bug, "does the address
this pairing produces land inside the section its symbol belongs to" turned a guess into a measurement over 8589
relocations, and immediately showed that the first fix attempt scored worse than the code it replaced.

Reach for that before trying to reason a fix out of a disassembly. A few dozen lines of Python over the ELF beats
re-running the game.

## Finding a game function to replace by hash

The tell is a game that's right on the software renderer and stale on the hardware backends, usually a video
that sticks on its first frame while the audio plays on. The game is writing pixels into VRAM with the CPU, and
the hardware backends only see that when it goes through something we hook. Games ship their own `memcpy`,
`memset` and so on, and we recognise known ones by a hash of their code: the `hardcodedHashes` table in
`Core/MIPS/MIPSAnalyst.cpp` names them, and `Core/HLE/ReplaceTables.cpp` swaps in a replacement
(`Replace_memcpy` hands VRAM copies to `gpu->PerformMemoryCopy`). SSX On Tour's videos were fixed this way.

Finding the function:

1. **Get to the right moment.** Confirm the thing is actually happening before breaking on it - SSX's
   videos only start 11.5s in, and a screenshot from earlier just shows the paper background. `memory.read` of
   the decode buffer at a few `cpu.runUntilTime` points tells you when it starts changing.
2. **Break on the write**, under the interpreter (`--cpu=interpreter`), since JIT memory breakpoints only work for constant
   addresses: `memory.breakpoint.add address=<a pixel mid-framebuffer> size=4 write=true enabled=true`, then
   `cpu.resume`. Pick a pixel in the middle of the frame, not the first one, which clears also hit.
3. **`hle.backtrace`** gives the function's entry address. Addresses come back
   as decimal in JSON; convert carefully (`145311076` is `08a94564`, not `08ab1564`).
4. **`memory.disasm` the whole function** and check that it really is what you're going to call it: argument
   order (`a0` dest, `a1` src, `a2` size for memcpy), and no side effects the replacement wouldn't have. A
   replacement that doesn't match the original is a much worse bug than the one it fixes.

Steps 2 and 3 as one script (the SSX run, so 12s in and a pixel at 240,136 of each 8888 stride-512
framebuffer). Parameters take hex as well as decimal; replies are always decimal:

```
cpu.runUntilTime us=12000000
memory.breakpoint.add address=0x040443c0 size=4 write=true enabled=true
memory.breakpoint.add address=0x040cc3c0 size=4 write=true enabled=true
cpu.resume
hle.backtrace
:quit
```

```bash
Tools/wsdbg/target/release/wsdbg.exe --sync --compact --quiet --launch \
    Windows/x64/Release/PPSSPPHeadless.exe GAME.iso --memstick=memstick --cpu=interpreter --graphics=vulkan \
    --timeout-wall=240 < script.txt 2>/dev/null | grep -v '^<- log'
```

The first `hle.backtrace` frame is the writer: `entry` is the function's start, `code` the store that hit.
Follow up with `memory.disasm address=<entry> count=64` and `hle.func.hash address=<entry>` in the same
session. Once a replacement is in place, the same script shows it working: the hit's `source` reads
`GPUMemcpy/...`, its `pc` is the function's entry, and the backtrace starts at the caller.

Getting the hash: `hle.func.hash address=<anywhere in the function>` returns what the table entry needs:

```
<- hle.func.hash {"address":145310976,"hash":"3966eef670a93781","known":null,"size":188}
```

`known` names the entry that already matches, if any (it read `memcpy` once SSX's was added). The hash is the
one `HashFunctions()` in `MIPSAnalyst.cpp` takes when the code is scanned (CityHash64 over the
instructions with immediates masked out), so it doesn't depend on where the function was loaded.

Add the entry to `hardcodedHashes` in hash order, with a comment naming the game. Then check that it took
effect: with `--log --loglevel=4` the loader prints `Replaced memcpy at 08a94500 with hash 3966eef670a93781`.
Check other regions of the game too. Since the hash doesn't depend on address, they usually match the same
entry (SSX's Korean release has the function at `08a94200`).

---

### Dnas Patches (`docs/dnas-patches.md`)

# Game patches installed on a PSP (DNAS)

Some early games shipped their updates and DLC as an "installer" package that the PSP's install
utility turns into a module only that PSP can load. Killzone: Liberation's Chapter 5 DLC is the known
case (issues #12343, #12344). PPSSPP installs the EU Killzone download itself when the game boots,
and can also load a patch that was installed on a real PSP, given that PSP's Fuse ID.

## What the download is

`PSP/GAME/UCES00279/` (UCUS98646 for the US game) gets two files:

- `EBOOT.PBP`, 8.5 MB, `CATEGORY=MG`, `BOOTABLE=0`, title "Killzone: Liberation Installer". Its
  `DATA.PSP` is the encrypted package the install utility unpacks. The XMB refuses to start it.
- `CHAPTER5.BIN`, 68 MB, the chapter's data. The patched game module reads it as it is.

## Installing it on a PSP

The install is done by the game's own boot program (`disc0:/PSP_GAME/SYSDIR/EBOOT.BIN`), which
imports `sceUtilityInstall*`. It only installs when started with the argument `-i`, and then only if
`ms0:/PSP/GAME/UCES00279/EBOOT.PBP` is larger than 1 MB. The game passes `-i` itself, after the
online download in its menus (from `kzl.killzone.com`, long gone), so there's no way to trigger it
from inside the game today.

Starting the boot program with `-i` from PSPLink works. `modexec` doesn't (the LoadExec is refused),
but a tiny kernel-mode PRX calling the CFW's `sctrlKernelLoadExecVSHDisc` (`SystemCtrlForKernel`,
NID `0x577AF198`) with `key = "game"` and the argument block `"disc0:/PSP_GAME/SYSDIR/EBOOT.BIN\0-i\0"`
brings up the system's install screen. Afterwards the folder holds `KZL.PRX` (8.6 MB, the patch),
and the 8.5 MB `EBOOT.PBP` has been replaced by a 10 KB one for the XMB.

The Fuse ID is the two words at `0xBC100090` and `0xBC100094`, which only kernel code can read.
PSP tools show it as 12 hex digits, the `0x94` word first.

## Installing it in PPSSPP

`InstallDrmBBPackage()` in `Core/PSPLoaders.cpp` runs when one of the games it lists boots (those
`g_kirk2Results` covers) and `PSP/GAME/<DISC_ID>/EBOOT.PBP` is over 1 MB. It does what psheet does, with no console in the
picture: it writes the package's files next to it and replaces the installer with the package's own
`EBOOT.PBP`. It only does this if it can decrypt every `.PRX` in the package, so otherwise the download is
left alone.

The package is the PBP's `DATA.PSP`, read by `pspReadDrmBBPackage()` (layout from psheet's
`bbox_access.c` functions). Everything in it uses sceDrmBB MAC and cipher type 1, so known keys only:

- The whole PBP is MACed with a fixed key from psheet's data segment, in its last 16 bytes.
- `DATA.PSP` +0x00: a 0x50-byte header MACed (at +0x50) with the secure install ID, which means the
  ID can be recovered from the MAC (`bbmac_getkey`). The EU and US downloads use the same one. Its
  bytes 0x10-0x50 decrypt with the fixed key (header key: bytes 0-0x10, seed 0) to two 16-byte keys,
  then version 1, flags, 1, 0, the parameter table's offset and count, and the file table's offset.
- Parameters are (id, value) pairs, decrypted with the first key and the secure install ID. `0x01010100`
  is the number of files.
- File entries are 16 bytes (u16 name offset, u16 name length, u32 data offset, u32 size, relative
  to the file table), and their names and data are decrypted with the second key and the secure install ID.
  The seed is always the 16-byte block number.

A `.PRX` comes out as a type 3 module whose first step (the 0x50 bytes at 0x80-0xB0, 0xC0-0xD0 and
0x12C-0x13C, with the KIRK header at 0xEC) is encrypted for KIRK command 2. The install sends the
first 0x400 bytes through `sceMesgIns_driver_D062B635` (uofw's `sceUtilsPrepareGetLoadModuleRLength`),
which runs command 2 in place and re-encrypts that block for the console. The rest of the module is
the same before and after. Command 2's key is unknown, but its result is the same on every PSP, so
`g_kirk2Results` in `PrxDecrypter.cpp` stores it for each module we know, keyed by the 16 bytes at
0xEC. `pspDecryptType3()` uses it in place of the first step.

To get the result for another package, run `--re-decrypt` on it, which then writes the module as
the package has it to `<out>.module`. Then either decrypt a PSP
install of it with KIRK command 3 and that PSP's Fuse ID, or run `sceMesgIns_driver_D062B635` on its
first 0x400 bytes on a PSP and decrypt that output with command 3 there.

## Loading it

The boot program calls `sceKernelLoadModuleDNAS(path, secureId, 0, 0)` with a 16-byte secure
install ID from its data (`81A19A6F0FA9CFBAE378F73FFD504C7F` for UCES00279), and treats any negative
result as "no patch", falling back to `disc0:/PSP_GAME/USRDIR/KZL.PRX`. Two layers:

1. **A PGD** (key index 1, DRM type 0), opened by `amctrl` with the secure install ID and the "flag 1"
   DNAS key. Its MAC and cipher use KIRK commands 5 and 8, which are AES under the per-console key
   with seed 1.
2. **A type 3 PRX**, decrypt mode 18, tag `0x89742B04`. This is type 5 with the first step done by
   KIRK command 3 instead of 7: a small command 3 block whose header is at `0xEC` and whose 0x50-byte
   body is the usual kirk header + SHA1 pieces, XORed with the tag's key and the secure install ID.
   The install produced that block with KIRK command 2, re-encrypting under the per-console key with
   seed 0. Command 2's own key (AES keyslot 3) isn't known, which is why the download can't be
   installed here.

The per-console keys come from the Fuse ID: the key mesh on psdevwiki's Kirk page, and the final
step from the KIRK ROM's `aes_set_perconsole_key` (uofw `src/kirk/kirk-rom.c`). The type 3 layout is
JPCSP's `crypto/PRX.java`. The tag's key and XOR are in its `KeyVault.java`, and uofw's `mesgled.h`
documents the decrypt mode.

In PPSSPP:

- `kirk_individual_key()`, `kirk_CMD3/5/8` in `ext/libkirk/kirk_engine.c`.
- `pspDecryptType3()` and `pspUnwrapDNASPGD()` in `Core/ELF/PrxDecrypter.cpp`.
- `sceKernelLoadModuleDNAS()` in `Core/HLE/sceKernelModule.cpp` takes the Fuse ID from the `FuseID`
  setting (`[SystemParam]` in `ppsspp.ini`) or `--fuse-id`.

A wrong Fuse ID fails the PGD's MAC, so the game just runs unpatched. A module PPSSPP installed has
no PGD and needs no Fuse ID.

To decrypt one offline:

```
PPSSPPHeadless --re-decrypt KZL.PRX --re-decrypt-out KZL.elf \
    --re-decrypt-key 81a19a6f0fa9cfbae378f73ffd504c7f --fuse-id <12 hex digits>
```

`--re-decrypt` also takes the download's `EBOOT.PBP`, and decrypts the `.PRX` in it.

---

### Frametest (`docs/frametest.md`)

# Framedump rendering tests (frametests)

PPSSPP has a test system that replays GE frame dumps (`.ppdmp`) through
`PPSSPPHeadless` with various rendering settings, comparing the rendered
output against stored reference images. It is designed to catch rendering
regressions across GPU backends and configurations.

The system has three parts:

- **The test runner**: `frametests.py` (repo root). Test-set agnostic - it
  works against any test data tree pointed to by its configuration.
- **The configuration**: a JSON file describing where the test data lives and
  which rendering variants to run (e.g. `frametests/frametests.json`).
- **The test set**: the frame dumps and reference images, maintained as the
  `frametests` git submodule (repo `hrydgard/ppsspp-frametest-ci`, pinned by
  commit - its config `frametests.json` lives inside it). Different
  environments can use different test sets - GitHub CI uses the submodule,
  custom machines can use their own (possibly much larger) ones.

## Quick start

```bash
# Build PPSSPPHeadless (see headless/README.md), then:
python3 frametests.py frametests/frametests.json
```

The first run generates reference images for any dumps that don't have them
yet (status `NEW`) and compares the rest (status `PASS`/`FAIL`). A summary is
printed and a self-contained HTML report is written to
`frametests/out/report.html` (default output dir; gitignored).

Exit code is `0` if all tests passed, `1` if anything failed, `2` on
configuration/usage errors.

## Configuration

The configuration is a single JSON file. All relative paths are resolved
against the config file's directory, so the same script works with any test
set by just pointing `--config` at a different file. The config lives with
the test set it describes (e.g. `frametests/frametests.json`) and is not
committed to the repository.

```json
{
	"testRoot": "dumps",
	"refRoot": "ref",
	"outputRoot": "out",
	"headlessPath": "",
	"timeout": 60,
	"maxMse": 0.0,
	"variants": {
		"soft": {
			"args": "--graphics=software"
		}
	}
}
```

| Key             | Description                                                                 |
|-----------------|-----------------------------------------------------------------------------|
| `testRoot`      | Directory tree containing frame dumps (`.ppdmp` files, possibly wrapped in `.zip`). |
| `refRoot`       | Where reference images live; mirrors the `testRoot` tree.                   |
| `outputRoot`    | Where logs, diff images, the report, and generated references go (default `out`). |
| `headlessPath`  | Path to the `PPSSPPHeadless` binary. Empty = auto-detect (see below).        |
| `timeout`       | Per-test timeout in seconds.                                                 |
| `maxMse`        | Maximum allowed MSE for screenshot comparison (0 = exact match).            |
| `variants`      | Map of variant name to variant configuration (see below).                   |

### Variants

Each test runs once per variant. A variant has a **suffix** and a
**compare-suffix**:

- The **suffix** (default: the variant name) is appended to the variant's own
  output image filenames (`actuals`, `diffs`, `logs`).
- The **compare-suffix** (default: the suffix) names the reference image the
  variant compares against: `<name>-<compare-suffix>.png`.
- **Reference images are only generated for variants whose suffix matches
  their compare-suffix.** This lets several variants share a single reference
  set - e.g. OpenGL and Vulkan outputs should be nearly bitwise identical, so
  a `gl` variant can compare against the `soft` references:

```json
"variants": {
	"soft": { "args": "--graphics=software" },
	"gl":   { "args": "--graphics=opengl", "compare-suffix": "soft" },
	"vul":  { "args": "--graphics=vulkan", "compare-suffix": "soft" }
}
```

A plain string value (`"soft": "--graphics=software"`) is shorthand for
`{ "args": "...", "suffix": "soft", "compare-suffix": "soft" }`.

## How a test runs

For each dump (recursively under `testRoot`) and each variant:

- If the variant's reference image `<name>-<compare-suffix>.png` is
  **missing** and the variant generates references (suffix ==
  compare-suffix): the dump is rendered and the output saved as the new
  reference. Status: `NEW`. This is how references are created - run locally,
  then commit the generated images (also copied to `<outputRoot>/generated/`
  for convenience).
- If the reference is **missing** for a variant that doesn't generate
  references (suffix != compare-suffix): Status: `ERROR` - the shared
  reference needs to be generated by the matching variant first.
- If the reference **exists**: the dump is rendered, the output saved to
  `<outputRoot>/actuals/`, and compared against the reference using MSE
  (mean squared error over R, G, B per pixel, alpha ignored). A visual
  comparison image (actual / reference+diff) is saved to
  `<outputRoot>/diffs/` whenever a comparison runs. Status: `PASS` or `FAIL`
  (mismatch, crash, or timeout).

A `FAIL` with no MSE reported usually means the headless binary crashed
before producing a screenshot, and a reference image that can't be loaded
(corrupt) is reported as `ERROR`. The full emulator log of every non-passing
test is kept in `<outputRoot>/logs/`.

### Reference images

- PNG, 512×272 (480×272 display in a 512-wide framebuffer), stored top-down
  (row 0 = top of screen). The BMP output format is bottom-up per the BMP
  spec; the flip is applied only when writing BMPs.
- The alpha channel is forced to 255 when writing PNGs (games often use alpha
  for non-visual purposes, which would otherwise produce transparent-looking
  images); pass `--screenshot-keep-alpha` to the headless binary to preserve
  it (e.g. via a variant). The MSE comparison ignores alpha either way.
- Generated with `--graphics=software` they are fully deterministic: a
  subsequent run produces byte-identical output, so `maxMse` can be 0.
- If rendering code changes the output, existing references may need
  regenerating: delete the affected reference images and re-run to regenerate
  them.

### Headless flags used

The runner uses `--screenshot=<ref>` (compare), `--screenshot-save=<file>`
(save output; PNG if the path ends in `.png`, else BMP) and
`--screenshot-diff=<file>` (always write a visual comparison when comparing).
See `headless/README.md` for details.

`--depth-save=<file>` writes the current depth buffer next to the screenshot: a
u32 width and height, then a u16 per pixel, or a float if the path ends in
`.f32`. The software renderer gives the GE's own values. The hardware renderers
keep depth at higher precision without the GE's rounding, so their u16 is
`floor(d * 65536)` and usually within one of the software renderer's; the
`.f32` version keeps the fraction, which shows whether two draws of the same
surface meet a depth test like `>=`.

## Command line options

```
usage: frametests.py [OPTIONS] [CONFIG.json]
```

| Option             | Description                                                           |
|--------------------|-----------------------------------------------------------------------|
| `CONFIG.json`      | Path to the configuration file (default: `frametests/frametests.json`). |
| `--filter=SUBSTR`  | Only run dumps whose relative path contains SUBSTR (case-insensitive). |
| `--strict`         | Treat missing reference images as failures (a configuration error).    |
| `--out-mode=all\|failures` | `all` keeps everything in the output dir; `failures` keeps only artifacts of non-passing tests (smaller CI artifacts). Default: `all`. |
| `--variants=NAME=ARGS,...` | Run these variants instead of the config's. |
| `--adb[=SERIAL]`   | Run on an Android device instead; see "Running on Android" below. |
| `--ios[=DEVICE]`   | Run on an iPhone or iPad instead; see "Running on iOS" below. |

The headless binary is located, in order of preference:

1. `headlessPath` from the config file.
2. The `PPSSPP_HEADLESS` environment variable.
3. Well-known paths relative to the current directory (e.g.
   `Windows/x64/Debug/PPSSPPHeadless.exe`, `build/PPSSPPHeadless`), newest
   by modification time.

## Running on Android

`--adb` runs every dump on an Android device over adb, with the `ppsspp_headless` executable from the
legacy NDK build, and compares each screenshot with the host's Vulkan render of the same dump (through
`Tools/image_compare.py`, so rasterizer noise doesn't count). The references in the config aren't used:
the question is whether the device's drivers draw what a desktop GPU does.

```bash
cd android && NDK=/path/to/ndk ./ab.sh APP_ABI=arm64-v8a HEADLESS=1 SKIPAPP=1 -j8 && cd ..
python3 frametests.py frametests/frametests.json --adb
```

The first run pushes the binary, `assets/` and the dumps to `/data/local/tmp/ppsspp-frametests`; later runs
only push what changed. The variants default to `vulkan=--graphics=vulkan --dump-compat,gl=--graphics=gles
--dump-compat`, and `--host-args` (default `--graphics=vulkan --dump-compat`) sets how the host renders its
side. `--dump-compat` applies the game's `compat.ini` settings, which headless otherwise skips for dumps;
without them some games look wrong on every backend (Coded Arms needs `AccurateSplines`). A failing test's diff image shows host, device and the
differences side by side. Runs go to the device one at a time.

## Running on iOS

`--ios` does the same on an iPhone or iPad, comparing against the host's Vulkan render the same way.
An iOS device only runs apps, so PPSSPPHeadless is built as one there (`org.ppsspp.headless`,
`ios/HeadlessMain.mm`): it starts UIKit, runs the usual headless `main` on a thread with the arguments
`devicectl` launched it with, and exits. The screen stays black meanwhile, since it renders offscreen.
It needs a development-signed build, which is what lets `devicectl` copy files in and out of it.

```bash
cmake -S . -B build-ios-headless -GXcode -DCMAKE_TOOLCHAIN_FILE=cmake/Toolchains/ios.cmake \
    -DIOS_APP_STORE=ON -DDEVELOPMENT_TEAM_ID=<team> -DHEADLESS=ON
xcodebuild -project build-ios-headless/PPSSPP.xcodeproj -scheme PPSSPPHeadless -configuration Release \
    -destination 'generic/platform=iOS' -allowProvisioningUpdates build
python3 frametests.py frametests/frametests.json --ios
```

Keep the device unlocked while it runs. frametests.py installs the app, copies the dumps the device doesn't
have yet into its `Documents/dumps`, and renders each variant's dumps in launches of up to 100 (each
`devicectl` call costs a second or two, so one launch per dump would be slow). A dump that crashes the app
ends its launch; the rest go into the next one. The variants default to `vulkan=--graphics=vulkan
--dump-compat`, as iOS has no OpenGL. There's no JIT without a debugger attached, so the device runs the
IR interpreter and decodes vertices without the vertex JIT.

To run the app by hand:

```bash
xcrun devicectl device copy to --device <udid> --domain-type appDataContainer \
    --domain-identifier org.ppsspp.headless --source dump.ppdmp --destination Documents/dump.ppdmp
xcrun devicectl device process launch --device <udid> --terminate-existing --console org.ppsspp.headless \
    --graphics=vulkan --screenshot-dir=shots dump.ppdmp
```

Relative paths are relative to `Documents`, and `--console` shows its output. `--ios-clear=DIR`
(handled by the app, not headless) deletes a folder under `Documents` first. `devicectl` has no way to
delete files, and **never pass it `--remove-existing-content`**: that empties all of `Documents`, not
just the destination folder.

The `build-ios` folder works too if it's configured with `-DHEADLESS=ON`, as long as Xcode doesn't
have that project open: `xcodebuild` then hangs before building anything, even `xcodebuild -list`.

## CI integration

In CI (`GITHUB_ACTIONS` is set) the runner behaves as if `--strict` was
passed and prints `::error` annotations for each failing test, so failures
show up inline in the GitHub Actions log.

The CI workflow extends the existing `test` job (Linux leg) in
`.github/workflows/build.yml`: the `Fetch tests` step inits the `frametests`
submodule (alongside `pspautotests`), then after the pspautotests run it
executes the frametests and uploads the report as an artifact:

```yaml
- name: Fetch tests
  run: git submodule update --init pspautotests frametests

- name: Execute frametests
  if: runner.os == 'Linux'
  run: python3 frametests.py frametests/frametests.json --out-mode=failures

- name: Upload frametest report
  uses: actions/upload-artifact@v7
  if: runner.os == 'Linux' && always()
  with:
    name: frametest-report
    path: frametests/out/
```

A missing reference image in CI means the test set is incomplete - the run
fails and the generated reference is available in
`frametests/out/generated/` (part of the uploaded artifact) so it
can be committed.

## Custom machines

The script is designed to run on machines with different GPUs and bigger test
sets than CI. Copy the repo, point at a custom config:

```bash
python3 frametests.py /path/to/my-config.json
```

The config can live anywhere and point at any test data tree; only the
script itself is shared. Add hardware-specific variants (`--graphics=vulkan`,
`--msaa=...`, etc.) to the machine's own config - no code changes needed for
new flag combinations, as long as the headless binary supports them.

## Adding a new framedump

1. Drop the dump into `frametests/dumps/` (either as a `.ppdmp` or zipped).
2. Run `python3 frametests.py frametests/frametests.json --filter=<name>` -
   the reference image(s) are generated and copied to `<outputRoot>/generated/`.
3. Commit the dump and the reference images under `frametests/ref/`, and push
   them to the `frametests` submodule repo (`hrydgard/ppsspp-frametest-ci`).
   Then bump the submodule pointer in PPSSPP so CI picks up the new test.

The CI test set (dumps and references) is maintained separately from the
runner; the `frametests/` submodule is just one of several possible test sets
- custom machines can use their own (possibly much larger) ones.

## Comparing renderers

Two backends never match pixel for pixel: edges land a pixel apart, filtering and interpolation round
differently, 16-bit framebuffers expand their colors differently. To find what really differs between,
say, Vulkan and the software renderer (or a PSP capture), render the same dumps with each and compare the
directories with `Tools/image_compare.py`:

```bash
python3 Tools/image_compare.py out_vulkan/ out_soft/ --sort --heatmaps heat/
```

It counts a difference only where a pixel is outside the range of the other image's 3x3 neighborhood
(widened by a tolerance) and enough of its neighbors are too, so shifted edges and speckle drop out, and
separately counts 8x8 blocks whose averages differ, for broad shifts. The heatmaps show both images and the
counted pixels. On the GitHub dump set, Vulkan and OpenGL come out equivalent for 795 of 957 dumps, and
what's left is mostly real (frames one of them renders black, missing effects).

## Replaying a dump on a real PSP

The references above are PPSSPP's own renders, so they catch regressions, not
wrong output. To see what the hardware draws, replay the dump on a PSP:
`pspautotests/utils/ppdmp-playback` is a PSP program that runs a `.ppdmp` natively
(all dump versions PPSSPP writes), and its `run.py` drives it over PSPLink from
the host:

```bash
cd pspautotests
python3 utils/ppdmp-playback/run.py --out /tmp/shots \
	--headless ../build/PPSSPPHeadless "../frametests/dumps/Depth/21391 Coded arms ULUS10019_0001.zip"
```

It saves what the PSP displays as `NAME-psp.png`, and with `--headless` renders
the same dump in PPSSPPHeadless (`--graphics=software` by default), saves
`NAME-ppsspp.png` and prints the MSE between the two. Each frame stays on the
PSP's screen for 1.5 seconds (`--hold`). It needs the PSPSDK (it rebuilds the PRX) and a PSP in
PSPLink with `usbhostfs_pc` serving the pspautotests root, the same setup as
[pspautotests-hardware.md](pspautotests-hardware.md); like every hardware run,
one at a time. See its README for replaying a subset of the primitives.

A zip holding several dumps plays its last `.ppdmp`, in PPSSPP and in the replayer, so extract that one
when you need the file itself.

If a replay hangs, `run.py` resets the PSP and prints the last lines the replayer wrote. `--progress=N`
prints a line every N dump commands and `--trace-from=N` every command from N on, and `--cmds=N` replays
only the first N dump commands, which bisects a hang in a few runs. The replayer's Makefile has no header
dependencies: after changing `replay.h`, delete the `.o` files, or a stale object corrupts the `Replay`
object.

To find the draw where the two diverge, stop both at the same primitive:
`run.py --end=N` on the PSP and `--replay-end=N` on PPSSPPHeadless. A draw into
an offscreen buffer can be compared directly: `run.py --display=ADDR,STRIDE,FMT`
shows and screenshots that buffer instead of the display (e.g.
`--display=04000000,512,3` for an 8888 target at the start of VRAM), and
PPSSPPHeadless takes `--screenshot-render-target` to save the current render
target.

PPSSPP's software renderer now matches the PSP bit for bit on nearly every dump
here, so a difference is worth chasing. Hardware backends still differ slightly
(filtering, edge rules), so compare their MSEs between versions instead.

To record a dump from a running game without the UI, use the WebSocket debugger's
`gpu.record.dump` through `Tools/wsdbg`. Its reply comes later, so wait for it:

```bash
printf ':sleep 6\ngpu.record.dump\n:wait gpu.record.dump 60\n:quit\n' | \
	Tools/wsdbg/target/release/wsdbg --sync --compact --quiet --launch \
	build/PPSSPPHeadless game.iso --state=STATE.ppst --graphics=software --debugger-run=0 > out.txt
```

The reply's `uri` is a base64 `data:` URI holding the `.ppdmp`.

---

### Kernel Hle Review (`docs/kernel-hle-review.md`)

# sceKernel HLE review notes

Notes from a review pass over `Core/HLE/sceKernel*.cpp` (~16k lines across 14 files), done
2026-09-03. Kept mostly so the next person doesn't re-derive the same conclusions - the
"verified clean" section is as much the point as the findings.

Cross-referenced against JPCSP (`../JPCSP`) where the correct behaviour wasn't obvious from
the PSP docs.

## Bug classes that turned up

Each of these appeared in more than one place, so they're worth grepping for when touching this code:

**Timeout writeback without a clamp.** `CoreTiming::UnscheduleEvent` returns the scheduled time
minus the current time, which is *negative* when the event is overdue but not yet processed - the
exact situation when a wait is satisfied around its own timeout. Nine of ten sites wrote
`(u32)cyclesToUs(cyclesLeft)` straight into the game's timeout variable. Now funnelled through
`HLEKernel::WriteRemainingTimeout` in `KernelWaitHelpers.h`; the two thread-end sites keep their own
copy because they unschedule even when the game passed no timeout pointer.

**Addresses validated, extents not.** `Memory::IsValidAddress` checks a single byte.
`KernelImportModuleFuncs` used it for `nidData` and `varData` and then indexed those arrays
`numFuncs` (u16) and `numVars` (u8) times; `sysclib_strcpy` used it for a destination it then wrote
`strlen(src) + 1` bytes into. Use `IsValidRange` with the size you're actually going to touch.

**Unbounded walks over game-supplied data.** The module variable relocation list was scanned until
it happened to hit a zero word; the mbx packet list was walked until it came back around to its
head. Both follow only *valid* pointers, so nothing faults - they just run off the end of memory or
spin forever. Bound them by what's mapped (`Memory::ClampValidSizeAt`) or by the count the object
already tracks.

**Overflow checks that don't match the arithmetic they guard.** `sceKernelCreateFpl` and
`sceKernelCreateTlspl` validate `blockSize * count` at 4-byte alignment, but then allocate using an
alignment the caller chooses, so the aligned total can still wrap - allocating a small block while
the block count stays huge, and later handing out `address + block * alignedSize` outside it.

**Functions that return a pointer, returning an error code.** `sceKernelAllocHeapMemory` passed
`BlockAllocator::Alloc`'s `(u32)-1` straight out, and returned UID error codes for a bad heap id.
JPCSP documents the contract as "the address of the allocated memory block, or NULL on error", so
every failure has to be 0 or callers checking for null see success.

## Verified clean - don't re-flag these

- **`readyCallbacksCount` bookkeeping.** There's a self-suspecting `"became negative"` report in
  `__KernelCheckCallbacks`, but the invariant holds. The increment (only on the 0 -> nonzero
  transition in `__KernelNotifyCallback`) is matched by three decrements: running a callback
  (`__KernelRunCallbackOnThread` zeroes `notifyCount` first), `sceKernelDeleteCallback`, and thread
  deletion. The thread-deletion path looks like it should double-decrement with a later
  `sceKernelDeleteCallback`, but `PSPThread::Cleanup()` destroys the callbacks immediately after the
  decrement loop, so the second lookup fails.
- **`u32 error;` uninitialized before `kernelObjects.Get`, then `if (error) return;`.** Safe -
  `Get` writes `outError` on both paths, 0 on success.
- **Kernel object name copies.** Every `strncpy(x.name, name, KERNELOBJECT_MAX_NAME_LENGTH)` writes
  into a `char[KERNELOBJECT_MAX_NAME_LENGTH + 1]` and follows with an explicit NUL. Correct.
- **`strncpy(moduleName, modinfo->name, ARRAY_SIZE(module->nm.name))`** in `sceKernelModule.cpp`
  mixes two buffers' sizes but is safe: 28 into a zero-initialised 29.
- **VPL and FPL allocation failure handling.** Both check for `(u32)-1` properly; the heap was the
  outlier.
- **The unchecked `ReadUnchecked_U32` sites** in `sceKernelThread.cpp` (register restore, extended
  stack restore, the exit-callback parameter area) all have `IsValid4AlignedRange` checks a few
  lines above.
- **`sceKernelCreateHeap`'s `alloc.Init(address + 128, size - 128)`** looks like it underflows for a
  heap smaller than 128 bytes, but can't: `BlockAllocator::AllocAligned` rounds the caller's size
  variable up by reference and every partition allocator is constructed with grain 256.
- **VTimer scheduling arithmetic.** The `base + schedule - current` u64->s64 conversion wraps
  negative on underflow, which the `goalUs < minGoalUs` clamp catches.

## Latent, worth knowing about

`__KernelSendMsgPipe` / `__KernelReceiveMsgPipe` have two shapes of transfer loop. The buffered one
explicitly breaks when a transfer would move zero bytes; the unbuffered ones (`bufSize == 0`) don't -
they only make progress inside `if (bytesToSend > 0)`, so a queued waiting thread with a zero
`freeSize` would spin forever. That can't happen today because every `AddSendWaitingThread` /
`AddReceiveWaitingThread` call site is guarded by a `size != 0` check, but the invariant lives two
call sites away from the loop that depends on it.

## Open, deliberately not changed

- **`__KernelStartThread` doesn't bound `argSize`.** `sp -= (argSize + 0xf) & ~0xf` with `argSize`
  up to `INT_MAX` pushes the stack pointer below the thread's stack. The copy itself goes through
  the validating `Memory::Memcpy`, so this is guest-side corruption only, and real hardware probably
  doesn't validate it either - changing it risks breaking games that depend on that. The
  `argSize + 0xf` signed overflow is UB regardless and deserves a cast.
- **Three unchecked `__GetCurrentThread()` dereferences**: `__KernelCurHasReadyCallbacks`,
  `__KernelWaitCallbacksCurThread`, `KernelRotateThreadReadyQueue`. Of 40 uses in that file the rest
  check, assert, or use a ternary - and `__KernelWaitCurThread`, the direct sibling of the second
  one, has `_assert_(thread != nullptr)`. No path was found where a syscall runs without a current
  thread, so these were left alone rather than papering over a real invariant.

## Coverage

Read closely: `Heap`, `Semaphore`, `Alarm` in full; `Module`'s import path; `Interrupt`'s sysclib
block; `Memory`'s pool creation and `PartitionMemoryBlock`; `sceKernel.cpp`'s object pool and
savestate loop; `Thread`'s start/refer paths and the callback machinery; `Mbx`'s message list;
`VTimer`'s scheduling.

Audited across all 14 files by pattern: unchecked memory access, timeout writeback, null checks
after `kernelObjects.Get`, name copies, allocator return values, uninitialised members.

Also checked: `MsgPipe`'s transfer loops, and the interrupt dispatch in `sceKernelInterrupt.cpp`
(`__TriggerInterrupt` indexes `intrHandlers` without a bounds or null check, unlike
`__RunOnePendingInterrupt`, but every caller passes an internal constant).

Not read line by line: the `EventFlag` / `Mutex` wait-queue bodies, and thread scheduling proper.

---

### Metal Backend (`docs/metal-backend.md`)

# The Metal backend

A native Metal backend for Mac and iOS (tvOS too), alongside OpenGL, D3D11 and Vulkan. It's selected as
`GPUBackend::METAL` (`--graphics=metal`). It's the default on iOS since 2026-10-22; Vulkan through MoltenVK
stays the default on Mac and tvOS. Written 2026-10-19; file references will drift.

**Why:** programmable blending. Apple GPUs let a fragment shader read the color it's about to blend with
(`[[color(0)]]`), so PSP blend modes that need shader blending no longer copy the framebuffer for every
draw. MoltenVK can't expose that. Removing the MoltenVK dependency is a smaller benefit, and comes later.

## Where things are

| Path | What |
|---|---|
| `Common/GPU/Metal/MetalRenderManager.h/.mm` | Command recording and replay, push buffers, shader translation, background compiles |
| `Common/GPU/Metal/thin3d_metal.mm` | thin3d's `DrawContext`, for the UI, presentation and the shared GPU code |
| `Common/GPU/Metal/MetalGraphicsContext.h/.mm` | The `GraphicsContext`: device, layer (or an offscreen backbuffer for headless), render thread |
| `GPU/Metal/` | The emulation side: draw engine, shader manager, texture cache, framebuffer manager, `GPU_Metal` |

The backend only exists where `PPSSPP_API(METAL)` is set: Mac and iOS, not the libretro cores. The Objective-C
files are compiled with ARC.

## Shaders

The backend uses the same shaders as Vulkan: the generators emit Vulkan GLSL, glslang compiles it to
SPIR-V, and SPIRV-Cross's `CompilerMSL` turns that into MSL 2.3 (`TranslateVulkanGLSLToMSL`). Generating
MSL directly was considered and dropped. Translation takes about 0.5 to 0.7 ms (glslang) plus 0.2 to 0.4 ms
(SPIRV-Cross) per shader, against around 100 ms for Metal's own compile of the MSL when it isn't in the
system's Metal cache.

- **Bindings:** a Vulkan binding number is used as the Metal buffer, texture and sampler index. The vertex
  buffer is at index 16, and compute shaders' push constants at 15.
- **Coordinates:** `flip_vert_y` makes clip space match Vulkan's, so everything else (viewports,
  framebuffer origin, texture coordinates) matches Vulkan exactly. thin3d reports the Vulkan coordinate
  convention.
- **Framebuffer reads:** the fragment shader generator declares a `subpassInput` for shader blending when
  `FS_BIT_USE_FRAMEBUFFER_FETCH` is set, and SPIRV-Cross (`use_framebuffer_fetch_subpasses`) turns it into
  `[[color(0)]]`. Only Apple GPUs report `framebufferFetchSupported`.
- **Invariance:** functions are compiled with `preserveInvariance`, since multipass effects depend on
  different vertex shaders producing identical positions. It costs no measurable speed.

Shaders are translated when the shader manager creates them, since recording needs the texture masks the
translation produces. The MSL compile and the pipeline state are created on compile threads
(`RunOnCompileThread`). Draws record a `MetalPipelineState` that may still be compiling, and the replay
waits for it, as Vulkan does. Nothing is skipped.

Each game's shader IDs and pipeline keys go to `<discID>.mtlshadercache` (with a checksum, like the Vulkan
and GL caches), and are recreated while the game boots. Cold Wipeout Pure, 5 emulated seconds from a
savestate: 3.0 s with synchronous compiles, 1.6 s with background compiles.

## The render manager

The emulator thread records commands into a chunk, and they're encoded into Metal command buffers on a
render thread (when `bRenderMultiThreading` is on and more than one frame is in flight), or inline at
submit.

- **Lazy passes.** A render pass starts on its first draw, since Metal needs the load actions up front and
  resets all encoder state with each pass. A clear restarts the pass with clear load actions; binding the
  framebuffer that's already bound continues the pass, like Vulkan.
- **Redundant state** is dropped while recording.
- **Pass merging.** The replay merges later passes to the same framebuffer into earlier ones when nothing
  in between reads or overwrites it, by the rules of `VulkanQueueRunner::ApplyRenderPassMerge`. God of War
  went from 41 to 18 render passes a frame.
- **Depth.** A pass where no draw tests or writes depth or stencil neither loads nor stores it. The
  backbuffer's depth is memoryless on Apple GPUs.
- **Uploads** for new textures go in a command buffer of their own, enqueued before the frame's commands.
  Texture scaling's compute dispatches run there too.
- **Lifetimes.** Command buffers don't retain what they use (`commandBufferWithUnretainedReferences`), which
  saves a retain and release per binding. Anything that may still be in use goes through `DeferRelease`,
  which keeps it until the GPU is done with the frame. Frames in flight are limited by a semaphore.
- **Push buffers** keep 256 bytes free after each allocation, since the vertex decoder can write past the
  end of its output.

## Textures and framebuffers

- 16-bit PSP textures (5650, 5551, 4444) stay 16-bit on Apple GPUs, in `B5G6R5Unorm`, `BGR5A1Unorm` and
  `ABGR4Unorm` with a swizzle. Metal names packed channels from the lowest bits up, and the PSP stores red
  lowest.
- At max quality filtering, textures up to 256x256 get a full mip chain, generated with a blit.
- Hardware texture scaling runs the same compute shaders as Vulkan. Shaders with a constant buffer (only the
  smiley test shader) aren't supported.
- Framebuffers are `RGBA8Unorm` with `Depth32Float_Stencil8`. A depth-only or stencil-only copy goes
  through a buffer, since texture copies take both planes.
- MSAA works like the Vulkan backend's: a multisampled framebuffer has multisampled color and depth textures
  that are drawn to and kept between passes, and each pass resolves them into the ordinary ones (depth and
  stencil to sample 0), which everything else reads. Copies between two multisampled framebuffers copy the
  samples too. Metal can't copy a single plane of a multisampled depth/stencil texture, so those copy both.
  With programmable blending, the shader blends each sample with its own color, so edges come out a little
  different from Vulkan's, which blends with the resolved color.
- Readbacks for the GE debugger and screenshots work. Headless has no backbuffer to read, like headless
  Vulkan, and the debugger falls back to the PSP's framebuffer.

## Testing

- `PPSSPPHeadless --graphics=metal` renders offscreen, for frame dumps, savestates and the WebSocket debugger.
- With `MTL_DEBUG_LAYER=1`, Metal validates the API use. Lifetime bugs show up more reliably in the app,
  booting and quitting a game with `NSZombieEnabled=YES` too.
- Comparing against Vulkan on the same Mac is the main check. All 1061 frame dumps in `ppsspp-framedumps`,
  rendered with both: 1049 are identical. Of the rest, 10 differ because of programmable blending, and are
  closer to the PSP captures (Tenchu went from about 10,000 differing pixels to 900). The Kurohyou dumps are
  unreliable. The benchmark savestates match Vulkan to within a few levels per channel.
- On iOS, pass launch options through `devicectl`'s process arguments (`--graphics=metal`); the app adds
  `--` options from its own launch arguments.

## Speed

Measured 2026-10-17 on an Apple Silicon Mac, 10 emulated seconds from 8 savestates, against Vulkan through
MoltenVK: wall time from 11.7% lower (Ridge Racer) to 2.6% higher, CPU time 2 to 11% lower. God of War was
6% faster after pass merging; before it, Metal was slower because of the extra render passes.

## Not done

- **Intel and AMD Macs** are untested. They get no programmable blending, no 16-bit textures and no
  memoryless depth (all Apple GPU only), so they take the same paths as other GPUs elsewhere.
- **tvOS** shares the iOS code, but hasn't been built or run with Metal.
- **The dev screen's pipeline list** is Vulkan only.
- **Stereo rendering** (multiview, framebuffer arrays) is deliberately left out.
- **Making Metal the default on Mac, and removing MoltenVK,** come later. Existing iOS installs keep the
  backend saved in their ini; only new installs and reset settings get Metal. The render manager includes
  Vulkan headers for `GLSLtoSPV`, which has to change before MoltenVK can go.

---

### Multi Swapchain (`docs/multi-swapchain.md`)

# Multiple swapchains (design)

Status: design, nothing implemented yet.

The first user is an iOS "TV mode": with AirPlay connected, the game runs full screen on the TV and the
phone shows only the touch controls (and the menus). The same mechanism is meant to carry later uses:
ImGui debugger windows outside the main window (ImGui viewports), a desktop window mirroring a VR
headset, and an external display on Android or desktop. So the graphics side is general, and the iOS
part is just its first client.

## Where we are

Everything assumes one output, at every layer:

- **thin3d:** the backbuffer is the `nullptr` framebuffer. `BindFramebufferAsRenderTarget(nullptr)`,
  `GetFramebufferDimensions(nullptr)`, `CopyFramebufferToMemory(nullptr)`, `Present()`, the
  `LOST_BACKBUFFER`/`GOT_BACKBUFFER` events and the present-mode caps all mean "the" swapchain
  (`Common/GPU/thin3d.h`).
- **Vulkan:** `VulkanContext` holds one `surface_`/`swapchain_`/`presentation_` and the surface-derived
  state (format, extent, caps, transform, present modes). The render manager has one
  `FrameDataShared::swapchainImages_`, one set of backbuffer framebuffers and one depth buffer
  (`VulkanQueueRunner.h`). A frame can contain only one backbuffer pass (`SanityCheckPassesOnAdd`
  asserts). The BACKBUFFER render pass is looked up without a format in its key
  (`VKRRenderPass::Get`). Queue selection checks present support against the one surface
  (`ChooseQueue`).
- **`VulkanPresentation`** (`Common/GPU/Vulkan/VulkanPresentation.h`) already abstracts an image chain
  (acquire, present, images, extent, format, final layout). libretro and headless use it to *replace*
  the swapchain. It is the seed of this design.
- **Metal:** one `layer_`, `rtDrawable_` and `bbDepth_` in `MetalRenderManager`. Backbuffer passes are
  merged against a single `BACKBUFFER_TARGET` sentinel.
- **D3D11:** the platform context (`D3D11Context`) owns the one `IDXGISwapChain` and hands its RTV in
  through `GOT_BACKBUFFER`.
- **OpenGL:** one context, one default FBO (`g_defaultFBO`), one `swapFunction_`.
- **Frontend:** one global `g_display`, read in about 250 places, from layout to input scaling.
  `ScreenManager::Render` is the only place that binds the backbuffer. `EmuScreen::render` draws the
  game (`gpu->CopyDisplayToOutput`) and then the touch controls (`renderUI`) into that same pass.

## Graphics: `Draw::Swapchain`

A swapchain is a presentable chain of images: a window, a `CAMetalLayer`, an XR swapchain, a libretro
frame callback. It's a real object, a kind of `Framebuffer`, so the existing framebuffer calls take it
and the backbuffer stops being a special `nullptr`. The current implicit backbuffer becomes the
**primary swapchain**, which the draw context creates along with itself.

```
/* Detailed source-code truncated for AI context efficiency. */
```

- **`nullptr` is an alias for the primary,** so nothing breaks. The direct callers use
  `GetPrimarySwapchain()`. Some code still passes a `Framebuffer *` that may be null (non-buffered
  rendering, for one), so `nullptr` can only become an assert after that's audited. Backends that
  support one swapchain map the primary to their old `nullptr` path (`PrimaryToNull`). Metal treats
  all its swapchains the same way.
- **Size changes on resize.** Unlike other framebuffers, a swapchain's `Width()`/`Height()` change when
  its surface does, and only at a frame boundary.
- **Platform code keeps the primary out of its hands.** `GraphicsContext::InitSurface` still creates
  the primary, so platform code doesn't change.

- **One frame, many presents.** `BeginFrame`/`EndFrame` and the frame semaphores and fences stay
  global. `Present()` presents every swapchain that got a pass this frame. A swapchain without a pass
  is not acquired or presented, so a debugger window can update at 10 Hz for free.
- **Lifetime:** `CreateSwapchain` and `DestroySwapchain` run on the render thread at a frame boundary.
  Destroy waits for the frames in flight that use it. The platform keeps the native surface (layer,
  window) alive until destroy returns (see "iOS").
- **Out-of-date and resize are per swapchain.** Recreating the TV's swapchain must not touch the
  primary's. Losing a secondary surface makes it skip frames until resized or destroyed, never fail
  the frame.
- **Formats:** phase 1 requires every swapchain to use the primary's color format, which is BGRA8 on
  every platform we care about. Then the BACKBUFFER render pass and every pipeline compiled against it
  stay compatible. Mixed formats would need the format in the render-pass key and per-format pipeline
  variants; that's left for when something needs it (HDR output).
- **Caps:** `DeviceCaps::multipleSwapchains`. The frontend checks it and falls back to mirroring.

### Frame pacing

FIFO swapchains block on acquire. With two of them, the frame loop paces to whichever is slower, and
an unlucky vsync phase between two displays makes that stutter. Policy:

- **Exactly one swapchain paces.** Its acquire blocks as today, and the frame-timing machinery
  (present IDs, `FrameTimeHistory`, `GetCurrentPresentMode`) follows it. In TV mode that's the TV,
  since the game is there.
- **The others never block.** They acquire with a timeout of 0. If no image is ready, that swapchain
  skips the frame (its pass becomes a no-op), and it's presented on the next one. They use MAILBOX or
  IMMEDIATE where the surface offers it.
- **On iOS** a `CAMetalLayer` is always vsynced (`displaySyncEnabled` is macOS-only), so a
  non-pacing layer just drops frames when it has no drawable. That's acceptable for touch controls.
  This needs measuring with a 60 Hz TV and a 120 Hz ProMotion phone.

### Vulkan

1. **Split `VulkanContext`'s surface state into `VulkanSurfaceSwapchain`:** surface, swapchain,
   format, extent, caps, pre-transform, present modes, `cbGetDrawSize`. Move `InitSurface`,
   `ReinitSurface`, `InitSwapchain`, `DestroySwapchain` and `DestroySurface` onto it. `VulkanContext`
   keeps a primary one, and its current accessors forward to it, so callers don't change.
2. **Make the real swapchain a `VulkanPresentation` too.** Today it's the `presentation_ == nullptr`
   branch in each consumer. With a real-swapchain implementation, every output is a presentation:
   KHR swapchain, libretro, offscreen, later OpenXR. The render manager then deals with one interface,
   and the branches go away.
3. **Per-output state in the render manager.** `FrameDataShared::swapchainImages_` and the queue
   runner's `backbuffer_`, `framebuffers_` and `depth_` move into a `VKROutput` (presentation, images,
   views, semaphores, framebuffers, depth, out-of-date counter). The per-frame `acquireSemaphore`,
   `curSwapchainImage`, `hasAcquired` and `skipSwap` become a small array indexed by output (cap of
   4). Output 0 is the primary.
4. **Steps.** A RENDER step with no framebuffer gets an output index. `SanityCheckPassesOnAdd` changes
   from "one backbuffer pass" to "one pass per output, after all offscreen passes". `RunSteps`
   already acquires lazily on the first backbuffer step, and that becomes per output.
5. **Submit and present.** The present command buffer waits on the acquire semaphore of every output
   acquired this frame (stage COLOR_ATTACHMENT_OUTPUT) and signals each one's rendering-complete
   semaphore. Real swapchains present in a single `vkQueuePresentKHR` with `swapchainCount = N`, and
   read the per-swapchain results from `pResults`. Other presentations get their own `QueuePresent`.
6. **Queue support.** `CreateSwapchain` checks `vkGetPhysicalDeviceSurfaceSupportKHR` for the
   existing graphics queue family and fails if it's missing. That never happens on Apple, Windows or
   desktop Linux. On Android it's always the same queue.
7. **Rotation.** The pre-transform is per surface, and so is the rect rotation that reads
   `g_display.rotation` (`VulkanRenderManager.cpp`, `RotateRectToDisplay` in the queue runner). The
   rotation goes into `VKROutput` and gets applied per pass.

The single-output path should come out doing exactly what it does now: same waits, same single
present. Most of the regression risk is in step 5, which every platform runs.

### Metal

`layer_`, `rtDrawable_`, `bbDepth_` and the pending size move into a `MetalSwapchain`. The
`BACKBUFFER_TARGET` sentinel becomes the swapchain pointer, so passes merge per target. Replay acquires
each drawable lazily, as today, and the frame's last command buffer does `presentDrawable:` for each.
For a non-pacing layer, skip the pass when `nextDrawable` would block.

### D3D11 and OpenGL

- **D3D11:** easy, and later. Creating the `IDXGISwapChain` moves from `D3D11Context` into the draw
  context, one per HWND, and `Present()` presents each one.
- **OpenGL:** `multipleSwapchains = false` for now. Desktop GL could use one context with
  `MakeCurrent(window_i)` before each window's pass and swap, which needs a per-surface default FBO
  and swap function, and a surface ID in the PRESENT task. Nothing on the iOS path uses GL.

## Frontend: outputs and roles

`g_display` stays as it is and describes the **primary output**: the screen the UI and touch input
live on (the phone). Its 250 readers don't change. Making it per-output would touch layout, input,
DPI and fonts everywhere, for no gain here.

Secondary outputs are described separately:

```cpp
enum class OutputRole {
	Game,     // Just the emulated display, laid out full screen. No UI.
	Mirror,   // A copy of the primary (unused at first, the OS mirrors for us).
	ImGui,    // Later: an ImGui viewport.
};

struct OutputDisplay {
	Draw::Swapchain *swapchain;
	OutputRole role;
	int pixelWidth, pixelHeight;
	float refreshHz;
	float overscanInset;  // TVs.
};
```

These live in a small `OutputManager` owned by the screen manager. The platform layer adds and
removes them (see "iOS").

### Rendering

There is still one screen stack. A screen can draw to more than one output:

```cpp
// Draws this screen's content for a secondary output. The swapchain is already bound.
// Returns false if the screen has nothing for it.
virtual bool renderOutput(const OutputDisplay &output) { return false; }
```

`ScreenManager::Render` stays the only place that binds a swapchain:

1. `PreRender` for every screen, as now. Emulation and post-shader passes run here.
2. For each secondary output, bind its swapchain and ask the stack from the top down. The first
   screen whose `renderOutput` returns true has drawn it. If no screen claims it, the output falls
   back to clearing it (or, on iOS, it's taken away so the system mirrors again).
   - `EmuScreen` claims `Game` outputs. It draws `CopyDisplayToOutput` with that output's
     `OutputParams` and nothing else.
   - The pause menu and the other dialogs don't claim anything. So while the pause menu is up, the
     phone shows the menu and the TV keeps the game (darkened, as behind the menu today).
   - Later, other screens can claim a TV too. The game browser could show a big-screen backdrop
     instead of a mirror of the menu.
3. The primary pass, as now. While `EmuScreen` sends the game to another output, its `render` leaves
   out its own `CopyDisplayToOutput` and draws the touch controls over black. Optionally it draws a
   small game preview, which is a second `CopyToOutput`.

The other option was a stack entry holding one screen per swapchain. It doesn't fit, because input,
focus and dialogs belong to one place (the phone). Per-output screens would have to be pushed and
popped in step, and most entries would have only one.

ImGui windows aren't screens. ImGui owns its viewports. After the screens, the ImGui step draws each
viewport to its own swapchain, so it shares the swapchain layer with the screen stack and nothing else.

### Presentation parameters

`PresentationCommon` should get its output size and layout explicitly instead of from `g_display`.
Mostly it already does, through `PSP_CoreParameter().pixelWidth/Height` and
`UpdateDisplaySize`, and "auto" render resolution follows the same values, so it scales for the TV
on its own. What still reads the global:

- `GetScreenFrame` (insets, DPI)
- the stretch logic in `CalculateDisplayOutputRect` (device orientation)
- `rot_matrix` in `CopyToOutput` and the post-shader passes
- the DisplayLayoutConfig choice (`GetDeviceOrientation()`)

These become an `OutputParams { width, height, rotation, insets, orientation }` handed to
`CopyDisplayToOutput`. The game output on a TV is always landscape, with no rotation and with overscan
as its insets. That refactor is contained (about five call sites) and is worth doing first, since it
can be tested on its own.

### Restrictions

- **Skip buffer effects** (non-buffered rendering) draws the game straight into the bound
  backbuffer during emulation, so it can't target another output. In that mode the game stays on the
  phone, with a one-time notice.
- **Screenshots** taken from the backbuffer (the primary swapchain) would capture the
  phone. Game screenshots should read the game output, or render the game to an offscreen buffer.
- **Cardboard VR** turns off on a `Game` output.

## iOS

- **Getting the scene:** up to iOS 26, a `UIWindowSceneSessionRoleExternalDisplayNonInteractive`
  configuration in Info.plist, with `ExternalDisplaySceneDelegate`. From iOS 27, that's ignored, and the
  app has to register from a view controller instead (`registerSceneAccessory:` with
  `UISceneAccessory externalNonInteractiveSceneAccessoryWithConfiguration:`). We do both. Without a
  scene, iOS just mirrors. An AirPlay Apple TV 4K comes in at 3840x2160.
- **Connect:** the delegate keeps the scene, and posts "external display available" (size from
  `screen.nativeBounds`, refresh rate, overscan) to the native side. It doesn't create a window yet.
- **TV mode on:** when it should be active (setting on, `EmuScreen` running), the main thread creates
  a `UIWindow` on the scene with a `CAMetalLayer`-backed view (`contentsScale = screen.nativeScale`).
  It then queues a request to the render thread, which calls `CreateSwapchain(METAL_EXT, layer)` at a
  frame boundary and registers a `Game` output.
- **TV mode off, disconnect, or resign active:** the render thread destroys the swapchain first, and
  the main thread waits for that (the same handshake as `requestExitVulkanRenderLoop`). Only then does
  it remove the window. Removing the window should give the TV back to system mirroring. **Verify on
  the device:** if iOS doesn't resume mirroring, we need a `Mirror` output for the menus instead.
- **Settings:** "When a TV is connected: Show the game on the TV (default) / Mirror the screen". The
  pause menu gets a toggle for the current session.
- **Out of scope:** iPad Stage Manager's interactive extended displays (role `Application`), and
  tvOS.
- **AirPlay latency:** this is the same video stream as mirroring, so the same lag. Input stays local,
  but the picture is late.

## Later clients

- **ImGui viewports:** `ImGuiConfigFlags_ViewportsEnable`, a platform backend that creates SDL or
  Win32 windows, and renderer hooks that map to this design:
  - `Renderer_CreateWindow` → `CreateSwapchain`
  - `Renderer_RenderWindow` → `BindFramebufferAsRenderTarget(swapchain)` and draw
  - `Renderer_SwapBuffers` → nothing, `Present()` covers it

  It also needs input routed per window, which is its own piece of work.
- **Desktop VR mirror:** with OpenXR on Vulkan, each XR swapchain is a `VulkanPresentation` (acquire
  maps to `xrAcquireSwapchainImage`/`xrWaitSwapchainImage`, present to `xrReleaseSwapchainImage`),
  and the desktop window stays primary. VR is GL-only today, so this also needs a Vulkan OpenXR path.
- **Android external display:** a `Presentation` dialog with its own `Surface`, as a `Game` output on
  Vulkan.

## Phases

1. **`OutputParams` refactor** of `PresentationCommon` and `CopyDisplayToOutput`. No change in
   behaviour, checked with frametests.
2. **thin3d `Swapchain` API plus Metal,** the iOS default. Single-output path unchanged: unit tests,
   pspautotests, frametests.
3. **Dev client on macOS SDL:** a `--game-window` option opens a second SDL window as a `Game` output
   (with `--graphics=metal`). This tests most of the design in a fast loop, without AirPlay. Headless
   can add an offscreen second output, so frametests can check what it shows.
4. **iOS TV mode,** on Metal, tested with the Apple TV.
5. **Vulkan** (`VulkanSurfaceSwapchain`, presentation-for-everything, per-output render manager
   state), for Android, the Mac default and Vulkan users on iOS. Single-output path unchanged: unit
   tests, pspautotests, frametests, an Android device. Then D3D11 when something wants it.

Open questions:
- Does removing the external window resume mirroring?
- How do two vsynced layers pace on a ProMotion phone?

---

### Patching Files (`docs/patching-files.md`)

# Patching files from a script

Most edits should go through an editor tool that does exact string replacement. This document is about
the cases where you reach for a script instead - a repetitive change across many files, say - and the
two ways that silently corrupts a diff on Windows.

The short versions of both live in `AGENTS.md` (General instructions, rules 5 and 6). This is the detail.

## Line endings

Which ending a file has depends on where it was checked out. Markdown, `.cpp` and most sources are
stored in git with LF, and a Windows checkout converts them to CRLF on the way out; a Linux checkout
leaves them alone. So the same file - `.vcxproj`, `.vcxproj.filters`, `android/jni/Android.mk`,
`libretro/Makefile.common`, `AGENTS.md`, much of the source - is CRLF in one working copy and LF in
another. Don't hardcode either, and don't "fix" a file's endings to match what a doc claims.

If you patch one with a script, read **and** write with `newline=''`, which keeps whatever was there.
Reading with Python's default universal-newline translation and writing with `newline=''` silently
converts the whole file, turning a two-line addition into a 5000-line diff.

Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in the
editor.

## Backslashes in a bash heredoc

**Don't feed Python to `bash -c` via a heredoc when the code contains backslashes.** The Git Bash /
MinGW layer strips one level of backslash escaping on the way in, *even with a quoted delimiter*
(`<<'PY'`), which normally suppresses all substitution. So the script Python receives is not the one
you wrote:

| You write in the heredoc | Python actually sees | Result |
|---|---|---|
| `"\r\n"` | `"\r\n"` | fine - survives, because you *want* Python to interpret it |
| `"\\n"` (intending a literal `\n` in the output) | `"\n"` | **a real newline is written into the file** |
| `'foo \\\r\n'` as a match anchor | `'foo \<CR><LF>'` | **anchor silently doesn't match**, reported as "anchor missing" |

The tell for the first case is a compiler error like `C2001: newline in string literal`; the second case
produces no error at all, just a patch that quietly did nothing. Both are invisible in the heredoc you
wrote.

The rule: **a heredoc is fine as long as every backslash in the script is one you want Python to
interpret. The moment you need a literal backslash in the *output*, stop.** Then either:

- use an exact-string-replacement editor tool instead (no shell in the path - the best option for the
  common case of "insert a few lines of C++ that contain `\n`"), or
- write the script to a file and run `python thescript.py`, or
- build the backslash as `chr(92)` so no literal backslash appears in the heredoc at all.

Note this is about the *Python source*, not the data: reading and rewriting a CRLF file with
`newline=''` and `\r\n` anchors works fine in a heredoc, and is the normal way to patch files here.

---

### Pkg Notes (`docs/pkg_notes.md`)

# The PKG package format (PSP game updates)

A `.pkg` is the NPDRM container Sony distributed downloadable content in - full PSN games, DLC,
themes, and the thing these notes are about: **game updates**. An update package holds a patched
EBOOT (`PBOOT.PBP`) plus whatever data files the patch replaces, and installing it drops them in
`ms0:/PSP/GAME/<DISC_ID>/`. The patched EBOOT then runs with the original UMD (or the original
PSN game) still supplying everything it doesn't override.

This document is what was learned decoding some update packages on 2026-08-21. `Tools/pkg.py` is a
working parser/extractor built from it; PPSSPP reads and installs them itself now - see "How PPSSPP
handles them" and the verification sections at the end.

Format reference: <https://www.psdevwiki.com/ps3/PKG_files>, cross-checked against
[pkg2zip](https://github.com/mmozeiko/pkg2zip).

## File layout

```
+0x000  header            (0xC0 bytes, plaintext)
+0x0C0  extended header   (0x40 bytes, plaintext, PSP/Vita only)
+0x100  hashes/signatures
+0x280  metadata          (plaintext, offset and count are in the header)
 ...
+data_offset              encrypted area: item table, then filenames, then file contents
```

### Header

```
+0x00  u32       "\x7FPKG" magic (0x7F504B47)
+0x04  u16       revision. 0x8000 retail, 0x0000 debug
+0x06  u16       type. 1 = PS3, 2 = PSP/Vita
+0x08  u32       metadata offset (0x280 in everything seen)
+0x0C  u32       metadata entry count
+0x10  u32       metadata size
+0x14  u32       item count
+0x18  u64       total package size
+0x20  u64       data offset - start of the encrypted area
+0x28  u64       data size
+0x30  char[0x30] content id, e.g. "JP0177-ULJM05681_00-PJD2UPDATEVR0101"
+0x60  u8[0x10]  QA digest
+0x70  u8[0x10]  riv - the AES counter block (see below)
+0x80  u8[0x40]  header CMAC and signatures
```

### Extended header

Present on PSP and Vita packages, magic `"\x7Fext"` (0x7F657874) at +0xC0. The only field that
matters for reading the package is the **key index**, a u32 at +0xE4 - or equivalently
`header[0xE7] & 7`, which is how pkg2zip reads it. Every PSP package seen uses key index 1.

### Metadata

A flat sequence of `u32 id, u32 size, u8 value[size]` records. The ones worth reading:

| id | Meaning |
| --- | --- |
| 2 | content type. **7 = PSP** (also 0xE/0xF/0x10 for PC Engine / Minis / NeoGeo) |
| 4 | package size |
| 6 | title id |
| 13 | offset and size of the item table, inside the encrypted area |
| 14 | offset and size of `PARAM.SFO`, inside the encrypted area |

Everything else (DRM type, SDK revision, QA digest, install dir) is informational.

## Encryption

The whole area from `data_offset` on is AES-128-CTR, with the counter block starting at `riv`
(header +0x70) and incrementing once per 16 bytes: the block at byte offset *n* of the encrypted
area uses counter `riv + n/16`. Offsets in the item table are relative to `data_offset`, so
that division is straightforward - no separate bookkeeping.

Two different keys are used *within the same package*, and each item says which one applies via
its own `psp_type` byte (see the item table below):

| Package / item | Key |
| --- | --- |
| PS3 package, or PSP item with `psp_type != 0x90` | `2e7b71d7c9c9a14ea3221f188828b8f8` |
| PSP item with `psp_type == 0x90` | `07f2c68290b50d2c33818d709b60e62b` |
| Vita, key index 2/3/4 | `AES-ECB(vita_key_N, riv)`, key by index |

That per-item split is the one thing that isn't obvious from the wiki page and will make a reader
produce garbage filenames for most of a package while a couple of entries decode perfectly. In a
game update it's the `PBOOT.PBP` and the patch data files that carry `0x90`; the icons,
`PARAM.SFO`, `PS3LOGO.DAT`, the directory entries and `ISO.BIN.EDAT` use the PS3 key.

## Item table

`item_count` records of 0x20 bytes, at the item table offset from metadata id 13 (0 in every
update package seen, i.e. right at the start of the encrypted area):

```
+0x00  u32  filename offset (relative to data_offset, always 16-byte aligned)
+0x04  u32  filename length
+0x08  u64  data offset (relative to data_offset, always 16-byte aligned)
+0x10  u64  data size
+0x18  u8   psp_type - 0x90 selects the PSP key, see above
+0x19  u8[2] padding
+0x1B  u8   flags
+0x1C  u32  padding
```

Filenames are stored in the encrypted area too, and are decrypted with **the item's own key**, not
the package's main key.

`flags` is a content type, and in these packages it maps 1:1 onto how the file contents are
encrypted - a decoder can tell what it is holding before looking at it:

| flags | Meaning | Contents start with |
| --- | --- | --- |
| 2 | NPDRM EDAT | `NPD\0` - only ever `ISO.BIN.EDAT` |
| 3 | plain file | whatever it is (PNG, `\0PSF`, ...) |
| 4 | directory | - |
| 5 | PSP EDAT | `\0PSPEDAT` |
| 8 | PSP EDAT (`.sprx` modules) | `\0PSPEDAT` |
| 11 | PBP | `\0PBP` |

Both flags 5 and 8 give you a `\0PSPEDAT`, but they are not the same thing inside. A flags-5 data
file wraps a PGD, which PPSSPP decrypts at runtime through `sceNpDrmEdataSetupKey()` in
`Core/HLE/scePspNpDrm_user.cpp`. A flags-8 `.sprx` wraps an encrypted PRX instead, and goes through
the module loader - see "NPDRM `.sprx` modules" below. The byte at 0x0E of the header tells them apart
(3 for the PGD kind, 1 for the PRX kind).

## What an update package contains

Always this shape:

```
PARAM.SFO                      CATEGORY=PP, TITLE_ID, VERSION
PS3LOGO.DAT                    a PNG, despite the name
ICON0.PNG / PIC0.PNG / PIC2.PNG   sometimes
USRDIR/                        directory entry
USRDIR/CONTENT/                directory entry
USRDIR/CONTENT/PBOOT.PBP       the patched EBOOT
USRDIR/CONTENT/...             the patch data files
USRDIR/ISO.BIN.EDAT            272 bytes, NPD header
```

The outer `PARAM.SFO` is the *package's* - `CATEGORY=PP` (game patch), and its `VERSION` is the
package version, not the patch version. The interesting SFO is the one **inside** `PBOOT.PBP`:
that one has `CATEGORY=PG`, `DISC_ID`, `DISC_VERSION`, `APP_VER` (the patch version) and
`PSP_SYSTEM_VER` (the firmware the patch needs), which is what a real PSP matches against the
disc before deciding to boot the patch.

## What the packages measured

- All are content type 7, `CATEGORY=PP`. Some are for digital NP\* titles, others for UMD UL\*/UC\*
  titles. `PSP_SYSTEM_VER` ranges 6.10 to 6.60.
- **Every single `PBOOT.PBP` uses PRX tag `0x2E5E10F0`** in its `DATA.PSP`. PPSSPP already has
  that key - `Core/ELF/PrxDecrypter.cpp`, in the `TAG_INFO2` table, commented
  "5.00 PSP-2000 (Game PSN Update 2 LBP)". So **no new crypto is needed to run these**.
- Verified end to end rather than assumed: extract Hatsune Miku Project DIVA 2nd's update, rename
  `PBOOT.PBP` to `EBOOT.PBP`, and boot it headless with the UMD mounted -

  ```
  ./build/PPSSPPHeadless --cpu=interpreter --graphics=software --memstick=<ms> \
      --mount="Hatsune Miku - Project Diva 2nd (Japan).iso" \
      <ms>/PSP/GAME/ULJM05681/EBOOT.PBP
  ```

  It logs `Decrypting tag 2E5E10F0`, loads the ELF (`tag=ELF/PdvApp`), resolves its imports, and
  reads `Diva2Data.cpk` / `Diva2Script.cpk` / `Diva2Sound.cpk` off `disc0:`. Runs without error.
  (`--mount` is what makes that work: `Load_PSP_ELF_PBP` in `Core/PSPLoaders.cpp` mounts the ISO
  on `disc0:`, `umd:` and `umd1:` when booting an ELF or PBP.)

**pkg2zip cannot extract update packages**, so it is not an alternative here. Its PSP path only
recognises `USRDIR/CONTENT/EBOOT.PBP` (a full PSN game, which it converts to an ISO),
`PSP-KEY.EDAT` and `CONTENT.DAT`, and `continue`s past everything else - `PBOOT.PBP` and every
patch file are silently dropped, with no warning that anything was skipped.

## How PPSSPP handles them

Four pieces:

- **`Core/Util/PkgUnpack.cpp`** reads a package: header, item table, both PARAM.SFOs, and the
  decryption. `PkgReader::Open()` gives you a `PkgInfo` with the disc ID, disc version and patch
  version; `InstallPkg()` writes the payload out. Sits next to `PSARUnpack.cpp`, and like it needs
  nothing but the AES already in `ext/libkirk`.
- **`UI/InstallPkgScreen.cpp`** is what opening a `.pkg` gets you, the same way a `.zip` gets
  `InstallZipScreen` - it shows what the update patches, what it'll take up on disk, and where it's
  going. The size is exact rather than an estimate: package contents aren't compressed, so summing
  the item table is the answer. `GameManager::InstallPkgOnThread()` does the work.
- **`GameScreen`** shows an installed update in the info pane, and offers "Delete Game Update" in
  its context menu. Deleting takes the whole `PSP/GAME/<DISC_ID>` folder when the update is all
  that's in it, and only `PBOOT.PBP` when a digital game shares the folder - nothing records what
  an install wrote, so the executable is the only part still identifiable afterwards.
- **`FindGameUpdatePBOOT()` in `Core/PSPLoaders.cpp`** is the boot-time half. Starting a disc looks
  for `ms0:/PSP/GAME/<DISC_ID>/PBOOT.PBP`, and boots that instead of `disc0:/PSP_GAME/SYSDIR/EBOOT.BIN`
  if it's there, leaving the disc mounted.

`PPSSPPHeadless --install-pkg=DIR <file.pkg>` does an install without the UI, which is how the
above got tested. It prints what the package is and installs into DIR exactly (the app picks
`PSP/GAME/<DISC_ID>` itself).

### Install layout

The package's own PS3-style wrapping is stripped: `USRDIR/CONTENT/<x>` and `USRDIR/<x>` both become
`<x>` in the game folder, and the `USRDIR` and `USRDIR/CONTENT` directory entries are dropped
rather than created. The root-level files - `PARAM.SFO`, `PS3LOGO.DAT`, `ICON0.PNG`, `PIC0.PNG`,
`PIC2.PNG` - are store metadata and are **not** installed. Writing that `PARAM.SFO` in particular
would be actively wrong: a folder holding a `PARAM.SFO` and no `EBOOT.PBP` is what PPSSPP
identifies as *save data*, so the update would show up in the savedata list.

### Which updates get used

The update's `DISC_ID` has to match the disc's, or it's ignored with a warning - that's what keeps
an update from being applied to the wrong game.

`DISC_VERSION` is advisory. An update is built against one specific disc revision, and PPSSPP logs
a warning when they differ, but still boots it: refusing outright is a worse failure mode than
letting the user find out, since they installed it deliberately. This is not what a real PSP does.
It comes up in practice - the LittleBigPlanet v2.05 update targets disc version 1.00, and the
common European dump is 1.01, and it works.

There's no setting to turn this off. Anyone who has a `PBOOT.PBP` sitting in a game folder either
installed it here or copied it off a real memory stick, and in both cases booting it is what they
were after.

### Verified

- All packages parse and install through the C++ path, byte-identical to `Tools/pkg.py` - each
  one reports its disc ID and versions and writes its payload without an error.
- Hatsune Miku Project DIVA 2nd (ULJM05681): install the v1.01 update, boot the UMD, PPSSPP boots
  `PBOOT.PBP` and the game reads its CPKs off `disc0:`.
- LittleBigPlanet (UCES01264): install the v2.05 update, boot the v1.01 UMD, and the patched game
  opens `ms0:/PSP/GAME/UCES01264/PATCH.ARC` alongside the disc's own `lbp_archive.arc` - the update
  is actually in use, not just booted.
- `python3 test.py -g --graphics=software`: no failures, so restructuring the disc boot
  path didn't disturb anything.
- Truncated and item-table-corrupted packages are refused with a message rather than crashing.
  Corruption *inside* file data still installs - nothing here verifies the package CMAC, same as
  every other PKG tool.
- The browser listing, install screen and install itself were checked by hand in the app.

## Digital (NP\*) titles

About half of the packages patch a digital title rather than a UMD, and that half is tested too.

The one that settles the question is **Super Robot Taisen Operation Extend (NPJH50521)**, because
it's a real NPUMDIMG `EBOOT.PBP` rather than a decrypted ISO dump - `NPDRM: PSAR ID: 4d55504e`,
mounted on `disc0:` by the NPDRM block device, with the game's own 560 MB EBOOT sitting in the same
folder as the update. All eight of its update revisions were installed and booted in turn, and each
one loads a distinguishably different executable:

```
disc executable  .text 0x419b1c
v1.01 0x41fb8c   v1.02 0x4233dc   v1.03 0x42952c   v1.04 0x42a53c
v1.05 0x42a61c   v1.06 0x42a59c   v1.07 0x42b48c   v1.08 0x42b7dc
```

So **`ISO.BIN.EDAT` does not re-key the PBOOT**, which was the open worry: a digital title's patched
executable is encrypted exactly like a UMD one, and needs nothing PPSSPP doesn't already have.

**`DISC_VERSION` being advisory matters far more than expected.** Most of the pairs hit a mismatch,
because the dumps in circulation are later disc revisions than the updates were built against.
Refusing outright would make most of them unusable. Elminage Original was the one clean
exact-match case, disc 1.01 against an update for 1.01, and it boots without a warning.

### NPDRM `.sprx` modules

Package payloads are full of `\0PSPEDAT` files. That's fine for
*data*: `sceNpDrmEdataSetupKey()` in `Core/HLE/scePspNpDrm_user.cpp` wraps an open file descriptor
with the `0x04100002`/`0x04100001` ioctl pair, and the game reads plaintext.

A few packages wrap **executables** that way - `.sprx` modules the game loads with
`sceKernelLoadModuleNpDrm`. Those need more than the data path does. Until they were handled, God
Eater 2 (NPJH50832) installed cleanly, booted its `PBOOT.PBP`, and then looped forever failing to
load `system.sprx`; Shiren 4 Plus (NPJH50698), which keeps the whole game in one `.sprx` behind a
small loader, failed the same way.

An NPDRM module is two layers, and the loader originally saw only the outer one:

```
+0x00  "\0PSPEDAT" header, 0x90 bytes
       +0x08  u32   key mode; low byte is what sceNpDrmGetFixedKey takes (3 in all of these)
       +0x0C  u16   payload offset (0x90 in everything seen)
       +0x0F  u8    flag bits: 1 = XOR in the licensee key, 2 = XOR in the 16 bytes at 0x40
       +0x10  char[0x30]  content ID, "JP0365-NPJH50698_00-SIREN4PLUS2012MA"
+0x90  a normal "~PSP" PRX: tag 0x407810F0 at 0xD0, decrypt_mode 23 at 0x7C
```

`sceKernelLoadModule()` steps over the EDAT header, and the PRX inside then decrypts. Two keys go
into that, both worked out from JPCSP (`ModuleMgrForUser.sceKernelLoadModuleNpDrm`, `crypto/DRM.java`,
`crypto/PRX.java`, `crypto/KeyVault.java`), and both feed `pspDecryptType5()`, which already had a
slot for each.

**xor2, the per-content key** - `NpDrmDeriveModuleKey()` in `Core/HLE/scePspNpDrm_user.cpp`, four
steps in order:

1. `sceNpDrmGetFixedKey(kirk, key, edat+0x10, 0x01000000 | edat[0x08])` - already in
   `ext/libkirk/amctrl.c`, and identical to JPCSP's `hleNpDrmGetFixedKey`: our `key_363C` is its
   `drmFixedKey`, our `key_357C[0/1/2]` are its `drmEncKey1/2/3`, and its AES-CBC under an all-zero
   IV over one block is our `AES_encrypt`. Nothing had called this function before.
2. If `edat[0x0F] & 1`: XOR the licensee key the game passed to `sceNpDrmSetLicenseeKey()`, which
   this file already kept but never used. The game sets it before it loads the module.
3. If `edat[0x0F] & 2`: XOR the 16 bytes at `edat+0x40`. None seen here use it.
4. AES-128 decrypt the result under `drmModuleKey`, the one constant that had to be added here.
   JPCSP does CBC with a zero IV; over a single block that is a plain `AES_decrypt`.

**xor1, a static key picked by `decrypt_mode`** - the PRX header byte at 0x7C being 23
(`DECRYPT_MODE_SPRX`), which is what these payloads are. JPCSP keys this on the mode rather than on
the tag, and so do we: tag 0x407810F0's table entry has no seed of its own in JPCSP's tables either,
so our table was never wrong, it just had nothing to say about a case selected somewhere else.
`pspDecryptType5()` takes the mode-derived XOR when the mode calls for it and falls back to the tag
table's otherwise - the same precedence JPCSP uses, which leaves every tag that does carry a seed
(the `pauth` ones) exactly as it was.

No new decryption logic was needed. `pspDecryptType5()` is structurally identical to JPCSP's "new
method" for type 5: `expandSeed(pti->key, pti->code, seed)` XORs xor2 over the 0x90-byte scrambled
key buffer as `RoundXOR(buf2, 0, 0x90, xor2, null)` does; `PRXType5::decrypt()` XORs both over the
0x50-byte kirk header and SHA1 as `RoundXOR(buf2, 0x14, 0x50, xor1, xor2)` does, and then xor1 alone
over the 0x60 bytes at `id` as `RoundXOR(buf4, 0x14, 0x60, xor1, null)` does. JPCSP's `RoundXOR` is
`buf[i] ^= key[i & 0xF]`, the same repeating XOR as our `xor[i % 0x10]`.

### The other half of it: KL4E

Decrypting is only half the job. Shiren 4 Plus's `f5psp.sprx` decrypts to bytes that start with
`KL4E`: every one of these modules has `comp_attribute = 0x0201`, i.e. compressed, and
`(comp_attribute & 0xF00) != 0` means KL4E/KL3E rather than gzip. So decryption alone would just
move the failure from "unsupported PRX type" to "decompression failed".

`Core/Util/KL4E.cpp` already handles that - it went in for firmware modules that use the same
compression, and these get it for free. It's worth knowing the two halves are independent, because
each one on its own leaves the module unloadable and the error doesn't say which is missing.

Note that none of this can be checked offline against the hand-decrypted `.sprx` files that
circulate for God Eater 2: their flag byte says the licensee key is part of the derivation, and that
key only exists while the game is running. Those files are still useful as a cross-check of the
installer itself - the `PBOOT.PBP` in such a folder is byte-for-byte what `InstallPkg()` writes, and
the only difference is that the `.sprx` files beside it were decrypted by hand.

### Where that leaves the three module titles

- **God Eater 2 (NPJH50832)** installs, boots, loads its modules and plays. Decryption alone wasn't
  enough for it: it also needed the type-B relocation fix in `ElfReader::LoadRelocations2` (issue
  #8075), where two `lui`s sharing one `addiu` got different high halves, so a callback pointer
  landed 0x48 bytes inside a function.
- **Shiren 4 Plus (NPJH50698)** loads `f5psp.sprx` - the log says `'FDS3PSP' is KL4E-compressed,
  decompressing` - and runs.
- **Tales of the World Radiant Mythology 3 (NPJH50353)** still doesn't reach its modules inside a
  headless boot, so it remains untested rather than known-good.

### Still not tested

- Only God Eater 2 has been played past a title screen. For the rest the runs are short headless
  boots, so "the update is in use" means the patched executable is what loaded and ran - not that a
  patched *asset* was read. LittleBigPlanet's `PATCH.ARC` covers that for a UMD title; there's no
  equivalent observation for a digital one yet.

---

### Pspautotests Hardware (`docs/pspautotests-hardware.md`)

# Writing pspautotests and running them on a real PSP

[docs/pspautotests.md](pspautotests.md) covers running the *existing* tests against PPSSPPHeadless.
This document covers the other half: connecting a real PSP over USB, running a test on it, and
recording its output as the `.expected` file that becomes the ground truth.

You need this whenever the answer to "what does the hardware actually do?" isn't already in an
`.expected` file - which is most of the time when you're implementing something new.

## What you need

- A PSP with custom firmware and a USB cable. (Verified against firmware 6.61 / PSPLink v3.0.)
- **PSPLink** installed on the PSP, in `PSP/GAME/psplink`, and *running* - launched from the game
  menu. This is not the same as connecting the PSP in USB mass-storage mode; if the PC sees
  "PSP Type A" you're in USB mode, and you want "PSP Type B".
- The **pspdev toolchain** on the PC, which supplies `psp-gcc`, `pspsh` and `usbhostfs_pc`.
  On macOS it lives in `~/pspdev` by default and is *not* on `PATH`, so prefix commands with
  `PATH="$HOME/pspdev/bin:$PATH"` or export it once per shell. On macOS you also need
  `brew install libusb-compat`; on Windows, the libusbK driver via Zadig.

Ask the user to connect the PSP and start PSPLink - you can't do it for them. To confirm it's
there before touching anything else:

```bash
ioreg -p IOUSB -l -w 0 | grep -i "USB Product Name"     # macOS; want "PSP Type B"
```

## The three moving parts

```
  PSP (PSPLink)  <--USB-->  usbhostfs_pc  <--TCP 3000-->  pspsh / gentest.py
```

- **`usbhostfs_pc -b 3000`** bridges USB to a TCP port and serves the PC filesystem to the PSP as
  `host0:/`. **Start it from the `pspautotests` root**, because `host0:/` is literally its working
  directory, and that's where a test's output files land.
- **`pspsh -p 3000`** is a shell on the PSP. `pspsh -p 3000 -e "<cmd>"` runs one command and exits.
  Handy ones: `ls`, `pwd`, `reset` (reboot PSPLink after a hung test), `pspver`, `power`, `scrshot`.
- **`gentest.py`** builds a test, runs it through `pspsh`, waits for it to finish and copies the
  output over the `.expected` file. It starts `usbhostfs_pc` itself if the port isn't already open.

**One PSP, one test at a time.** Never start a second `gentest.py` or `pspsh` while one is still
running, and that includes an agent issuing several as parallel tool calls. They share the one
PSPLink session and the output files in `host0:/`, so they collide: tests that pass alone "time out
after 10 seconds", or the PSP hangs. Re-recording a directory means running its tests one after
another.

Bringing it up, once, from the repo root:

```bash
export PATH="$HOME/pspdev/bin:$PATH"
cd pspautotests
(cd common && make)                 # builds libcommon.a; gentest.py refuses to run without it
usbhostfs_pc -b 3000 &              # leave running; prints "Connected to device"
pspsh -p 3000 -e ls                 # sanity check - should list the pspautotests directory
```

If `ls` shows `host0:/` contents you have a working chain. If it hangs or shows nothing, PSPLink
isn't running or the USB driver didn't bind.

**Under WSL** the PSP first has to be passed through from Windows with usbipd-win (steps 10-12 of
<https://pspdev.github.io/psplink/windows.html>). `Tools/psplink_wsl.sh` does that and then starts
`usbhostfs_pc` in the current directory, so run it from `pspautotests` in place of the
`usbhostfs_pc` line above. It asks for a UAC prompt the first time the PSP is shared, and re-attaches
after a PSPLink reset or a move to another USB port.

## How a test reports its results

`common/common.c` wraps every test. Before `main()` it redirects the process's `stdout` and
`stderr` to `host0:/__testoutput.txt` and `host0:/__testerror.txt`, and after `main()` returns it
writes `host0:/__testfinish.txt` as a completion marker. So in the `pspautotests` root you'll see:

| File | Meaning |
|---|---|
| `__testoutput.txt` | the test's stdout - this is what becomes the `.expected` file |
| `__testerror.txt` | stderr; **non-empty means `gentest.py` refuses to write `.expected`** |
| `__testfinish.txt` | written on clean exit; its absence is how a timeout is detected |
| `__screenshot.bmp` | written by `emulatorEmitScreenshot()`; becomes `.expected.bmp` |

`gentest.py` deletes all four before each run, so a stale file can't be mistaken for a result.

That redirection is also why `usbhostfs_pc`'s working directory matters: get it wrong and the
output files appear somewhere unexpected, or the test can't open its data files.

## Writing a new test

A test is one directory under `pspautotests/tests/`, holding a `Makefile`, the source, the built
`.prx`, and the `.expected`. Minimal `Makefile` - use `common.mk`, not the older verbose style
still found in some directories:

```make
TARGETS = shortname

COMMON_DIR = ../../../common
include $(COMMON_DIR)/common.mk
```

`TARGETS` lists every test in that directory (one `.c`/`.cpp` per entry); `COMMON_DIR` is a
relative path, so count the levels. `common.mk` sets `BUILD_PRX = 1` and links `libcommon`.

The source includes `<common.h>`, which `#define`s `main` to `test_main` so the wrapper above runs.
Add `<sysmem-imports.h>` if you need `sceKernelSetCompiledSdkVersion*`. Then just `printf`.

```c
#include <common.h>
#include <pspiofilemgr.h>

int main(int argc, char **argv) {
	printf("result: %08x\n", sceIoSomething());
	return 0;
}
```

Two things `common.h` gives you that are easy to miss: `ARRAY_SIZE`, and `checkpoint()`, which
prefixes each line with `[r]`/`[x]` to record whether a reschedule happened - see
[docs/pspautotests.md](pspautotests.md) for what those markers mean. Use plain `printf` when
scheduling isn't what you're testing.

Then generate the expected output, from the `pspautotests` root:

```bash
python3 gentest.py io/shortname/shortname       # path under tests/, without .prx
```

It runs `make` in the test's directory first, then runs it on the PSP and writes
`tests/io/shortname/shortname.expected`.

Finally register the test in the repo root's `test.py`: new tests go in **`tests_next`**, and move
to `tests_good` only once PPSSPP passes them. Then check PPSSPP against the hardware:

```bash
python3 test.py --graphics=software io/shortname/shortname
```

### Design rules for a test that can actually pass

- **Only print things that are the same on hardware and in the emulator.** Kernel pointers, heap
  addresses and absolute times are not - PSPLink shifts the memory layout, so a test that prints
  them can never pass. Print offsets from a base, or ranges, instead.
- **Sort anything whose order isn't the point.** `sceIoDread` returns a real FAT directory in
  creation order and a host directory in whatever order the host filesystem gives; if you're
  testing names, `qsort` by name and the difference disappears.
- **Make it repeatable.** If the test creates files, delete them at the start *and* the end - an
  aborted run otherwise leaves state that changes the next run's output.
- **Choose an encoding with no ambiguity.** When dumping a buffer, don't render NUL as `.` if the
  data can contain a literal `.`. Pick a character the data can't contain (`|` is forbidden in FAT
  names, so it works there) - otherwise the `.expected` quietly lies.

## Gotchas

- **`gentest.py` runs `make` for the whole test directory, not just your test**, so a neighbour
  that doesn't compile stops your test before it ever reaches the PSP. Everything under `tests/`
  builds with pspdev GCC 15 as of the "Make the tests build with a current pspdev toolchain"
  commit; if you hit a broken one anyway, `gentest.py -k` (`--keep`) skips `make` entirely and
  you can build your own target by hand with `make yourtest.prx`.
- **When you touch one test, rebuild every `.prx` in its directory and re-record them all.** Many
  committed binaries were built with a much older SDK, and a rebuild with the current toolchain can
  change what a test *does*: `time_t` is 64-bit now, so `rtc/convert`'s
  `sceRtcSetTime_t(&pt, 62135596800ULL)` marshals differently, and in `tests/intr` the SDK's
  `ModuleMgrForUser` stubs, linked next to the directory's own import of that library, silently did
  nothing - `waits` printed `sceKernelGetModuleId`'s result for `sceKernelStartModule`. Rebuilding
  a whole directory at once turns such surprises up while you're there to explain them, rather
  than one at a time whenever someone happens to touch a neighbour. So run a plain `make` in the
  directory, record each test on hardware, and commit the binaries and `.expected` files together.
  (The binaries also grow by roughly a third: `testgp.prx` went from 117 KB to 191 KB.)
- **Diff every re-recorded `.expected` against the old one before committing.** A difference means
  the test changed on purpose, the toolchain changed what it does (fix the test), or the firmware
  did. Many old recordings are likely from a PSP on an earlier firmware than today's 6.61, and a
  driver change or a bug fix between versions shows up regardless of `sceKernelSetCompiledSdkVersion`
  (`intr/registersub` depends on which drivers have hooked which interrupts: interrupt 8 had a
  handler in the old recording and has none on 6.61). Say which in the commit message, and keep
  the 6.61 result, since that's the firmware PPSSPP models. Note the committed `.expected` files have CRLF
  line endings (they were recorded on Windows) while a fresh run writes LF, so compare with
  `diff <(tr -d '\r' < __testoutput.txt) <(tr -d '\r' < the.expected)`.
- **Each test PRX stays resident after it runs.** Run a handful back to back and the next
  `Load/Start` fails with `0x80020190` (out of memory) - which looks exactly like a hung test.
  `pspsh -p 3000 -e reset` between runs, and wait for the PSP to come back before the next one.
- **`host0:` is not a FAT volume, and it isn't even the same across hosts.** It's PSPLink's bridge
  to the PC, so it inherits the PC's filesystem: `tests/io/directory` was recorded on Windows and
  does not match on macOS, where `..` reports a different size and short names come back as
  `1.txt` rather than `1.TXT`. Anything testing FAT semantics has to run against `ms0:` - create a
  scratch directory on the real memory stick and clean it up - and anything reading `host0:` will
  only reproduce on the OS it was recorded on.
- **Start `usbhostfs_pc` yourself before the first `gentest.py` run in a scripted session.** When
  `gentest.py` has to start it, the bridge inherits the script's stdout and keeps running, so
  anything waiting for the pipe to close (`gentest.py ... | tail`, or an agent's shell tool) hangs
  long after the `.expected` file has been written. With the port already open it returns at once.
- **A test that hangs leaves the PSP wedged.** `gentest.py` issues `pspsh -e reset` after a
  timeout, but if you ran the PRX by hand, do that yourself. Default timeout is 10s; raise it with
  `-t SECONDS`.
- **Bound every wait on the PSP, including the whole command.** A test that hasn't finished in 10
  seconds is hung; a longer timeout only delays finding out which call blocks. To check whether the
  PSP is alive, run `pspsh -p 3000 -e ls` in the background and give up after 10 seconds (macOS has
  no `timeout` command). `gentest.py -t` doesn't cap the run either: its recovery `pspsh -e reset`
  blocks forever if the PSP is really gone, so put a limit on the shell command as well.
- **A PSP that doesn't answer may only need the host side restarted.** If it still enumerates over
  USB (`ioreg -p IOUSB -l -w 0 | grep -i "USB Product Name"` shows "PSP Type B" on macOS), kill
  `usbhostfs_pc`, start it again from the pspautotests root, and probe once more. Otherwise someone has
  to restart PSPLink or reconnect the cable.
- **Never let a test exit with a utility dialog still active.** A dialog left in INIT, RUNNING or
  FINISHED keeps the utility module busy, and the next `reset` hangs PSPLink until it's restarted by
  hand. In utility tests, bound every wait, and run a case that might not finish last and alone.
- **`tests_to_generate` in `gentest.py`** - the list used when you pass no arguments - is stale;
  several paths in it no longer exist. Always name the test you want.
- **`--sdkver` matters for some APIs.** `gentest.py --sdkver=6060010 --sdkver-func=606` makes the
  test call `sceKernelSetCompiledSdkVersion606()` at startup, and `-a`/`--all-versions` sweeps every
  known version reporting which ones behave differently - a fast way to find version-gated
  behavior. A test can also call `sceKernelSetCompiledSdkVersion*()` mid-run to cover several
  versions in one `.expected`.
- **On a PSP, printed output is also drawn on the screen**, into the display framebuffer. A GPU test that
  renders into that framebuffer and prints between draws reads its own text back: each line changed the
  alpha of a few pixels under it in the next draw, deterministically on the PSP and never in PPSSPP. Set
  `HAS_DISPLAY = 0` (`extern "C" int HAS_DISPLAY;`, from `common.c`) after setting up the display.
- **The module must be named `TESTMODULE`** (`common.c` does this) or `gentest.py` prints the load
  line as an unexpected result.

## Kernel-mode tests

Some behaviour differs by privilege - `sceKernelCreateTlspl` accepts partitions 1, 3 and 4 from a
kernel module and rejects them with `ILLEGAL_PERM` from user mode - so occasionally a test has to
run as a kernel module. `tests/threads/tls/kernel` is the working example. Set `COMMON_KERNEL = 1`
in the Makefile before including `common.mk` and you get a kernel build:

```make
TARGETS = partition
EXTRA_OBJS = tlspl-imports.o
COMMON_KERNEL = 1

COMMON_DIR = ../../../../common
include $(COMMON_DIR)/common.mk
```

Three things make this work, and all three took a while to find, so don't undo them casually:

- **`USE_KERNEL_LIBS`, set automatically by `common.mk`.** The stock `crt0_prx.o` references
  `__libcglue_init`, which pulls in the whole of libcglue, which imports `sceNetInet`,
  `sceUtility` and the `ForUser` IO libraries. A kernel module that imports any of those fails to
  load with `8002013C` (library not found), and no amount of trimming `LIBS` helps because the
  reference comes from the startup object itself. `USE_KERNEL_LIBS` selects a different startup -
  and brings `-nostdlib`, so `common/kernelglue.c` supplies the newlib support hooks that go
  missing: `_sbrk` over a fixed heap, the `_read`/`_write`/`_close` stubs, no-op retargetable
  locks, and `module_start`.
- **Nothing may import a `ForUser` library.** `ThreadManForUser` is fine, but `SysMemUserForUser`
  is not, which is why `common.c` compiles the `sceKernelSetCompiledSdkVersion` helpers out of
  kernel builds - a function pointer table referencing them is enough to pull the stubs in.
  Watch for this: `psp-strings yourtest.prx | grep For` lists what you're importing. Beware also
  that `ThreadManForKernel` does not export the Tlspl calls - importing them from there links
  cleanly and then returns `8002013A`, library not yet linked, from every call.
- **Size.** The kernel partition has about 285 KB free with PSPLink resident, 242 KB of it
  contiguous, and loading over the cable needs room for the file *and* the loaded image at once.
  A kernel build therefore gets a 4 KB `schedfBuffer` and a 4 KB heap instead of the user build's
  64 KB and 21 MB. `psp-size yourtest.elf` plus the `.prx` file size against `meminfo`'s MAXFREE
  tells you whether it will fit; `800200D9` (memblock alloc failed) means it didn't.

Output works normally - `printf`, `checkpoint` and `schedf` all behave - but a kernel build writes
to `host0:` with `sceIo` directly rather than through newlib's `FILE`, since that's what dragged
libcglue in. Formatting still goes through newlib's `vsnprintf`, so the usual format specifiers
are all available.

Output reaches an emulator the same way it reaches the cable: a kernel build feeds
`sceIoDevctl("emulator:", SEND_OUTPUT)` as well as writing the file, and calls
`sceKernelExitGame` at the end so headless stops rather than spinning to its timeout. Both are
easy to forget when writing a new harness - without the first the test appears to produce nothing,
and without the second it always reports TIMEOUT even though it ran.

One emulator-side note, since it took a while to pin down: privilege on the PSP belongs to the
*caller*, not to the syscall. PPSSPP's `hleIsKernelMode()` only reports whether the entry point
itself is a kernel-only export, so a kernel module calling an ordinary `ForUser` NID used to look
like user mode. `__KernelCurThreadIsKernelMode()` answers the question this test needs.

## Worked example: FAT short names

`tests/io/shortname` was written this way and is a decent template. It creates a scratch directory
on `ms0:`, fills it with names that exercise the 8.3 rules, and dumps the raw `d_private` block
that `sceIoDread` fills in.

It was written to *dump* rather than *decode* on purpose, and that immediately paid off: the
struct in the SDK's own `pspiofilemgr_dirent.h` does not match what firmware 6.61 writes when the
compiled SDK version is unset or below 3.08. The real layouts are

| Compiled SDK version | Layout |
|---|---|
| unset, or <= 3.07 | short name at byte 0 (13 bytes), long name at byte 13 - no size field |
| >= 3.08 | caller-supplied size at byte 0, short name at byte 4 (16 bytes), long name at byte 20 |

which is what `Core/HLE/sceIo.cpp` implements, now confirmed on hardware rather than inherited from
JPCSP. Decoding with the SDK struct instead would have produced strings truncated at the front and
an `.expected` that silently enshrined the mistake.

The test also caught three real emulator/hardware differences, still open in `tests_next`:
hardware preserves the original case in `d_name` (`readme.txt`) where PPSSPP uppercases it
(`README.TXT`), and hardware appends `~1` to the short name of any name that isn't already valid
uppercase 8.3 (`readme.txt` -> `README~1.TXT`, `MiXeD.txt` -> `MIXED~1.TXT`) where PPSSPP only does
so on a collision.

## Committing

`pspautotests` is a git submodule, so a new test is two commits:

1. Commit inside `pspautotests/` (the new test directory, including the built `.prx` and the
   generated `.expected`), rebase it onto `origin/master`, and push it straight to `master` - no
   branch, no PR, and no need to ask first.
2. Commit in the PPSSPP repo, including **both** the `test.py` change and the bumped submodule
   pointer (`git add pspautotests`) - otherwise CI checks out the old submodule and the test
   doesn't exist.

---

### Pspautotests (`docs/pspautotests.md`)

# pspautotests

This test suite is located in the `pspautotests` submodule/subdirectory at the root of the repo. Its `tests/` subdirectory contains tests arranged by category (audio/, cpu/, gpu/, threads/, etc.).

The runner script `test.py` at the repo root contains two lists: `tests_good` (known-passing regression tests) and `tests_next` (tests that don't yet pass — move things here → `tests_good` by fixing PPSSPP).

Tests are compiled as PRX binaries (a variation on ELF) which PPSSPP can load. They also have an `.expected` file alongside them, containing the reference output from running on a real PSP via PSPLink. The goal is for PPSSPPHeadless's output to match.

## Prerequisites

- **Initialize the submodule** (if not done): `git submodule update --init`
- The PRX binaries and `.expected` files are already committed inside the submodule — **no need to compile tests yourself** just to run them.
- To build PPSSPPHeadless itself, see the next section.

## Building PPSSPPHeadless

### Windows (Visual Studio)

Open `Windows/PPSSPP.sln` in Visual Studio and build the **PPSSPPHeadless** project, or use MSBuild directly:

```
MSBuild.exe -noAutoResponse Windows/PPSSPP.sln -target:PPSSPPHeadless -property:Configuration=Debug -property:Platform=x64 -maxCpuCount
```

The built executable will be at `Windows/x64/Debug/PPSSPPHeadless.exe`.

> CMake-based builds do exist but are **not the recommended way** on Windows — stick with the VS solution.

### Linux / macOS

Use `./b.sh --debug`, then find the executable under `build/` (e.g. `build/Debug/PPSSPPHeadless`).

## Running tests

### Using test.py (recommended)

Make sure Python is available. On Windows the Microsoft Store alias may interfere; use `py` or the full path to `python.exe`.

- Run all good tests: `py test.py -g`
- Run all broken/next tests: `py test.py -b`
- Run all tests (good + next): `py test.py`
- Run a specific test (or space-separated list): `py test.py -g cpu/cpu_alu/cpu_alu`
- Run a group of tests by prefix match (add `-m`): `py test.py -g -m audio/atrac`

### Direct headless invocation

```
Windows/x64/Debug/PPSSPPHeadless.exe --root pspautotests/tests/../ --compare --timeout-wall=5 --graphics=software pspautotests/tests/audio/atrac/addstreamdata.prx
```

Instead of a single PRX, you can pass a directory (e.g. `pspautotests/tests/threads/mbx/...`) to run all tests under it, recursively.

**Key flags:**
- `--root` — points to the directory above `tests/` so the headless can find the expected directory layout.
- `--compare` — enables output comparison against `.expected` files.
- `--timeout-wall=N` — real seconds per test before killing it (default 5). `--timeout-emulated=N` is the
  same idea in emulated time, and both can be set at once. `--timeout` is the old name for `--timeout-wall`.
- `--graphics=software` — uses software GPU backend (required for headless; no real GPU available).

### What you'll see

**Passing test:**
```
pspautotests/tests/audio/atrac/addstreamdata.prx:
  audio/atrac/addstreamdata - passed!
1 tests passed, 0 tests failed, 0 tests missing.
```

**Failing test:**
```
pspautotests/tests/threads/mbx/refer/refer.prx:
O   hi 0 prio=00 next=OTHER  hi 1 prio=00 next=ITSELF  ...
E   hi 1 prio=00 next=ITSELF  ...
+   ...
0 tests passed, 1 tests failed, 0 tests missing.
Failed tests:
  threads/mbx/refer/refer
```

Lines prefixed with `O` are what PPSSPP produced (the Output), `E` is the corresponding line from
the `.expected` file recorded on a real PSP, and `+` means a match.

The diff is line-by-line, so an `O` line followed by an `E` line at the same conceptual position
means PPSSPP produced different output at that spot. A `+` line means both outputs agreed on that
line.

### The `[x]` and `[r]` markers - they're about scheduling

Most tests print their lines through `checkpoint()` in `pspautotests/common/common.c`, which tags
every line with `[x]` or `[r]`:

```
O [x] While open: OK (allocated 12)
E [x] While open: OK (allocated 0)
```

The tag is not decoration. Each `checkpoint()` call starts a helper thread that does nothing but
set a flag, then terminates and restarts it for the next one. If that thread got a chance to run
before the next checkpoint, the line is tagged `[r]` - a **r**eschedule happened while the code
under test ran. If it never got scheduled, the line is tagged `[x]`. (With `CHECKPOINT_ENABLE_TIME`
the tag becomes `[x/1234]`, adding the microseconds since the previous checkpoint.)

So the marker records **whether the syscall between the two checkpoints yielded to another
thread**, which is a big part of what these tests are checking.

That means a diff where only the marker differs, like

```
O [x]   GetCharInfo on open: 00000000
E [r]   GetCharInfo on open: 00000000
```

is not a value bug at all - the return value matched. It says our implementation of that call
doesn't reschedule where the real one does (or the other way round). Fixing it means changing
whether the HLE function yields - `hleReSchedule`, `hleDelayResult` and friends - not what it
returns. Don't go hunting for a wrong value; there isn't one.

## Workflow for fixing a test

1. Pick a test from `tests_next` in `test.py`.
2. Run it with headless to confirm failure and see what differs (`O` vs `E` lines).
3. Read the test source (`.c`/`.cpp`) and the `.expected` file to understand the API being tested.
   The `.expected` file was recorded from a real PSP — it's the ground truth. The test source
   reveals what syscalls are made and in what order. Sometimes the test deliberately corrupts
   state to probe kernel error handling.
4. Form a hypothesis: look for a systematic pattern in the diffs (wrong order, wrong error
   code, missing output). Cross-reference with multiple expected files that exercise the same
   API — they may reveal the PSP's real behavior from different angles.
5. Make changes to PPSSPP's HLE or other core code. Do not make super-targeted changes just
   to fix the test — instead, fix the underlying issue in a way that would also make sense
   on real PSP hardware.
6. Rebuild PPSSPPHeadless and re-run the test.
7. Run the full `tests_good` suite (`py test.py -g`) to check for regressions. A correct fix
   should not break any previously passing tests.
8. Rinse and repeat until it passes, then move it from `tests_next` to `tests_good` in `test.py`.

### Tips from experience

- The diff output compares the full text output line-by-line. To see PPSSPP's raw output
  without the diff overlay, omit `--compare`:
  ```
  Windows/x64/Debug/PPSSPPHeadless.exe --root pspautotests/tests/../ --timeout-wall=5 --graphics=software path/to/test.prx
  ```
  There's another trick too, --print-equal-lines, which prints matching lines with a '=' prefix, so you can see the full output with context.
- Tests can show contradictory expected outputs at first glance. For example, the mbx/send
  test expected file shows FIFO message order (normal sends), while the mbx/refer test shows
  LIFO (after corruption) — because `sceKernelReferMbxStatus` on a real PSP *updates*
  `firstMessage` during traversal, changing the apparent head. Understanding the expected
  file's behavior often requires reading multiple related tests together.
- When searching for the underlying issue, trace through the HLE implementation with the
  test's syscall sequence. Check whether the kernel writes into PSP-visible memory — if so,
  test code can corrupt those values, and the PSP kernel may have specific handling for that.
- **Pointer addresses differ between PSP and PPSSPP.**  A test that prints raw kernel pointers
  (heap addresses, TLS block addresses, etc.) will always have mismatched expected output
  because PSPLink shifts memory layout.  Fix by changing the test to print offsets from a
  base address instead of absolute addresses.  After changing the test, run it on hardware
  to generate a new `.expected`.
- **The diff notation:**
  - `O` line = present in PPSSPP's output but not in expected (PPSSPP-only).
  - `E` line = present in expected but not in PPSSPP output (expected-only).
  - `+` line = context line (shown around diffs for context).
  - `=` line (with `--print-equal-lines`) = matching line.
  - `[r]` / `[x]` prefix = rescheduling occurred / did not occur since last line.
- **When a function is a stub** (`UNIMPL` in log), the expected output is a good specification
  for what to implement.  Look at multiple test cases in the expected file to understand the
  full range of valid and invalid inputs, return codes, and side effects.
- **Time-dependent tests** (RTC, timezone conversions) depend on the host machine's clock
  and timezone.  The PSP's `sceRtcParseDateTime` was a stub — the expected file showed
  exactly which RFC 3339 / RFC 2822 formats are accepted and which are rejected (return -1).

## Troubleshooting

- **"Python was not found" on Windows** — the Microsoft Store alias is interfering. Use `py` (the Python launcher) or the full path, e.g. `"C:/Users/.../AppData/Local/Programs/Python/Python314/python.exe" test.py -g`. Or disable the alias in Settings > Apps > Advanced app settings > App execution aliases.
- **No PRX files found** — run `git submodule update --init` from the repo root to fetch the `pspautotests` submodule.
- **PPSSPPHeadless exits immediately / "CPU not started"** — the test PRX may be missing or the `--root` path is wrong. Ensure `--root` points to the parent of `tests/`.
- **MSBuild error MSB1008** — the `MSBuild.rsp` response file may be interfering. Always pass `-noAutoResponse` on Windows builds.

---

### Reverse Engineering (`docs/reverse-engineering.md`)

# Reverse-engineering a PSP firmware module

`PPSSPPHeadless --re-module` loads a single PRX on its own - no game, no boot - and writes a report
about it: the module header, its exports and imports with NIDs resolved to names, one annotated
disassembly file per function, and the call graph.

This is a developer tool for understanding the PSP, not something a user ever runs.

## Running it

```bash
./build/PPSSPPHeadless --memstick ~/.config/ppsspp \
    --re-module flash0:/kd/libmp3.prx \
    --re-out /tmp/re
```

To copy a file out of a disc image as it is (a movie, a data archive), use `--dump-file
disc0:/PSP_GAME/USRDIR/X.PMF --dump-file-out OUT game.iso`, which also lists a directory. Use it, or
`--re-module` for the game's own binaries, rather than writing an ISO or CSO reader.

`--re-module` takes either a host path or a PSP-style `flash0:/kd/foo.prx`, which is resolved
against the configured NAND directory - so `--memstick` (or `--nand`) has to point at a dump.
PPSSPP can produce one itself from a firmware updater with `--unpack-updater`; see
[PsarFileFormat.md](PsarFileFormat.md).

| Option | |
|---|---|
| `--re-module PATH` | The module to load. Host path, or `flash0:/kd/foo.prx`. |
| `--re-out DIR` | Where the report goes. Created if missing; defaults to `re-out`. |
| `--re-func NAME` | Only disassemble this one function, by name or `0x08801234`. |
| `--re-syms FILE` | A `.ppsym` file of names to apply first, so the disassembly reads properly. |

## What comes out

- `<module>.index.md` - header, segments, export table (library, NID, address, name), import table,
  and every function with its size, caller/callee counts and whether it writes `v0`.
- `<module>/<addr>_<name>.asm` - one file per function.
- `<module>.xref.json` - the call graph, for asking "who calls this" without grepping.

Two things in the disassembly are worth knowing about:

**`lui`/`addiu` pairs are folded** and reported as the address they form, with a symbol name where
one is known (`; = 08004a10 <sampleRateTable>`). Every global and constant table is reached through
such a pair, so this is usually how you find the data a function works on.

**Each function gets a register evidence block rather than a guessed signature.** It reports, for
`a0`-`a3`, whether each was read before being written, written before being read, never touched, or
- the interesting case - *never read but still live across a call*:

```
;   a0   READ before written  -> used as a parameter here
;   a1   never read, but live across a call -> FORWARDED from our caller
```

That last one matters because MIPS code routinely takes an argument it never touches and leaves it
in place for a callee to pick up. A tool that inferred "this function takes one argument" from the
reads alone would be wrong, and so would you. Treat `FORWARDED` as evidence that the real arity is
larger than what is read here, and settle it by looking at what the callees do with the register.

## Naming things

The loader's scan names every function it finds `z_un_<address>`, which makes for unreadable
disassembly. The tool names what it can automatically - exported functions get their name from the
NID via PPSSPP's own HLE tables, and every import stub is named after the function it resolves to -
but the rest is up to you.

Accumulated names go in a `.ppsym` file, the same module-relative format the emulator saves from
`hle.module.saveSymbols` and the ImDebugger, and are applied with `--re-syms`. Names compound: once
a function is named, every call site that reaches it reads as that name, so later functions get
progressively cheaper to work out.

Those files are keyed by module name and crc32 (`PSP/SYSTEM/SYMBOLS/<name>_<crc>.ppsym`), so a set
of names only ever attaches to the exact build of the module it was written against. The report's
header line prints the crc to match.

## Notes and caveats

- Modules load at a fixed base (`0x08000000` for kernel modules), and the index prints a `+offset`
  column, so addresses can be compared against another tool's view of the same module.
- Some modules - `sysmem.prx` and `loadcore.prx` among them - have `modinfo` pointers that are file
  offsets rather than addresses, so the loader's own function scan finds nothing in them. The tool
  falls back to scanning the module's text range directly. Those modules are hand-written assembly
  without standard prologues, so expect few, large "functions".
- Nothing is executed. The tool brings up the memory map, timing, the HLE tables and the kernel
  allocators, then runs the real module loader and stops.

---

### SceAudio (`docs/sceAudio.md`)

# sceAudio: how the PSP's audio output actually behaves

This describes the behavior `Core/HLE/sceAudio.cpp` and `Core/HLE/__sceAudio.cpp` model, and
where that behavior came from. Confirmed on a real PSP by `pspautotests/tests/audio/blocking`.

The headline, because it is the thing most likely to be assumed wrong: **the blocking output
calls are not a queue that callers line up behind.** Each channel holds one buffer, the
Output2/SRC channel holds two, and a caller who finds no room is told
`SCE_ERROR_AUDIO_CHANNEL_BUSY` and expected to go away and come back.

## Two different pieces of hardware

`sceAudio` presents nine channels, but they are not the same thing underneath, which is why the
emulator keeps them in two different structures rather than one array of nine: `AudioChannel`
for the eight the mixer walks, and a single `AudioSRCChannel` for the ninth.

**Channels 0-7** go through a software mixer running as a kernel thread. Every time the audio
DMA finishes a block, that thread wakes, takes up to 64 samples from each channel that has a
buffer, sums them into a 32-bit accumulator, clamps, and writes 64 stereo frames into the other
half of a double buffer.

**Channel 8** - `sceAudioOutput2*`, `sceAudioSRC*` and `sceVaudio*` are all the same channel -
never touches the mixer. It builds DMA descriptors that point straight at the game's buffer and
lets the codec resample it. `sceAudioOutput2Reserve(n)` is literally
`sceAudioSRCChReserve(n, 44100, 2)`, and the other Output2 entry points are one-instruction tail
calls to the SRC ones.

They also have separate DMA channels, so one starting does not disturb the other.

## The driver plays out of the game's memory

Nothing is copied when a buffer is handed over. `sceAudioOutputBlocking` stores the pointer, and
the mixer walks it forward 64 samples at a time until it is spent, then clears it and signals.
`sceAudioGetChannelRestLen` counts down in steps of 64 for exactly that reason.

The emulator used to copy the whole buffer into a ring of samples at enqueue time, with the
volume already applied. That made a game rewriting a buffer it had already handed over
invisible, and it made the queue depth an emulator choice rather than a hardware fact.

## One buffer per channel

The driver's single enqueue point, which all four `sceAudioOutput*` functions reach, is:

```c
if (sampleCount == 0)     return SCE_ERROR_AUDIO_CHANNEL_NOT_INIT;
if (sampleAddress != 0)   return SCE_ERROR_AUDIO_CHANNEL_BUSY;
remaining = sampleCount;
sampleAddress = ptr;      // may be 0
return sampleCount;
```

`sceAudioOutput` and `sceAudioOutputPanned` return that result as it stands, so a second
non-blocking output is refused until the first buffer has finished.

The blocking pair adds a wait, but only for the first thread to ask:

```c
r = enqueue(...);
if (r != BUSY)         return r;
if (channel->waiting)  return BUSY;      // somebody else is already parked here
channel->waiting = 1;
r = wait for this channel's bit in the driver's event flag;
if (r < 0)             return r;         // waiting is left set! (not emulated)
retry the enqueue;
channel->waiting = 0;
```

The error path is a real firmware bug. The event flag wait fails at once with interrupts or
dispatch disabled (`800201a7`) or inside an interrupt (`80020064`), and the driver returns that
without clearing `waiting`. Nothing ever clears it after that, so the channel stays busy for
good, and `sceAudioChRelease` refuses to release it.

**The emulator returns the same errors but doesn't leave the flag set.** Whether the channel was
busy at that moment is a question of timing, and ours is close but not exact: emulating the bug
means a small scheduling difference could drop a channel for the rest of the game where hardware
wouldn't. No game can depend on losing a channel, so the risk only runs one way.
`tests/intr/waits` leaves the case out for this reason.

The SRC channel's wait fails the same way, and since the flag is never looked at, even a
completion that was already there isn't taken: the buffer stays armed and the error comes back.
That one is emulated, since nothing is lost - the buffer plays out as usual.

**Only one thread can be parked on a channel.** That single flag is why a game that runs a movie
thread and a sound-effect thread over one output gets sensible behavior on hardware and did not
in the emulator: the loser is told the channel is busy and skips its turn, where an emulator that
blocked it instead made the two threads alternate, halving the movie's audio rate. That is
https://github.com/hrydgard/ppsspp/issues/12888, and it is what
`tests/audio/blocking/contend` pins down.

The same flag locks out `sceAudioChRelease`, `sceAudioSetChannelDataLen` and
`sceAudioChangeChannelConfig`, which all return `SCE_ERROR_AUDIO_CHANNEL_BUSY` while a thread is
parked. `sceAudioChangeChannelVolume` does not check anything.

A null pointer is accepted and sets `remaining` without setting `sampleAddress`, so nothing
plays. That is the documented way to wait for a channel to drain, and it is the one case where
the two rest-length calls disagree - see below.

## Two buffers on the SRC channel

Channel 8 has two DMA descriptors, so two buffers can be in flight. The third caller is refused
outright and does not even get the chance to wait:

```c
if (!reserved)                    return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED;
if (both descriptors armed)       return SCE_ERROR_AUDIO_CHANNEL_BUSY;
arm one;
wait for one descriptor to retire;
return sampleCount;
```

Starting the DMA sets the same event flag bit that a retiring descriptor does, which is why the
first output after an idle stretch returns without waiting while every one after it waits exactly
one buffer. The emulator models that unclaimed completion with a flag on the channel.

## The two rest-length calls are not the same function twice

```c
sceAudioGetChannelRestLen(ch)     = remaining + (waiting ? sampleCount : 0)
sceAudioGetChannelRestLength(ch)  = (sampleAddress ? remaining : 0) + (waiting ? sampleCount : 0)
sceAudioOutput2GetRestSample()    = (armed descriptor count) * current sampleCount
```

Both channel versions count a parked thread's buffer as well as the one playing. They only
differ after an output with a null pointer, which leaves `remaining` set with no buffer, and the
hardware duly reports `0x400` from one and `0` from the other.

`sceAudioOutput2GetRestSample` reports in units of the *current* length, so after
`sceAudioOutput2ChangeLength(64)` a 4096-sample buffer armed earlier reads back as `0x40`.

## Where the emulator has to fake something

The mixer thread outranks whoever called, so on hardware the first 64 samples of a buffer are
consumed before the output call has returned - a channel reserved for exactly 64 samples is free
again immediately. The emulator's mixer is a timer event instead, so it re-phases that event to
the moment the mixer's DMA starts and runs one block right then. The SRC channel gets the phase
reset but no early read, since its DMA feeds the codec directly. Without this the answer to
`sceAudioGetChannelRestLen` right after an output would depend on where the timer happened to
be, and would differ from run to run.

That only happens when the DMA is actually idle. After a block that had samples in it, the DMA
is still playing that block out, so a buffer handed over within it is not read early: it waits
for the next block like any other. (Also from `tests/intr/waits`, where a second 64-sample buffer
is still in the channel when the next call looks.)

Two things are still approximate, both below one mix block:

- A descriptor retires when the DMA transfer finishes, which is slightly ahead of the audio
  being heard, so hardware frees an Output2 buffer about 100us earlier than the emulator does.
- The emulator only retires SRC buffers on block boundaries, so a buffer whose length is not a
  multiple of the block can be up to 1.5ms late. `audio/output2/frequency` and
  `audio/output2/rest` are in `tests_next` for this reason.

## Argument checking

Recorded here because several of these were guesses before, and because the *order* of the
checks is observable.

| function | checks, in order |
|---|---|
| `sceAudioChReserve` | the free-channel search runs 7 down to 0 and needs the channel both released and finished playing; bad channel `80260003`; already reserved `80260003`; count not a positive multiple of 64 up to 0xFFC0 `80260006`; format not 0 or 0x10 `80260007` |
| `sceAudioChRelease` | bad channel `80260003`; not reserved `80260008`; a thread parked `80260002`. Clears only the reservation - a buffer already playing plays out. |
| `sceAudioSetChannelDataLen` | bad channel `80260003`; **bad length `80260006`, before the reservation is looked at**; parked thread `80260002`; not reserved `80260001` |
| `sceAudioChangeChannelConfig` | bad channel `80260003`; parked thread or buffer in flight `80260002`; not reserved `80260008`; bad format `80260007` |
| `sceAudioChangeChannelVolume` | either side above 0xFFFF `8026000b`; bad channel `80260003`; **no reservation check**; a negative volume leaves that side alone |
| `sceAudioOutput`, `sceAudioOutputPanned` | each volume compared signed, so negatives pass and mean "leave unchanged" |
| `sceAudioOutputPannedBlocking` | the two volumes are ORed before comparing, so a negative one fails with `8026000b` |
| `sceAudioOutput2ChangeLength` | **length outside 17..4111 `80260006`, before the reservation**; not reserved `80260008` |
| `sceAudioSRCChReserve` | channels not 2 or 4 `80000104`; channels 4 `80000003`; count outside 17..4111 `80000104`; bad frequency `8026000a`; already reserved `80268002` |
| `sceAudioSRCChRelease` | not reserved `80260008`; a descriptor still armed `80268002` |

Accepted SRC frequencies are 8000, 11025, 12000, 16000, 22050, 24000, 32000 and 48000, plus
whatever the output is currently running at - which is how 44100 and 0 get through.

## sceAudioOneshotOutput

The odd one out: it plays a single buffer on a channel it never reserves. The channel stays
free as far as everything else is concerned, so `sceAudioChRelease` on it answers `80260008`,
and it returns to the pool by itself once the buffer runs out. It returns the channel number,
takes any positive sample count - alignment and the 0xFFC0 ceiling do not apply - and unlike
the other outputs a negative volume is an error rather than "leave it alone". There is no busy
check either: a second one-shot while the first is playing just replaces it.

No game is known to call it. It is implemented because the behavior turned out to be simple
once traced, not because anything needed it.

## sceVaudio is a third shape

`sceVaudio` reserves the same channel as Output2 and SRC, but its own release is not the same
call. It hands the channel a null pointer first - the drain idiom above - and only then does the
ordinary SRC release. So it blocks for one buffer and returns 0 where the other two would refuse
with `80268002`, and whatever was playing is played out rather than cut off. It also ignores its
own reservation flag and releases whatever holds the SRC channel, which is why
`tests/audio/sceaudio/reserve` gets away with what it calls the "wrong release".

`sceVaudioChReserve` marks vaudio reserved *before* delegating and does not undo that when the
delegate fails, so a caller that got `80268002` because Output2 held the channel is told
`80000021` next time round until a release clears it.

## What a call costs

Worth knowing because a game whose losing thread retries in a loop feels it directly.
`tests/audio/blocking/overhead` measures it in buckets:

| call | cost |
|---|---|
| Output2/SRC output, both descriptors already armed | 30-100us |
| Output2/SRC output, any other outcome, including an unreserved channel | over 100us |
| mixer output that is refused, blocking or not | under 10us |
| mixer output that starts the DMA | over 100us |

Every SRC output ends up querying the codec, which is the slow part; only the refused case gets
out before it. Mixer channels are the other way round - cheap unless the call is the one that
brings the DMA up.

## Savestates

`AudioChannel` is at section version 4 and the SRC channel has a section of its own. Anything
older stored a ring of already-mixed samples, which cannot be turned back into a buffer pointer
and a position, so loading one drops the pending audio and wakes any parked threads. That costs
a fraction of a second of silence on load and nothing else. Older states also carry the SRC
channel as a ninth entry in the channel array, which is read and discarded.

---

### SceGe (`docs/sceGe.md`)

# sceGe: how the PSP's display list queue behaves

This describes the behavior `GPU/GPUCommon.cpp` (the queue) and `Core/HLE/sceGe.cpp` (the
interrupt side) model. All of it is what a game can observe, and all of it is pinned down on a
real PSP by the tests in `pspautotests/tests/gpu/ge` (`queue`, `queue2`, `break`, `breakwait`,
`callbackstate`, `intrsuspend`, `enqueueparam`) and `pspautotests/tests/gpu/signals`.

The headline, because everything else follows from it: **the GE stops at every SIGNAL and every
FINISH, and it takes the interrupt to get it going again.** After a signal it carries on with the
same list. After a FINISH the game's finish callback runs *first*, and only then does the list
leave the queue and the next one start. Nothing the game calls in between makes the GE run.

## Lists and their states

Up to 64 lists can exist at once; with all of them in use, enqueueing fails with `0x80000022`.
Ids are opaque, and an id is reused once its list is gone.

| State | Meaning |
|---|---|
| NONE | Not a list. `sceGeListSync(id, 1)` gives `0x80000100`. |
| QUEUED | On the queue, not executing. |
| RUNNING | The GE is executing it, or is stalled on it. |
| COMPLETED | Hit its FINISH and is off the queue. The id can still be asked about, until `sceGeDrawSync(0)` turns every COMPLETED list into NONE. |
| PAUSED | Stopped by `sceGeBreak(0)`, by a PAUSE signal, or put at the head with `sceGeListEnQueueHead`. |

Besides its state, a list remembers whether it **has started executing** at some point
(`DisplayList::started`), which several checks below depend on, and whether something is pending
on it: a pause that was requested but hasn't been delivered, a pause that has been, or a SYNC.
`DisplayList::signal` holds that, in the same enum as the behaviors of the SIGNAL command, with
`PSP_GE_SIGNAL_HANDLER_SUSPEND` doubling as "paused, and the game has been told".

## Enqueueing

`sceGeListEnQueue` and `sceGeListEnQueueHead` differ only in where the list goes. In order:

1. The list, stall, context and stack addresses all have to be 4-byte aligned (`0x80000103`), and
   a stack depth of 256 or more is `0x80000104`.
2. Games built with SDK version `0x01FFFFFF` or below get a 32-entry stack they didn't ask for,
   and the next step can't fail for them. Otherwise the stack is whatever `PspGeListArgs` says,
   which may well be none.
3. **The new list is compared against every list on the queue:**
   - Same list address: `0x80000021`. What's compared is the address the other list was
     *enqueued* with, not where it has got to since, and the uncached mirror of an address
     counts as the same. After `sceGeBreak(0)` stops a list, it's the address it stopped at that
     collides instead, and the address it was enqueued with is free again.
   - Same stack, **and the other list has started**: `0x80000021`. Two lists that are both still
     waiting their turn can share a stack without complaint.
4. Then, depending:
   - *Tail, queue empty:* RUNNING, and the GE starts on it right away. If it has a context, the
     GE state is saved into it first.
   - *Tail, queue not empty:* QUEUED at the back.
   - *Head, queue not empty:* the current head has to be PAUSED, or it's `0x800001FE`. The new list
     becomes PAUSED at the head, and the old head QUEUED behind it.
   - *Head, queue empty:* PAUSED, and **not started**. It takes a `sceGeContinue` to run it.

"Every list on the queue" includes a list whose finish callback is running right now. So a
finish callback that enqueues its own list again gets `0x80000021` (on a newer SDK), and anything
else it enqueues is QUEUED behind it rather than started.

## SIGNAL

A SIGNAL followed by END stops the GE and raises an interrupt. It always applies to the list at
the head of the queue.

| Behavior | What happens |
|---|---|
| `HANDLER_SUSPEND` (1) | The signal callback runs **with the GE stopped**, then the GE carries on. For SDK <= `0x02000010`, the list reads as PAUSED for the duration. |
| `HANDLER_CONTINUE` (2) | The GE carries on, *then* the signal callback runs. |
| `HANDLER_PAUSE` (3) | The list becomes PAUSED **immediately**, the argument is remembered, and the GE carries on. No callback yet. See below. |
| `SYNC` (8) | The GE carries on, and the list's next FINISH is swallowed: no callback, no completion, it just keeps going. |
| `JUMP`, `CALL`, `RET`, and their relative and origin variants (0x10 - 0x16) | Done in software by the interrupt. CALL uses the list's stack; overflowing or underflowing it leaves the GE stopped. |
| 0x20 - 0x2F, 0x30, 0x38 | Set a texture or CLUT address relative to the list or to the offset address. PPSSPP doesn't implement these. |

PPSSPP treats `HANDLER_CONTINUE` like `HANDLER_SUSPEND`: the GE waits for the callback. On
hardware the two run side by side, so a callback that looks at the GE finds it further along,
which is all `gpu/signals/continue` fails on. Waiting is the safe side to err on - a callback
that prepares something the list is about to use still gets there first.

Callbacks get the low 16 bits of the SIGNAL (or FINISH) command, the argument they were
registered with, and - for SDK > `0x02000010` only, otherwise 0 - the address after the END.

### The pause window

Between a PAUSE signal and the FINISH that delivers it, a list is in an odd place. The GE is
still executing it, but:

- `sceGeListSync(id, 1)` already says PAUSED.
- `sceGeContinue` returns `0x80000021`. So does `sceGeBreak(0)`.
- `sceGeListUpdateStallAddr` returns 0 but **has no effect on the GE**, because only a RUNNING
  list's stall address does. So a list that stalls inside the window is stuck for good, and
  `sceGeBreak(1)` is the only way out. Games therefore never put a stall there, and neither
  should a test that wants to finish.

When the FINISH arrives, the **signal** callback (not the finish one) is called, with the
argument from the PAUSE signal. The list stays PAUSED at the head of the queue, and nothing
behind it runs until the game calls `sceGeContinue`, which now works.

## FINISH

A FINISH followed by END stops the GE and raises an interrupt. If a SYNC or a pause is pending on
the list, see above. Otherwise, in this order:

1. The list becomes COMPLETED. It is still at the head of the queue.
2. The **finish callback** runs. The GE is idle, and its state is still what the list left it as:
   a context isn't restored until the next step. From in here, `sceGeListSync` on the next list
   says QUEUED, `sceGeDrawSync(1)` says DRAWING if there is a next list and COMPLETED if not,
   `sceGeListDeQueue` on the finished list says `0x80000021`, and `sceGeListUpdateStallAddr` on
   it says `0x80000020`.
3. If the list had a context, the GE state is restored from it.
4. The list leaves the queue.
5. The new head, if there is one and it isn't PAUSED, is started. If it has a context and hasn't
   started before, the GE state is saved into it.
6. If the queue is empty instead, threads waiting in `sceGeDrawSync(0)` are woken.
7. Finally the threads waiting in `sceGeListSync(id, 0)` for the finished list are woken - after
   the ones from step 6, which shows when they have the same priority.

If a SIGNAL and a FINISH are both pending by the time the interrupt runs, only the FINISH counts.

### Is the GE busy?

`sceGeSaveContext` and `sceGeRestoreContext` fail with -1 while the GE is *executing*, and that's
all they go by - not whether lists are queued. A GE waiting at a stall address counts as
executing. One that has stopped at a SIGNAL or FINISH doesn't, so both work from a finish
callback and from a `HANDLER_SUSPEND` callback, however much is queued behind. From a
`HANDLER_CONTINUE` callback they fail, as the GE is off again by then. `GPUCommon::BusyDrawing()`.

### While the interrupt can't be taken

All of the above happens in the interrupt, so with interrupts off (`sceKernelCpuSuspendIntr`)
the queue simply stands still at the first FINISH: that list keeps reading as DRAWING, the next
one as QUEUED, and no callback runs. It all catches up, in order, when interrupts come back.
Suspending thread dispatch (`sceKernelSuspendDispatchThread`) doesn't hold any of it up.

### How PPSSPP does this

PPSSPP doesn't run the GE in parallel with the CPU. `ProcessDLQueue()` executes a list all the way
to whatever stops it - a stall, a SIGNAL, a FINISH - adding up how long that would have taken, and
schedules the interrupt for that point in the future. That's the accepted inaccuracy, and it means
there's a stretch where a list has reached its FINISH but the game hasn't been told, which doesn't
exist on hardware. `DisplayList::pendingInterrupt` marks it.

What keeps the *sequencing* right in spite of that is one rule in `ProcessDLQueue()`: **while the
head of the queue has an interrupt pending, nothing runs**, whoever asks - an enqueue, a stall
update or a continue. The finished list stays on the queue until `InterruptEnd()`, which takes it
off and lets the next one go, as in steps 4 to 7 above. Before that rule, the next list ran right
away and the finished one left the queue at once, so a finish callback saw an empty queue: a list
it enqueued was started instead of queued, and then couldn't be dequeued.

Three places deliberately treat a list with its FINISH pending as already gone, since on hardware it
would be: the duplicate check when enqueueing (Exit enqueues the same list again right after it
ends), `drawCompleteTicks`, which is set when the last list reaches its FINISH rather than
when the interrupt is delivered, so that a `sceGeDrawSync(0)` in between doesn't wait for nothing,
and `sceGeBreak(1)`. That one throws away interrupts that have been *raised* and couldn't be taken,
as hardware does, but not the ones we simply haven't raised yet: a game that breaks right after
its last list and then waits for what the finish callback signals had that callback long ago.

## Waiting

The two waiting calls, `sceGeListSync(id, 0)` and `sceGeDrawSync(0)`, refuse outright where a
thread can't wait, *even if there is nothing to wait for*: `0x80020064` from a GE callback or
any other interrupt, `0x800201A7` with dispatch or interrupts suspended.

`sceGeListSync(id, 0)` doesn't care what state the list is in. It waits for that id to complete or
be dequeued, and for an id that isn't pending - never used, or done - it returns 0 at once.

`sceGeListSync(id, 1)`:

| List state | Result |
|---|---|
| RUNNING | STALLING (3) if the GE is at the stall address, else DRAWING (2) |
| QUEUED | PAUSED (4) if it has started before (an EnQueueHead bumped it), else QUEUED (1) |
| COMPLETED | 0 |
| PAUSED | 4 |
| NONE | `0x80000100` |

`sceGeDrawSync(0)` waits for the queue to drain, and then **turns every COMPLETED list into
NONE**. Nothing else does, short of `sceGeBreak(1)`. `sceGeDrawSync(1)` looks at the head of the
queue, or the one after if the head is COMPLETED, and answers STALLING or DRAWING depending on
whether the GE is at that list's stall address - which it can't be if it isn't on that list.

## Break, continue, dequeue, stall

`sceGeBreak(0)`, by the state of the head of the queue:

| State | Result |
|---|---|
| RUNNING | The GE is stopped, and the list becomes PAUSED, remembering where it got to. If that was in the middle of a SIGNAL/END or FINISH/END pair, it resumes from the start of the pair. Returns the id. |
| QUEUED | Becomes PAUSED, id returned. |
| PAUSED | `0x80000021` for SDK <= `0x02000010` or inside the pause window, otherwise `0x80000020`. |
| no list | `0x80000020` |

**`sceGeBreak(1)` throws the whole queue away** and resets the GE: every list becomes NONE. An
interrupt that was raised but not taken yet goes too, so a list that reached its FINISH just
before never gets its finish callback. It doesn't wake anybody. A thread in `sceGeListSync(id, 0)` or `sceGeDrawSync(0)` stays there - until
a *new* list happens to get the same id and completes, or the queue next drains, at which point
they return 0 as if nothing had happened.

`sceGeContinue`: with no list, 0. A PAUSED head resumes where it left off (`0x80000021` inside the
pause window). On a RUNNING head, `0x80000020`; anything else, `0x80000004`. Both of those are -1
for SDK <= `0x01FFFFFF`.

`sceGeListDeQueue`: `0x80000100` for NONE, `0x80000021` for a list that has started - which
includes COMPLETED ones, until `sceGeDrawSync(0)` - and otherwise the list leaves the queue,
becomes NONE, and threads waiting for it are woken. Other lists are left alone: if that was the
last one queued, COMPLETED lists stay COMPLETED, and nobody in `sceGeDrawSync(0)` is woken.

`sceGeListUpdateStallAddr`: takes effect only if the list is RUNNING, and a GE stalled on that
list resumes by itself. For QUEUED and PAUSED lists it's just remembered for when they run.
COMPLETED is `0x80000020`.

One thing not to do, in a test or anywhere: `sceGeBreak(0)` from inside a `HANDLER_SUSPEND`
callback with an SDK version above `0x02000010`. With an older one the list is PAUSED for the
duration and the break is refused (`0x80000021`). With a newer one it goes through, the GE is
restarted anyway when the callback returns, and the PSP hangs.

## Starting another executable

`sceKernelLoadExec` restarts the kernel, and the GE driver with it: the new executable finds an
empty queue, whatever the old one left on it, and a GE with every register and matrix set to
zero, which is how the driver starts up - the same as a program that was booted directly.
`GPUCommon::Reinitialize()` is that, and has to leave nothing behind - not the lists, not the
queue of their ids, and not the old program's GE state. (This part isn't confirmed by a test:
whatever runs after the restart can't report back through PSPLink.) It used to keep the
queue, so the old executable's ids came back as lists with no state and a pc of 0, waiting their
turn behind the new executable's first list, where they'd block everything. Crazy Taxi: Fare
Wars, which is a launcher for its two games, stopped at a black screen that way (#19894).

## Known leftovers in PPSSPP

- `gpu/signals/jumps` and `gpu/signals/simple` ask for a list's state the moment
  `sceGeListUpdateStallAddr` returns. On hardware the list is long done by then, callbacks and
  all. For PPSSPP it has reached its FINISH, but the interrupt is still to come.

- `IgnoreEnqueue` in `compat.ini` (Metal Gear Acid 2, #10906) skips the stack check. It was most
  likely hitting the not-started case, which no longer fails. If the game is fine without it,
  remove it.
- The GE has one stall address, which isn't the same thing as the one each list remembers:
  `sceGeListUpdateStallAddr` always updates the list's, but only a RUNNING list's update reaches
  the GE. PPSSPP has just `DisplayList::stall` for both. The pause window gets away with that,
  since a PAUSED list doesn't run. What doesn't is an old-SDK game updating the stall from inside
  a `HANDLER_SUSPEND` callback, where the list reads as PAUSED: on hardware the GE then stops at
  the *old* stall address until the next update from a thread, while reading as DRAWING rather
  than STALLING. PPSSPP lets it run on. `gpu/signals/handlercalls` shows it, and fails on it.
  Fixing it means giving `GPUCommon` a stall address of its own for execution, which touches the
  fast paths in `GPUCommonHW.cpp` and both GE debuggers.
- `sceGeBreak(0)` doesn't back up over a half-done SIGNAL or FINISH pair. With lists executing in
  one go, it's rarely in the middle of one.
- Signal CALL and RET save and restore only the pc, the offset address and the base address. On
  hardware the GE's own return addresses are part of it as well, and its CALL depth, which PPSSPP
  gets right by counting plain CALLs only back to the last signal CALL. The GE nests plain CALLs
  just two deep and skips a third (`gpu/callstack/depth`, and Soul Calibur: Broken Destiny, #7395).
- On hardware, a `sceGeListEnQueueHead` that fails with `0x800001FE` still uses up one of the 64
  lists until the next `sceGeBreak(1)`. Not emulated, and not worth it.

---

### Shader Compilation (`docs/shader-compilation.md`)

# Shader compilation stutter

Where the time goes when a game compiles its shaders and pipelines, mainly on the Mac (Vulkan through
MoltenVK), and how to measure it. Measured in October 2026 on an M1 with MoltenVK 1.4.2.

## The path a shader takes

1. The shader generators (`GPU/Common/VertexShaderGenerator.cpp`, `FragmentShaderGenerator.cpp`)
   write GLSL from a `VShaderID` / `FShaderID`.
2. glslang compiles it to SPIR-V (`GLSLtoSPV()` in `Common/GPU/Vulkan/VulkanContext.cpp`), on a
   dedicated thread per shader (`CompileShaderModuleAsync()` in `GPU/Vulkan/ShaderManagerVulkan.cpp`).
3. `vkCreateGraphicsPipelines()` runs on the thread pool, which `VulkanRenderManager::CompileThreadFunc()`
   feeds, one task per vertex/fragment shader pair. On MoltenVK this is where the rest happens:
   SPIRV-Cross turns SPIR-V into MSL, Metal compiles the MSL into an `MTLLibrary`, and then builds the
   `MTLRenderPipelineState`. MoltenVK's `vkCreateShaderModule()` only copies the SPIR-V.
4. The render thread waits for the pipeline in `VulkanQueueRunner` (`BlockUntilReady()`) when it
   reaches the first draw that uses it. That wait is the stutter.

## Where the time goes

Wipeout Pure from a savestate, starting with no PPSSPP cache and an empty Metal cache. MoltenVK's own
counters (see below):

| Stage | Time each | Count |
|---|---|---|
| glslang, GLSL to SPIR-V | ~1 ms | 27 |
| SPIRV-Cross, SPIR-V to MSL | 0.5 ms | 40 |
| Metal, MSL to `MTLLibrary` | 85-110 ms (max ~470) | 43 |
| Metal, `MTLFunction`s to a pipeline state | 5 ms | 63 |

Nearly all the cost is Metal compiling MSL, and it happens about once per distinct shader, not once
per pipeline. Pipelines that differ only in blend, cull, depth or stencil state cost about 5 ms each
once their shaders' libraries exist. On the Mac, then:

- Speeding up the shader generators or glslang gains nothing worth having.
- Cutting the pipeline count (more dynamic state, e.g. cull mode and depth/stencil through
  `VK_EXT_extended_dynamic_state`) helps little. In this run it would have taken 58 pipelines down to
  43 that differ in anything Metal bakes in, and the expensive step doesn't repeat for those anyway.
- Fewer distinct shaders (moving ID bits into uniforms) is the lever that matters, with the runtime
  cost that ubershaders bring, mostly in fragment shaders.

### The shared `VkPipelineCache`

We used to create every pipeline with the same `VkPipelineCache`, and MoltenVK serializes the MSL
compiles of pipelines that share one: its "retrieve shader library from the cache" time, which includes
waiting for another thread's compile, averaged 142 ms. The 3.7 s of compiling blocked the render thread
for 2.9 s in total, so there was almost no parallelism. The cache is only held in memory (we don't save
its data), and on MoltenVK all it saves is the SPIR-V to MSL step, so on Apple platforms we now pass
`VK_NULL_HANDLE` (`CreateVulkanPipeline()` in `GPU/Vulkan/PipelineManagerVulkan.cpp`).

| Cold Wipeout Pure, two runs each | With the cache | Without |
|---|---|---|
| Render thread blocked on pipelines, total | 2.9 s | 1.2 s |
| Longest single stall | 1.0-1.1 s | 0.3 s |
| Average wait inside MoltenVK's cache lookup | 142 ms | 11 ms |

`MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION=1` made no measurable difference.

## The caches

- **PPSSPP's per-game cache**, `PSP/SYSTEM/CACHE/<DISCID>.vkshadercache`. It holds the shader IDs and
  pipeline keys the game used, plus their SPIR-V, and `GPU_Vulkan::LoadCache()` recompiles all of it
  while the game boots. With it, Wipeout's 62 pipelines took 140 ms in total.
- **macOS's Metal cache**, keyed by the MSL source. With it warm, even a run *without* PPSSPP's cache
  took about 100 ms for all 62 pipelines. So on the Mac, a second boot is cheap with or without our
  cache, and a shader one game compiled is cheap in the next game too. Only the first time a shader
  is ever seen (and possibly after an OS or app update) is it slow.

### How game-specific are the shaders?

Less than expected. Across the 37 `.vkshadercache` files of one Mac's memory stick (at the current
`CACHE_VERSION`), for the seven biggest:

| Game | VS | Also in another game | FS | Also in another game |
|---|---|---|---|---|
| UCES00002 (Ridge Racer) | 12 | 92% | 47 | 51% |
| UCES01250 | 13 | 85% | 36 | 67% |
| UCES00001 (Wipeout Pure) | 14 | 93% | 25 | 80% |
| UCES00465 | 12 | 100% | 22 | 86% |
| UCES00842 (God of War: Chains of Olympus) | 11 | 82% | 21 | 76% |
| ULES00502 (GTA: Vice City Stories) | 10 | 90% | 20 | 70% |
| ULES01376 (Tekken 6) | 11 | 73% | 20 | 65% |

All 37 games together use only 48 distinct vertex shader IDs. The IDs depend on the device's GPU
features and the user's settings, and some of those caches come from short runs, so a list shipped with
the app would match less well than one gathered on the device. A per-device list of shaders seen in
several games, precompiled at boot, looks worth trying.

## Ideas not done yet

- Don't block the render thread on a pipeline that isn't ready: skip the draw, or keep the previous
  pipeline, for the few frames until it is. Visible (objects pop in), so probably an option, but it
  would remove the remaining stalls on every backend.
- Precompile cross-game shaders at boot, from the list above.
- A native Metal backend could use `MTLBinaryArchive`. Not measured. See
  [metal-backend.md](metal-backend.md).

## Measuring

`--shader-cache=false` turns off loading and saving PPSSPP's per-game cache for one run (headless or
the app; it isn't saved to the ini). On the Mac that isn't a cold start, since Metal's cache still
hits. Headless is an unbundled binary and shares Metal's cache with other command-line tools, in
`$(getconf DARWIN_USER_CACHE_DIR)com.apple.metal/`. For a real cold start, move the GPU's folders
there aside (on the M1, `16777235_322` and `32024`, the two big ones) and put them back afterwards.
`MTL_SHADER_CACHE_DISABLE` and similar environment variables don't turn the cache off.

```bash
build-release/PPSSPPHeadless "$HOME/PSP/Wipeout Pure.cso" --memstick=$HOME/.config/ppsspp \
    --state=$HOME/.config/ppsspp/PSP/PPSSPP_STATE/UCES00001_1.01_0.ppst \
    --graphics=vulkan --timeout-wall=30 --timeout-emulated=20 --log --shader-cache=false
```

- Each pipeline's creation time is logged at INFO level, as `Pipeline (x/N) time on ...`.
- MoltenVK's breakdown: set `MVK_CONFIG_PERFORMANCE_TRACKING=1` and
  `MVK_CONFIG_ACTIVITY_PERFORMANCE_LOGGING_STYLE=2`. The summary prints when the device is destroyed,
  at the end of the run.
- The render thread's stalls aren't logged. Time `BlockUntilReady()` in `VulkanQueueRunner.cpp`
  temporarily to see them; the sum of pipeline times overstates the stall, since compiles overlap.
- Run each configuration at least twice. Single cold runs varied by about 15%.

---

### Translations (`docs/translations.md`)

# Translated UI strings (assets/lang)

**When implementing new UI, translations come last, in their own commit after everything else is
done.** Write the English strings, get the feature built and working, commit that - then stop and ask
the user to check the English wording before translating anything. The English string is what all ~47
languages get derived from, so rewording it afterwards means redoing the whole sweep.

One .ini file per language, keyed by section and key against `assets/lang/en_US.ini`. **Don't hand-edit
the ~47 files**, and don't run the AI commands in `Tools/langtool` either - you can read the call site,
which its fixed prompt can't. Do the translating yourself and let the tool do the file surgery. Run it
from `Tools/langtool`:

1. Work out what the string actually means before translating it: find where it's used in the C++,
   what any `%1`/`%d` placeholders get substituted with, how long it can be without breaking the
   layout, and how neighboring keys are already phrased in each language (that's your style guide).
2. Write a scratch file, one line per language, named after the ini file minus the extension, with
   the English string under `en_US`:
   ```ini
   [Single]
   en_US = Test string
   sv_SE = Teststräng
   lt-LT = Testeilutė
   ```
   For a key that already exists, the `en_US` line has to be the current English text character for
   character - it overwrites `en_US.ini` like any other language, so a stray reword there quietly
   changes the string all the others were just translated from. Diff `en_US.ini` afterwards to check
   it didn't move.
   No trailing `# comments` on those lines, they'd end up inside the translation. Placeholders have to
   survive verbatim. If you don't know a language well enough, leave it out - step 4 gives it the
   English string as a placeholder, which is much better than a confident guess. If a language
   deliberately keeps the English string (a term like "Vsync" that isn't
   translated), do include it with the English text - it gets written with a `# same as English`
   comment, which stops langtool from trying to translate it again on every later run.
3. `cargo run -- import-single <scratch-file> <Section> "<Key>"` writes them all in, including a new
   key in en_US.ini, tagged `# AI translated` except for the en_US line, which is the string the rest
   were translated from. Note it overwrites existing values for that key, so take care with keys that
   already have human translations, and that the section has to exist already - langtool won't create
   one.
4. `cargo run -- add-new-key <Section> "<Key>"` gives every language you skipped the English string
   as a placeholder - `Key = Key`, with no `# same as English` marker, so it still reads as
   outstanding work - and leaves the translated ones untouched. Always do this: every language file
   should end up with the key. A missing key falls back to English at runtime anyway, but in the
   files it's indistinguishable from a translated one, so translators can't see what's left.
   `copy-missing-lines` does the same job across every key in the repo at once, which is a different
   job - it'll bury a two-string change under several hundred lines of unrelated housekeeping.
5. `cargo run -- validate` at the end, always. It checks that placeholders survived and exits
   non-zero if anything is off.

The other mechanical jobs (renaming and moving keys, sorting sections) are langtool commands too -
prefer them over editing the ini files by hand.

---

### Emu Api Protocol (`docs/emu-api/protocol.md`)

# Emulator API - raw protocol

This documents the raw `sceIoDevctl`-based protocol behind PPSSPP's emulator API. Most homebrew
should just use the C wrapper in [`ppsspp_emu_api.h`](ppsspp_emu_api.h) instead (see the
[README](README.md) and the [website docs](https://www.ppsspp.org/docs/development/ppsspp-internals/emu-api/)
for that) - read this page if you're extending the wrapper, writing a binding for another language,
or just curious how it works under the hood.

Important: None of this will work on the real PSP! Always check `IS_EMULATOR` (see below) before
relying on any of it, and keep a working fallback path for real hardware.

## How it works

The whole API is exposed through a single, existing PSP syscall: `sceIoDevctl`. PPSSPP recognizes
two special, fake device names that don't correspond to any real device:

* `"emulator:"`
* `"kemulator:"`

Both names are currently handled identically (there's no user/kernel distinction enforced), so
either works from a user-mode homebrew app. The implementation lives in `sceIoDevctl()` in
[`Core/HLE/sceIo.cpp`](https://github.com/hrydgard/ppsspp/blob/master/Core/HLE/sceIo.cpp) - search
for `"emulator:"` if you want to see exactly what each command does, or if you're adding a new one.

The standard `sceIoDevctl` signature is used, just with PPSSPP-specific `cmd` numbers and
argument/output block layouts:

```c
int sceIoDevctl(const char *devicename, unsigned int cmd, void *indata, int inlen, void *outdata, int outlen);
```

* `devicename` - `"emulator:"` or `"kemulator:"`.
* `cmd` - one of the `EMULATOR_DEVCTL__*` values below.
* `indata`/`inlen` - input block, meaning depends on `cmd`. Some commands instead just check whether `indata` is NULL/non-NULL as a boolean flag.
* `outdata`/`outlen` - output block, meaning depends on `cmd`. Most commands write a single `u32` or `float` here.

If `cmd` doesn't match any known command, `sceIoDevctl` returns an error (`UNKNOWN PARAMETERS`)
rather than crashing, so probing for support is safe.

## Commands

| Command | Value | Direction | Purpose |
|---|---|---|---|
| `EMULATOR_DEVCTL__GET_HAS_DISPLAY` | 1 | out: `u32` | Writes 1 if there's a real display (normal PPSSPP), 0 if running headless (`PPSSPPHeadless`). Useful to skip presentation/vblank-dependent work when there's nothing to show. |
| `EMULATOR_DEVCTL__SEND_OUTPUT` | 2 | in: bytes | Sends a raw block of text straight to PPSSPP's debug/log output (and to headless's collected output buffer, if any). Handy for logging from homebrew without going through `sceIoWrite` to a real file. |
| `EMULATOR_DEVCTL__IS_EMULATOR` | 3 | out: `u32` | Writes 1. This is the one to call first: if the `sceIoDevctl` call itself fails, you're not running under PPSSPP (or a build that doesn't implement this API), and none of the rest of this page applies. |
| `EMULATOR_DEVCTL__VERIFY_STATE` | 4 | none | Asks PPSSPP to do an internal savestate round-trip (save to memory, then verify it reads back correctly) as a consistency check. Runs asynchronously - it doesn't report the pass/fail result back to your code, it just gets logged on the PPSSPP side. Mainly useful for automated testing of the emulator itself. |
| `EMULATOR_DEVCTL__EMIT_SCREENSHOT` | 0x20 | none | Grabs the current framebuffer and delivers it through PPSSPP's internal debug-screenshot hook, which is used by things like the pspautotests/frametest infrastructure to collect result images. Not useful as a general "save a screenshot to memstick" feature - it doesn't write a file. |
| `EMULATOR_DEVCTL__TOGGLE_FASTFORWARD` | 0x30 | in: bool (NULL/non-NULL) | Turns PPSSPP's fast-forward mode on (non-NULL `indata`) or off (NULL). |
| `EMULATOR_DEVCTL__GET_ASPECT_RATIO` | 0x31 | out: `float` | Writes the display's current aspect ratio. Only correct in landscape orientation right now. |
| `EMULATOR_DEVCTL__GET_SCALE` | 0x32 | out: `float` | Writes the current display scale factor. Only correct in landscape orientation right now. |
| `EMULATOR_DEVCTL__GET_AXIS` | 0x33 | in: axis index (as `indata` value, not a pointer), out: `float` | Reads an analog axis value that a PPSSPP-side input plugin has injected (see below), by `JOYSTICK_AXIS_*` index. |
| `EMULATOR_DEVCTL__GET_VKEY` | 0x34 | in: key code (as `indata` value, not a pointer), out: `u8` | Reads whether a virtual key that a PPSSPP-side input plugin has injected is currently pressed, by PPSSPP's internal key code (see below). |

A couple of notes on quirks that are easy to trip over:

* For `GET_AXIS` and `GET_VKEY`, the "input" isn't the `indata` buffer contents - it's the `indata` pointer value itself, used directly as an integer index. This matches how the current PPSSPP implementation reads it (`argAddr` is compared against the axis/key range and used directly), so pass the index as if it were a pointer, e.g. `sceIoDevctl("emulator:", EMULATOR_DEVCTL__GET_AXIS, (void *)JOYSTICK_AXIS_X, 0, &value, sizeof(value))`.
* `GET_AXIS` and `GET_VKEY` don't read normal controller input (`sceCtrl*` already does that) - they read state from PPSSPP's HLE plugin system, i.e. values that a native PPSSPP-side plugin PRX has explicitly set for your homebrew to pick up. If no plugin is active, expect these to just come back as 0/unpressed.
* The axis index matches PPSSPP's internal `JOYSTICK_AXIS_*` enum (`Common/Input/KeyCodes.h`), and the key code matches PPSSPP's internal `NKCODE_*` enum (same file), not any PSP SDK enum. These mostly mirror Android's key/axis codes. A handful of the more useful ones: `NKCODE_DPAD_UP/DOWN/LEFT/RIGHT`, `NKCODE_BUTTON_CROSS/CIRCLE/SQUARE/TRIANGLE`, `JOYSTICK_AXIS_X/Y`. See the header for the full list if you need something more obscure.
* You'll sometimes see an `EMULATOR_DEVCTL__SEND_CTRLDATA` (0x10) constant referenced in older test code. It is not currently implemented by PPSSPP - calling it just gets you the generic "unknown parameters" error. Don't rely on it.

## Raw example

```c
#include <pspiofilemgr.h>

#define EMULATOR_DEVCTL__IS_EMULATOR     3
#define EMULATOR_DEVCTL__SEND_OUTPUT     2

int runningOnPPSSPP = sceIoDevctl("emulator:", EMULATOR_DEVCTL__IS_EMULATOR, NULL, 0, NULL, 0) == 0;

if (runningOnPPSSPP) {
    const char *msg = "Hello from homebrew, running under PPSSPP!\n";
    sceIoDevctl("emulator:", EMULATOR_DEVCTL__SEND_OUTPUT, (void *)msg, strlen(msg), NULL, 0);
}
```

---

--- METRICS ---
- Files Extracted: 39
- Estimated Token Budget: ~107302 tokens
- Canonical Reference: https://codewiki.google/github.com/hrydgard/ppsspp
