Save states
Saving and loading state mid-session, and moving state files and their screenshots between the container and RomM.
Save and load state mid-session
curl -X POST http://localhost:3000/streaming/api/session/save-state \
-H 'X-Broker-Secret: <shared secret>' \
-H 'Content-Type: application/json' \
-d '{ "slot": 3 }'/load-state takes the same request body. Both routes return
{"status": "saved"|"loaded"|"failed", "slot": N, ...}. Both need the shared
secret header shown above, so only the parent's backend (usually RomM) can
call them, not the room UI.
RomM keeps the full library of states, not the broker. Because of that, each
emulator here only ever uses one dedicated slot, no matter which slot
number you ask for: the broker just maps your request onto that one slot.
The response tells you the slot it actually used, and that same number shows
up as state_slot in the status response. The slot field is kept because
every emulator's request format expects it and because the parent needs a
number back, not because this broker actually keeps several separate save
slots.
slot runs from -1 to 10. -1 is RetroArch's auto slot, so a state RomM
captured there can be named as it was saved. Like any other number, it is
mapped onto the emulator's working slot, on RetroArch and every other
emulator alike.
A 400 response means the running emulator has no save states at all.
(desktop, shadps4, and the other emulators marked "no" under Supported
emulators save your progress through the game's own save
data instead.) A 409 response means there is no active session, or the
emulator has already stopped running. You can check which case applies ahead
of time by looking at supports_states in the status response, so you don't
need to keep your own list of which emulators support save states.
A "failed" status is a normal answer, not an error. For PCSX2, it means
PINE (the socket the broker uses to send PCSX2 remote commands) never
confirmed the save. For any emulator, it also covers loading a slot with no
state file in it, or a save whose file never showed up on disk.
Move a state file in or out
curl -X GET http://localhost:3000/streaming/api/session/state-file \
-H 'X-Broker-Secret: <shared secret>' -o state.bin
curl -X PUT "http://localhost:3000/streaming/api/session/state-file?filename=NAME" \
-H 'X-Broker-Secret: <shared secret>' --data-binary @state.binThese two routes are how RomM stores your save states instead of the
container storing them. Use the GET after a save, to pull the state file out
and file it away in RomM's library. Use the PUT to send a stored state back
in, so a load or a resume can reach it. The GET response reports the file's
name in the X-State-Filename header and its slot in the X-State-Slot
header. On RetroArch it also names the running core in X-State-Core,
and the folder the state file sits in under states/ in X-State-Library.
A legacy state sitting directly in states/ is named under the default
core's folder. A folder name RomM can't file under (one a header can't
carry, with a slash, surrounding spaces, or a leading dot) is left out.
The PUT checks one thing: could the running emulator have written this
file, under this name, for the game that's loaded right now? The slot number
baked into the filename doesn't matter for that check, because RomM's
library holds states saved under whatever slot happened to be in use at the
time. Instead, the broker renames the file to fit its own one working slot,
and the response reports the name it landed under. What actually gets
checked is the game itself, so a state for a different game, or from a
different folder, comes back as a 400 instead of landing as a stray file
in the save folder:
- RetroArch checks the name it uses for the loaded ROM.
- PCSX2 and PPSSPP check the game's serial at the start of the filename, as
in
SLUS-20946 (7D3A8B4E).03.p2sorULUS10041_1.00_1.ppst. Another game's state getsfilename is not a state this emulator would write. - Dolphin checks the game id the file itself opens with. Another game's
state gets
state file belongs to another game. Dolphin compares the four-character game code, not the two-character maker code after it.
For PCSX2, Dolphin and PPSSPP, the loaded game's id comes from the rom when
the broker can read one there. Only Dolphin does, from an .iso, .gcm or
.wbfs image. Otherwise it is the title_id RomM sends on activate (see
What RomM can tell the broker).
The broker sets it on every activate, with or without imports, so a wrong
title_id refuses that game's states on every push until it is corrected in
RomM. A session with no id takes every state on trust, as every push was
before. So does any session, for a state whose name or opening bytes hold no
id the broker recognises, such as a homebrew game's.
On PCSX2, Dolphin and PPSSPP the name is also held to the rules an archive
member's name is. The broker already uses only the last part of filename.
If that part is empty, . or .., starts with a dot or a space, holds a
backslash, colon, control character or bidirectional control character, or
runs past 255 bytes, it gets the same 400 as a name the emulator would not
write.
The PUT needs an active session and returns 409 without one, since
pushing a state in only makes sense while a running emulator can use it. The
GET works differently: it still works after the session ends. Exiting a
session saves a state on the way out, and RomM can only come back for it
once that shutdown has finished responding, so refusing the GET at that
point used to leave every exit state stranded in the container with no way
out. Instead, the broker holds onto the exited session's state until the
next activate call clears out the working slot. It answers 409 before
the very first session, and 404 once that slot is empty again. A 413
response on either route means the file is over
BROKER_STATE_FILE_MAX_BYTES (256 MiB by default). RomM enforces the same
size limit on its own side, so raising the limit on only one end just moves
which end rejects the file first.
Fetch the frame a state was taken at
curl -X GET http://localhost:3000/streaming/api/session/state-screenshot \
-H 'X-Broker-Secret: <shared secret>' -o state.pngThis route is what gives a stored state its thumbnail in RomM's resume
picker. The image is a screenshot of the streamed desktop, taken the moment
the save was sent. The save-state and exit routes grab it over the pixelflux
computer-use endpoint (a local screenshot API that Selkies, the video
streaming piece, exposes on 127.0.0.1:5000) right before the emulator is
told to save. The broker then shrinks the image so its longest side is
BROKER_STATE_SCREENSHOT_SIZE pixels, and only keeps it once the state
itself is confirmed saved. This works the same way for every emulator, so it
never depends on whatever screenshot feature, if any, the emulator itself
has. Like the state file, it stays readable after the session has exited. A
404 response means the state has no picture, either because the capture
failed or because the container is running without PIXELFLUX_CU set.
Either way, RomM still files the state, just without a thumbnail.
Resume after activate
A state pushed in with the PUT can arrive after activate has already
returned and the game is already booting. When that happens, the broker's
deferred resume load waits until the working slot is actually filled before
it fires, so it never tries to load a state that isn't there yet. Because of
this, RomM follows this order: call activate with resume_slot set, PUT
the chosen state, and let the broker's watchdog load it once the emulator
reports the game is running.
A state from outside the broker goes the same way on emulators whose
state_channel is push. Inside the archive it is refused with
state_uses_push.