emulators.retroarch
RetroArch launcher for any libretro core.
RetroArch launcher for any libretro core.
Covers the platform to core mapping, the buildbot core download, and the stdin command protocol for save, state and quit.
This is the general purpose provider: instead of one class per emulator we keep a map from RomM platform slug to libretro core.
RetroArch is the user's own desktop app, so we never touch its config file.
The launch uses --appendconfig <broker.cfg> to layer in only the
protocol-required settings on top of the user's config: the stdin command
interface, and the broker-managed save directories so the save archive logic
tracks real files.
Control plane: RetroArch's stdin command interface (config key
stdin_cmd_enable) reads newline-delimited commands from stdin and writes
replies to stdout. Commands:
SAVE_STATE: no reply; dispatchesCMD_EVENT_SAVE_STATEfrom the runloop (the save-state hotkey path).LOAD_STATE_SLOT <n>: bare echoLOAD_STATE_SLOT <n>(no success bit).STATE_SLOT_PLUS: no reply; current slot +1.STATE_SLOT_MINUS: no reply; current slot -1, floored at -1 (auto).SAVE_FILES:OK/NO(newline-terminated).GET_STATUS:GET_STATUS PLAYING <core_id>,<basename>(newline-terminated) orGET_STATUS CONTENTLESS.DISK_EJECT_TOGGLE: open or close the virtual tray.DISK_NEXT: step to the next disc in the loaded playlist.QUIT: no reply; queued for the runloop.
Saves and loads are confirmed on the filesystem instead of from a reply: a save by the state file it writes, a load by the access time of the state file it reads.
Save slots: there is no "save to slot n" command. SAVE_STATE writes whichever
slot is current, nothing reports which that is, and a state_slot in the
appended config does not survive content load (verified on 1.22.2: pinning 10
still wrote slot 0). None of that matters much here, because RomM keeps the
library of states and this only ever works in STATE_SLOT. What RetroArch does
have is that hard floor at -1, so counting MINUS presses down to it and PLUS
presses back up parks the slot absolutely. That runs once per launch, and
again only if a save lands somewhere else, which is the one thing that can
happen: the player cycling slots with their own hotkeys.
State file naming, under states/<library_name>/, the dir RetroArch sorts
each core's states into (library_name is the name the core reports):
<content_basename>.statefor slot 0.<content_basename>.state<n>for slot n.<content_basename>.state.autofor slot -1.- SRAM:
<content_basename>.srmundersaves/<library_name>/.
Because stdout carries only command replies here, the child is spawned with
a real stdout pipe drained by a reader thread, unlike the shared _spawn
which merges stderr into stdout (that would corrupt the reply stream).
attributelog= logging.getLogger(__name__)attributeROM_ROOT= Path(os.environ.get('ROM_ROOT', '/romm'))Root of the RomM library mount, from ROM_ROOT (default /romm).
A ROM candidate has to resolve to somewhere under it to be booted.
attributeRA_BASE_DIR= xdg_config_dir('retroarch')RetroArch's own directory root: $XDG_CONFIG_HOME/retroarch, else $HOME/.config/retroarch.
Every directory RetroArch has no explicit setting for hangs off this one root
on Linux, whatever that directory holds. Probed against the container's build
(1.22.2): started on a config naming none of them, RetroArch saved
libretro_directory and libretro_info_path as cores under this root, and
system_directory, savefile_directory and savestate_directory as
system, saves and states under it.
attributeRA_CONFIG_PATH= _resolve_config_path()The user's retroarch.cfg, under RETROARCH_CONFIG_DIR when that is set.
Safe to move, because the launch states it: --config names this file on
RetroArch's own command line. Without that the knob would move only the file
the broker reads the cores and system directories out of, while RetroArch kept
loading the one its own search order found.
attributeCORES_DIR= Path(os.environ.get('RETROARCH_CORES_DIR') or _configured_dir('libretro_directory') or RA_BASE_DIR / 'cores')Where libretro cores are installed and loaded from.
Taken from RETROARCH_CORES_DIR, else the libretro_directory in the user's
config, else cores under RA_BASE_DIR, which is RetroArch's own default
libretro_directory. RetroArch only gives a core its core info when the core
sits in its libretro_directory and the .info in its libretro_info_path,
so the broker config pins both here. Without it, GET_STATUS segfaults
RetroArch mid-session (1.22.2). Following the user's own libretro_directory
is also what makes a downloaded core show up in the desktop RetroArch without
its in-app core downloader.
attributeSYSTEM_DIR= Path(os.environ.get('RETROARCH_SYSTEM_DIR') or _configured_dir('system_directory') or RA_BASE_DIR / 'system')Where cores look for the assets and firmware they cannot ship themselves.
Taken from RETROARCH_SYSTEM_DIR, else the system_directory in the user's
config, else system under RA_BASE_DIR, which is where RetroArch looks when
nothing configures it.
Safe to move, because the launch states it: _write_broker_cfg puts this path
in system_directory, and --appendconfig gives that priority over the user's
config. Without that the knob would drop the firmware and the linked core
assets into a directory no core ever opens.
attributeCORES_BASE_URL= os.environ.get('RETROARCH_CORES_BASE_URL', 'https://buildbot.libretro.com/nightly/linux/x86_64/latest')Base URL cores are downloaded from, from RETROARCH_CORES_BASE_URL.
Defaults to the libretro buildbot's latest linux x86_64 nightly. The buildbot
ships every core as <core>_libretro.so.zip.
attributeGITHUB_API_BASE= os.environ.get('GITHUB_API_BASE', 'https://api.github.com').rstrip('/')GitHub API root, from GITHUB_API_BASE (default https://api.github.com).
Cores the buildbot does not carry name their own release source instead, and their releases are looked up here.
attributeRA_DATA_DIR= Path(os.environ.get('RETROARCH_DATA_DIR', '/config/.retroarch'))Root of the broker-managed RetroArch data, from RETROARCH_DATA_DIR.
Defaults to /config/.retroarch. Holds the save data and the append-config we
layer onto the user's RetroArch config at launch.
attributeSTATE_DIR= RA_DATA_DIR / 'states'The broker-managed savestate directory, states under RA_DATA_DIR.
attributeSAVE_DIR= RA_DATA_DIR / 'saves'The broker-managed savefile (SRAM) directory, saves under RA_DATA_DIR.
attributeBROKER_CFG= RA_DATA_DIR / 'broker.cfg'The append-config written per launch, broker.cfg under RA_DATA_DIR.
attributeCORE_OPTIONS_CFG= RA_DATA_DIR / 'broker-core-options.cfg'The per-core option pins written per launch, broker-core-options.cfg under RA_DATA_DIR.
_write_broker_cfg points core_options_path at this file and forces
global_core_options/game_specific_options so it always applies; see
_write_core_options.
attributeRA_LOG_PATH= Path(os.environ.get('RETROARCH_LOG_PATH', '/config/retroarch.log'))Where RetroArch's stderr is appended, from RETROARCH_LOG_PATH (default /config/retroarch.log).
attributeSAVE_FILES_WAIT= float(os.environ.get('RETROARCH_SAVE_FILES_WAIT', '10.0'))Seconds to wait for the SAVE_FILES reply at exit, from RETROARCH_SAVE_FILES_WAIT (default 10).
attributeSTATE_CONFIRM_WAIT= float(os.environ.get('RETROARCH_STATE_CONFIRM_WAIT', '10.0'))Seconds a save gets to land on disk, from RETROARCH_STATE_CONFIRM_WAIT (default 10).
The platform table's state_confirm_wait overrides this per platform, for
cores whose state files are large enough that this default is too tight.
attributeSAVE_LOCK_WAIT= float(os.environ.get('RETROARCH_SAVE_LOCK_WAIT', '30.0'))Seconds a save waits for an in-flight resume load or disc swap, from RETROARCH_SAVE_LOCK_WAIT.
Defaults to 30. A save queues rather than failing fast (the way a second disc swap does) because the state it would write mid-resume is the pre-resume one, and the resume then loads that back over the player's own save.
attributeEXIT_SAVE_LOCK_WAIT= float(os.environ.get('RETROARCH_EXIT_SAVE_LOCK_WAIT', '3.0'))Seconds the exit save waits for the tray, from RETROARCH_EXIT_SAVE_LOCK_WAIT.
Defaults to 3, far below SAVE_LOCK_WAIT. The exit runs under the API's
process-wide session lock, so every second spent here is a second every other
session route answers 409 for. A resume load still holding the tray at exit is
already off the rails, and waiting out the full save budget inside that lock
would not make it any likelier to finish.
attributeQUIT_WAIT= float(os.environ.get('RETROARCH_QUIT_WAIT', '10.0'))Seconds the second QUIT gets before SIGTERM, from RETROARCH_QUIT_WAIT (default 10).
attributeQUIT_CONFIRM_GAP= float(os.environ.get('RETROARCH_QUIT_CONFIRM_GAP', '0.1'))Seconds the first QUIT gets before a second press, from RETROARCH_QUIT_CONFIRM_GAP (default 0.1).
attributeRESUME_LOAD_WAIT= float(os.environ.get('RETROARCH_RESUME_WAIT', '90.0'))Seconds a deferred resume load waits for a running game and a state file.
From RETROARCH_RESUME_WAIT (default 90).
attributeRESUME_LOAD_SETTLE= float(os.environ.get('RETROARCH_RESUME_SETTLE', '3.0'))Seconds between the core reporting PLAYING and the load, from RETROARCH_RESUME_SETTLE (default 3).
attributeRESUME_LOAD_RETRY_GAP= float(os.environ.get('RETROARCH_RESUME_RETRY_GAP', '2.0'))Seconds between resume load attempts, from RETROARCH_RESUME_RETRY_GAP (default 2).
A core that echoed the load but never read the state was not ready for it, so the next attempt is worth spacing out rather than firing back immediately.
attributeRESUME_ATTEMPT_LOCK_WAIT= float(os.environ.get('RETROARCH_RESUME_LOCK_WAIT', '15.0'))Seconds one resume load attempt waits for the tray, from RETROARCH_RESUME_LOCK_WAIT.
Defaults to 15, which covers a save running to its own confirmation timeout
at the global STATE_CONFIRM_WAIT default. A platform whose
state_confirm_wait override runs longer than that can make one attempt
give up on a save still in flight; the retries drop the tray between
attempts, so this bounds a single attempt rather than the whole resume
budget, and the next attempt picks the tray up once the save releases it.
attributeLOAD_ACK_WAIT= float(os.environ.get('RETROARCH_LOAD_ACK_WAIT', '10.0'))Seconds to wait for the LOAD_STATE_SLOT echo, from RETROARCH_LOAD_ACK_WAIT (default 10).
attributeLOAD_CONFIRM_WAIT= float(os.environ.get('RETROARCH_LOAD_CONFIRM_WAIT', '10.0'))Seconds RetroArch gets to actually read the state file after the echo.
From RETROARCH_LOAD_CONFIRM_WAIT (default 10). The echo says the command
was parsed, not that a state was restored; the read is what says that.
attributeLOAD_LOCK_WAIT= float(os.environ.get('RETROARCH_LOAD_LOCK_WAIT', '30.0'))Seconds a manual load waits for the tray, from RETROARCH_LOAD_LOCK_WAIT (default 30).
Matches SAVE_LOCK_WAIT: a manual load and a deferred resume attempt both
back-date the same state file's access time and poll it to confirm their own
read, so they must never run that confirmation concurrently, or either one
can mistake the other's read for its own.
attributeLOAD_TAINT_RETRY_LIMIT= int(os.environ.get('RETROARCH_LOAD_TAINT_RETRY_LIMIT', '2'))Extra backdate-and-send cycles a tainted load gets, from RETROARCH_LOAD_TAINT_RETRY_LIMIT.
A handout landing inside the read wait taints that attempt for good under
relatime (see _wait_for_state_read), so a fresh marker is the only way
left to detect the load actually happening; without a retry here a load that
RetroArch genuinely performed would surface to the caller as a plain failure
whenever it raced a state-file handout.
attributeSTATE_READ_HANDOUT_GRACE= float(os.environ.get('RETROARCH_STATE_HANDOUT_GRACE', '2.0'))Seconds a handed-out state path stays suspect, from RETROARCH_STATE_HANDOUT_GRACE.
Defaults to 2, enough to cover the gap between a caller asking state_path
for the file and its own read of it landing.
attributeSTATE_SLOT= _state_slot_from_env()The one slot the broker works in, from RETROARCH_STATE_SLOT (default 0).
0 is RetroArch's own default, so a state written here is also the one the
player's own load hotkey reaches for. -1 works in the auto slot,
<content>.state.auto, instead: the homing presses stop on that floor,
SAVE_STATE writes it, and LOAD_STATE_SLOT -1 reads it. The appended config
keeps savestate_auto_save and savestate_auto_load off either way, so
RetroArch never writes or loads that file behind the broker's back.
attributeSLOT_STEP_DELAY= float(os.environ.get('RETROARCH_SLOT_STEP_DELAY', '0.1'))Seconds between slot-homing presses, from RETROARCH_SLOT_STEP_DELAY (default 0.1).
The pause is what keeps RetroArch from dropping presses.
attributeMENU_SETTLE= float(os.environ.get('RETROARCH_MENU_SETTLE', '0.5'))Seconds after a MENU_TOGGLE before the next command, from RETROARCH_MENU_SETTLE (default 0.5).
The menu swallows the hotkey-style commands (SAVE_STATE, the slot presses),
so one sent before the toggle has taken effect is lost the same way.
attributeMENU_COMBO= int(os.environ.get('RETROARCH_MENU_COMBO', '2'))Pad combo that opens the menu, from RETROARCH_MENU_COMBO (default 2, L3 + R3).
An index into RetroArch's input_menu_toggle_gamepad_combo list, applied only
when the user's own config sets no combo; 0 writes none. Without one, a player
on a pad has no way into the menu: the room's pads have no F1 key. RetroArch
reads the combo from every pad during play but only port 0 once the menu is
up, so any player can open the menu but only player 1 or the room's button
can close it.
attributeSLOT_HOME_STEPS= int(os.environ.get('RETROARCH_SLOT_HOME_STEPS', '24'))STATE_SLOT_MINUS presses used to home the slot, from RETROARCH_SLOT_HOME_STEPS (default 24).
The step count has to outrun any slot the player could have cycled to.
attributeFIRST_SAVE_SETTLE= float(os.environ.get('RETROARCH_FIRST_SAVE_SETTLE', '3.0'))Seconds a session's first save waits after PLAYING, from RETROARCH_FIRST_SAVE_SETTLE (default 3).
A fast-booting core (gambatte) can still be showing its own boot sequence
(the Nintendo logo scroll, or a flat boot-transition frame) rather than the
game's first real frame when the very first save of a session reaches
_try_save: the state file confirms fine, but the frame captured with
the save shows whatever is on screen at that instant. Counted from PLAYING
rather than from process spawn, since core load and ROM load between spawn
and PLAYING are themselves variable and would otherwise eat into (or blow
past) the budget before the game has actually started. Only the first save
pays this cost, since _slot_homed is already true for every later one; a
session whose first save comes well after PLAYING naturally owes no wait
at all.
attributeFIRST_SAVE_SETTLE_WAIT= float(os.environ.get('RETROARCH_FIRST_SAVE_SETTLE_WAIT', '20.0'))Seconds _wait_for_first_save_settle waits for a PLAYING timestamp, from
RETROARCH_FIRST_SAVE_SETTLE_WAIT (default 20).
Covers a save requested before _track_first_playing has observed PLAYING
even once, e.g. a very fast player or a slow-booting core; past this the
save proceeds unsettled rather than blocking indefinitely on a core that
never came up.
attributeDISC_TRAY_SETTLE= float(os.environ.get('RETROARCH_DISC_TRAY_SETTLE', '1.5'))Seconds the tray gets to open before discs are stepped, from RETROARCH_DISC_TRAY_SETTLE (default 1.5).
The settle is not optional: RetroArch drops a disc index change that arrives while the tray is still opening, and the failure is silent (the old disc stays mounted).
attributeOBSERVE_WINDOW_BEFORE_SLACK= float(os.environ.get('RETROARCH_OBSERVE_WINDOW_BEFORE_SLACK', '2.0'))Seconds _observe_library_name backdates _launch_wall by, from
RETROARCH_OBSERVE_WINDOW_BEFORE_SLACK (default 2.0).
Some network mounts (SMB, FAT) round a file's mtime to a coarse unit (as
little as 2 seconds), so a state written right at launch can read back as a
hair older than _launch_wall itself; without this slack that write would
be mistaken for stale save data from a previous session.
attributeOBSERVE_WINDOW_AFTER_SLACK= float(os.environ.get('RETROARCH_OBSERVE_WINDOW_AFTER_SLACK', '30.0'))Seconds _observe_library_name extends past "now" by, from
RETROARCH_OBSERVE_WINDOW_AFTER_SLACK (default 30.0).
The share and the broker's clocks are not guaranteed to agree; a mtime that is genuinely from this session but arrives slightly ahead of the broker's own clock should not be discarded as bogus. Wide enough to absorb ordinary skew, narrow enough that a stale future-dated file (a bad clock, a copied save) still fails the check.
attributeDISC_STEP_DELAY= float(os.environ.get('RETROARCH_DISC_STEP_DELAY', '0.1'))Seconds between DISK_NEXT presses, from RETROARCH_DISC_STEP_DELAY (default 0.1).
attributeDISC_SWAP_WAIT= float(os.environ.get('RETROARCH_DISC_SWAP_WAIT', '90.0'))How long a swap waits for the core to report a running game, from RETROARCH_DISC_SWAP_WAIT.
Defaults to 90 seconds. A mid-session swap answers on the first poll; a swap issued right after launch waits out the boot.
attributeCORE_DOWNLOAD_TIMEOUT= float(os.environ.get('RETROARCH_CORE_DOWNLOAD_TIMEOUT', '180'))HTTP timeout for core downloads and release lookups, from RETROARCH_CORE_DOWNLOAD_TIMEOUT.
Defaults to 180 seconds.
attributeJOYPAD_DRIVER= os.environ.get('RETROARCH_JOYPAD_DRIVER', 'linuxraw')The joypad driver forced at launch, from RETROARCH_JOYPAD_DRIVER (default linuxraw).
The pads here are Selkies interposer sockets, not real devices, and the fake libudev behind them gives every one the same identity. RetroArch's udev joypad driver reads that as one device plugged eight times ("Device ID 0 is already plugged") and ends up with no pads at all. linuxraw opens the js nodes directly, which the interposer does hook, so there is nothing to collide on. Set empty to leave the user's own driver alone.
attributePLATFORMSdict[str, dict[str, Any]]= _load_platforms()RomM platform slug to libretro core, loaded from _PLATFORMS_FILE.
Each entry names the core and its extensions; the extensions order doubles
as the preference order when a folder holds several candidates.
library_name is the name the core reports in retro_get_system_info.
RetroArch names the sorted save and state dirs after it, and it is not
always the core's file name: dolphin reports dolphin-emu.
save_ram is required, and says whether the core exposes
RETRO_MEMORY_SAVE_RAM on this platform, so that RetroArch loads its battery
save from <stem>.srm. Where it is false the core keeps its save in a file of
its own, or has none, so an imported .srm would never reach the game.
sav_is_srm is optional, and true only where a device's .sav for this
platform was checked to hold the same bytes, at the same offsets, as the
core's .srm. There an import places a .sav as the .srm; it is
renamed, never converted. An alternate states its own, and an untested core
has none.
savestate is assumed true; only specialized cores opt out.
assets maps a path under the RetroArch system dir to the directory on the
image holding those files, for cores that need data the .so does not carry.
core_source names where a core the buildbot does not carry comes from, and
save_subtrees narrows the save archive for cores whose savefile dir is also
their app-data dir.
resume_settle and state_confirm_wait override RESUME_LOAD_SETTLE and
STATE_CONFIRM_WAIT for cores that are slower than the defaults assume,
such as PPSSPP's multi-megabyte state files.
core_options pins libretro core option keys to values on every launch, via
_write_core_options; for cores that default to writing a game's save into
its own content file or disk image (PUAE) rather than SAVE_DIR, so the ROM
library never gets a save mixed into it. It also pins the machine a shared
core would otherwise guess from the content path (PUAE's puae_model for CDTV).
core_option_seeds pins a core option key the same way, but only the first
time it is missing from CORE_OPTIONS_CFG; once seeded, whatever value is
already on disk wins, including a value the user changed from RetroArch's own
Quick Menu, since _write_broker_cfg makes that file the one every core's
option changes round-trip through. _resolve_core_options computes the seed,
and a key also present in core_options always keeps the pinned value
instead. Used for VICE and Hatari's floppy/cartridge write protection, which
have no write-redirect option like PUAE's: seeding it on protects the ROM
library by default without permanently blocking a player who wants in-game
floppy saves back.
save_links maps a SYSTEM_DIR-relative path to a SAVE_DIR-relative one,
for a core that keeps a fixed-path save file outside every save_subtree
(NeoCD's backup RAM); _ensure_save_links symlinks the source at the
destination so the save archive's existing per-game isolation reaches it.
alternates names other cores vetted for the platform, keyed by core name,
for an operator's core: override to pick. An alternate takes only the
platform's extensions; it never inherits a core-owned field from the
default (the fields in retroarch_cores.CORE_OWNED_FIELDS), since those
belong to the core that set them, not to the platform.
extra_extensions lists extensions a core handles that core-info's
supported_extensions omits, so the offline check that extensions is a
subset of what the core supports (plus this list) still passes.
untested marks a platform whose default core nobody has booted a real game
on yet. It still launches; the cores route reports the default unverified, the
docs tier table tags it, and every launch logs a warning. It belongs to the
platform rather than the core, so an alternate may not carry it.
attributeRA_ARCHIVE_EXTS= ('.zip', '.7z')Archives RetroArch extracts itself before handing a core the content inside.
attributeARCHIVE_LIST_TIMEOUT= 30.0Seconds 7z l gets to list a ROM archive for an untested core's extension check.
attributeMSU1_CACHE_DIR= Path(os.environ.get('RETROARCH_MSU1_CACHE_DIR', str(RA_DATA_DIR / 'msu1')))Where zipped MSU-1 games are extracted and booted from (env RETROARCH_MSU1_CACHE_DIR).
Defaults to msu1 under RA_DATA_DIR, outside the states and saves
subtrees the save archive carries.
attributeMSU1_CACHE_MAX_GB= float(os.environ.get('RETROARCH_MSU1_CACHE_MAX_GB', '20'))Cap on the MSU-1 extraction cache in GB (env RETROARCH_MSU1_CACHE_MAX_GB, default 20).
The least recently booted games are evicted to make room for a new one.
attributeCORE_INFO_REFRESH= 'core_info.refresh'Makes RetroArch rebuild core_info.cache, which otherwise ignores a rewritten .info.
attributeMSU1_EXTRACT_TIMEOUT= 1800.0Seconds 7z gets to extract an MSU-1 archive, which can run to most of a gigabyte.