webstation-broker
DeveloperPython referenceemulators

emulators.ppsspp

PPSSPP (Sony PSP) launcher: ROM resolution, ini patching, and hotkey save states.

PPSSPP (Sony PSP) launcher: ROM resolution, ini patching, and hotkey save states.

PPSSPP has no control socket. Everything the broker needs pinned lives in two inis, both patched before every launch. ppsspp.ini gets FirstRun and CheckForNewVersion so a freshly seeded /config never opens the setup wizard or an update toast behind a session nobody can click into, and StateSlot so every save/load hotkey lands on the broker's one working slot without ever cycling it. controls.ini gets the bracket keys added to Save State and Load State: F1-F12 never reach PPSSPP through this container's streaming/input stack (confirmed empirically, including with a real keyboard through the user's own desktop session), so the brackets go in on every launch rather than being trusted to survive from a one-off manual bind through PPSSPP's own remap UI. They are merged into the action's existing comma-separated mapping list, not written over it, so whatever else the player bound to those actions still works. Both files are written with a leading UTF-8 BOM; the patcher has to strip it on read and put it back on write; a naive line scan would loosen the BOM onto the first [section] line and never match it.

Save states have no boot-time load flag, unlike Dolphin's -s, so a resume always goes through the deferred hotkey load below rather than loading before the window exists to fail into.

attributelog
= logging.getLogger(__name__)
attributeROM_ROOT
= Path(os.environ.get('ROM_ROOT', '/romm'))

Root of the RomM library mount (env ROM_ROOT, default /romm).

A resolved ROM must sit under it; candidates resolving outside are discarded.

attributeCONFIG_DIR
= xdg_config_dir('ppsspp')

PPSSPP's config root, which also holds the emulated memory stick.

Not configurable, and deliberately: nothing on PPSSPP's command line names it, so an override would move only the copy the broker reads and writes. Since the save data and states live under it too, that would point the dump and restore at a tree PPSSPP does not write to. launch exports the root this resolved to instead, which is an agreement the two cannot fall out of.

attributePSP_DIR
= CONFIG_DIR / 'PSP'

The emulated memory stick root, holding save data, states and the system inis.

attributeSYSTEM_DIR
= PSP_DIR / 'SYSTEM'

Directory holding ppsspp.ini and controls.ini.

attributeINI_PATH
= SYSTEM_DIR / 'ppsspp.ini'

The main ini the broker patches before every launch.

attributeCONTROLS_INI_PATH
= SYSTEM_DIR / 'controls.ini'

The control mapping ini the broker patches before every launch.

attributeSTATE_DIR
= PSP_DIR / 'PPSSPP_STATE'

Directory PPSSPP writes its .ppst save states and their .jpg screenshots into.

attributePPSSPP_LOG_PATH
= Path(os.environ.get('PPSSPP_LOG_PATH', '/config/ppsspp.log'))

Log file the broker tails for this emulator (env PPSSPP_LOG_PATH, default /config/ppsspp.log).

attributeSTATE_SLOT
= int(os.environ.get('PPSSPP_STATE_SLOT', '1'))

The one slot the broker works in (env PPSSPP_STATE_SLOT, default 1).

RomM holds the library of states, so a requested slot resolves to this one and the routes echo the effective slot.

attributeSAVE_KEY
= 'bracketleft'

xdotool key for saving the working slot.

F1-F12 never reach PPSSPP through this container's streaming/input stack (confirmed empirically: bound and unbound alike, nothing was ever delivered), so the working hotkeys are the bracket keys instead. The device-1 code is NKCODE (device 1 is the keyboard; device 10 entries elsewhere in this file are the gamepad's own, unrelated numbering) per PPSSPP's Qt/NKCodeFromQt.h, which maps Qt::Key_BracketLeft/Right to NKCODE_LEFT_BRACKET/RIGHT_BRACKET (71/72, Common/Input/KeyCodes.h), not the 132/134 an earlier remap-UI capture recorded, which traced back to the same broken injection path this binding works around.

attributeLOAD_KEY
= 'bracketright'

xdotool key for loading the working slot; see SAVE_KEY for why it is a bracket key.

attributeSTATE_WAIT
= float(os.environ.get('PPSSPP_STATE_WAIT', '20.0'))

Seconds a save state has to land on disk after the save hotkey (env PPSSPP_STATE_WAIT, default 20).

attributeSTATE_STABLE
= float(os.environ.get('PPSSPP_STATE_STABLE', '1.5'))

Seconds a state's size and mtime must both hold still before the write counts as finished.

From env PPSSPP_STATE_STABLE, default 1.5. Long enough that a stalled write is not mistaken for a finished one, short enough to stay well inside STATE_WAIT.

attributeLOAD_WAIT
= float(os.environ.get('PPSSPP_LOAD_WAIT', '20.0'))

Seconds PPSSPP has to read the state back after the load hotkey (env PPSSPP_LOAD_WAIT, default 20).

attributeLOAD_SETTLE
= float(os.environ.get('PPSSPP_LOAD_SETTLE', '2.0'))

Seconds a load gets to deserialize once the state file has been read.

From env PPSSPP_LOAD_SETTLE, default 2. The deserialize itself is unobservable from outside the process, so this is the one bounded guess in the load path.

attributeRESUME_LOAD_WAIT
= float(os.environ.get('PPSSPP_RESUME_LOAD_WAIT', '90.0'))

Seconds a deferred resume has to find both a state file and a game window (env PPSSPP_RESUME_LOAD_WAIT).

Defaults to 90. One budget covers both: a resume state is usually already on disk from the save archive, so nearly all of it goes on the boot.

attributeRESUME_LOAD_SETTLE
= float(os.environ.get('PPSSPP_RESUME_LOAD_SETTLE', '12.0'))

Seconds to wait after the game window is up before the load hotkey goes in.

From env PPSSPP_RESUME_LOAD_SETTLE, default 12. PPSSPP titles its window with the game as soon as the disc is mounted, but keeps loading and registering HLE module state well past that point, and a load that lands before the event table is complete leaves the core reporting an unregistered event and a black screen. This is not the wait for the window itself (that is polled for): it is the margin between a window saying a game is running and a core that can actually be loaded into.

attributeROM_EXTENSIONS
= ('.chd', '.cso', '.pbp', '.iso', '.elf', '.prx')

All formats PPSSPP boots directly, best first.

Best means most likely to be the verified, compressed copy; a folder holding several candidates picks by this order. PSP has no multi-disc titles, so there is no disc number to rank on.

attributeCACHE_DIR
= Path(os.environ.get('PPSSPP_CACHE_DIR', str(xdg_data_dir('ppsspp') / 'extracted')))

Where archived ROMs are extracted and booted from (env PPSSPP_CACHE_DIR).

Defaults to $XDG_DATA_HOME/ppsspp/extracted.

An extraction is kept and reused by every later launch of the same archive.

attributeCACHE_MAX_GB
= float(os.environ.get('PPSSPP_CACHE_MAX_GB', '20'))

Cap on the extraction cache in GB (env PPSSPP_CACHE_MAX_GB, default 20).

The least recently booted ROMs are evicted to make room for a new one.

attributeARCHIVE_LIST_TIMEOUT
= 60.0

Seconds 7z or unrar gets to list an archive's members before it is considered hung.