webstation-broker
REST API

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.bin

These 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.p2s or ULUS10041_1.00_1.ppst. Another game's state gets filename 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.png

This 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.

On this page