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 -k -X POST https://localhost:3001/streaming/api/session/save-state \
  -H 'X-Broker-Secret: <shared secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "slot": 3 }'

/load-state takes the same body. Both return {"status": "saved"|"loaded"|"failed", "slot": N, ...} and both are secret-only: the parent's backend is the caller, not the room UI.

RomM is the library of states, so each emulator works in exactly one slot and resolves whatever slot is asked for to its own. The response echoes the slot it actually used, which is also the state_slot in the status response. The field is kept because the per-emulator brokers take it and because it is what the parent needs to read back, not because the broker keeps ten of anything.

400 means the running emulator has no save states at all (desktop, shadps4 and the others marked "no" under Supported emulators persist through the game's own save data instead), 409 means there is no active session or the emulator process is gone. Which case applies is readable ahead of time from supports_states in the status response, so the parent does not need its own per-emulator table.

A "failed" status is a real answer, not an error: for PCSX2 it means PINE never acked, and for any emulator it covers a load of a slot that holds no state file, or a save whose slot never appeared on disk.

Move a state file in or out

curl -k -X GET https://localhost:3001/streaming/api/session/state-file \
  -H 'X-Broker-Secret: <shared secret>' -o state.bin

curl -k -X PUT "https://localhost:3001/streaming/api/session/state-file?filename=NAME" \
  -H 'X-Broker-Secret: <shared secret>' --data-binary @state.bin

This is how RomM becomes the datastore rather than the container: the GET follows a save so the state can be filed in the library, and the PUT sends any stored state back so a load or a resume can reach it. The GET reports the name in X-State-Filename and the slot in X-State-Slot.

The PUT takes a name back and asks only whether the running emulator could have written it for the loaded game. The slot in the name is not part of that test: the library holds states captured under whatever slot was in use at the time, so the name is restamped into this broker's one working slot and the response reports the name it landed under. What is checked is identity, the PCSX2 serial or the RetroArch content basename, so a state for another game or another directory is still a 400 rather than a stray file in the save tree.

The PUT needs a live session and returns 409 without one, since a state is only pushed in so a running emulator can reach it. The GET outlives the session: exit captures a state on its way out, and RomM can only come back for it once the teardown has answered, so refusing there was what left every exit state stranded in the container. The broker holds the exited session's state until the next activate clears the working slot, and answers 409 before the first session and 404 once the slot is empty. 413 on either side means the file is over BROKER_STATE_FILE_MAX_BYTES (256 MiB); RomM caps the same transfer, so raising one end alone only moves which end refuses.

Fetch the frame a state was taken at

curl -k -X GET https://localhost:3001/streaming/api/session/state-screenshot \
  -H 'X-Broker-Secret: <shared secret>' -o state.png

This is what gives a stored state its thumbnail in RomM's resume picker. Only emulators that write the frame as its own file answer it: RetroArch saves a <state file>.png beside every state, so it serves that. PCSX2 embeds the frame inside the .p2s and returns 404, which is the caller's cue to read it out of the state it already fetched rather than an error. It stays readable after exit on the same terms as the state itself, with 413 over BROKER_STATE_SCREENSHOT_MAX_BYTES (16 MiB).

Resume after activate

A state pushed with the PUT can arrive after activate has already returned and the game is booting. The deferred resume load waits for the working slot to fill before firing, so the sequence RomM uses is: activate with resume_slot, PUT the chosen state, and let the broker's watchdog load it once the emulator reports a running game.

On this page