webstation-broker
DeveloperPython referenceemulators

emulators.flycast

Flycast (Sega Dreamcast) launcher.

Flycast (Sega Dreamcast) launcher.

Handles ROM resolution, save state via a graceful close request, and boot-time resume.

Flycast has no control socket and, unlike DuckStation, installs no SIGTERM/SIGINT handler at all: a bare SIGTERM is a hard kill. Its only graceful-shutdown path is SDL_QUIT (a window close request), which dc_exit() turns into unloadGame(), and that is also the only place Dreamcast.AutoSaveState gets written.

Getting a real SDL_QUIT out of it takes more than an xdotool window-close command: windowclose is a raw XDestroyWindow, which never reaches SDL's own event loop, and windowquit's _NET_CLOSE_WINDOW is sent to the root window for a window manager to pick up, but this container's compositor (labwc, on wlroots) has no handler for that message at all, only for its own internal Close action. That action is what actually sends a real WM_DELETE_WINDOW to the window (wlroots' wlr_xwayland_surface_close()), and the only way to reach it externally is the same keybind a player would use: Alt+F4, which labwc's default config binds to Close. stop() therefore activates the window and sends that keypress, exactly the hotkey-injection shape dolphin.py/ppsspp.py already use for their own hotkeys, before falling back to the base SIGTERM->SIGKILL, the same graceful-trigger-then-escalate shape shadPS4 uses for its IPC STOP command.

Everything the broker needs (auto-load, auto-save, slot, fullscreen) rides -config section:key=value transient overrides on the command line rather than a patched ini, so nothing here touches emu.cfg. The section for the typed Dreamcast.* options is "config" (that's the whole key, dot included; see core/cfg/option.cpp), not "Dreamcast" as the key's own name might suggest.

Save state is a single deterministic file, <rom-basename>.state, in the flat XDG data dir ($XDG_DATA_HOME/flycast or ~/.local/share/flycast, same resolution flycast itself does). With slot pinned to 0 the broker can compute that path itself instead of guessing at the newest or most-recently-changed file in the directory.

That name is flycast's, not the broker's to pick, and it carries the rom's basename alone: two discs sharing a basename in different folders (a region or revision pair) write the same state file. So a confirmed write also records a <rom-basename>.state.rom marker naming the rom it came from, the same ownership trick duckstation.py uses on its flat savestates directory, and a resume is refused when the marker names a different rom. A state with no marker still loads, since an unidentified state is all the broker had before.

VMU saves are loose files in the same flat data dir (vmu_save_A1.bin etc., shared across games by default), beside the arcade nvmem and eeprom files. The whole directory ships as one save subtree rather than named files, since nothing here otherwise separates VMU saves from savestates from a first-run onboarding artifact. clear_working_slot() sweeps all of it at activate: none of it is namespaced by title or by player, and it runs before the rom is even known (activate resolves the rom, then clears the slot, then launches), so anything left behind is handed to the next player as their own and swept into their exit archive. The BIOS pair, emu.cfg and flycast's own subdirectories stay; they belong to the container, not to a session.

attributelog
= logging.getLogger(__name__)
attributeROM_ROOT
= Path(os.environ.get('ROM_ROOT', '/romm'))
attributeFLYCAST_BIN
= os.environ.get('FLYCAST_BIN', '/opt/flycast/AppRun')
attributeDATA_DIR
= xdg_data_dir('flycast')

Flycast's data directory, holding the VMU saves and the savestate.

Not configurable, and deliberately: nothing on Flycast's command line names it, so an override would move only the tree the broker dumps and restores and leave Flycast writing its own. That loses saves without reporting anything. launch exports the root this resolved to instead.

attributeFLYCAST_LOG_PATH
= Path(os.environ.get('FLYCAST_LOG_PATH', '/config/flycast.log'))
attributeROM_EXTENSIONS
= ('.chd', '.gdi', '.cdi', '.cue', '.elf')
attributeUNTRUSTED_SUFFIX
= '.untrusted'

Suffix a resume state is renamed with when a force-killed exit leaves it unusable.

The file is only suspected of being torn, never known to be, so it is set aside under this name rather than deleted. It stops matching *.state, so neither clear_working_slot nor a resume can pick it up, and it rides the save archive as ordinary save data (see save_file_kind), which is what makes it recoverable at all: DATA_DIR itself does not outlive the container.

attributeOWNER_SUFFIX
= '.rom'

Suffix of the marker file recording which rom a resume state belongs to.

The marker sits beside the state it names, holds the booted rom's resolved path, and rides the save archive with it. It does not match *.state, so it is never mistaken for a state.

attributeCONTAINER_FILES
= frozenset({'dc_boot.bin', 'dc_flash.bin', 'emu.cfg'})

DATA_DIR entries that belong to the container rather than to a session.

The Dreamcast BIOS pair is uploaded once during setup and every session needs it, and emu.cfg holds the graphics, audio and controller preferences the desktop session set. Everything else in the flat data dir is one player's progress, so clear_working_slot sweeps it and leaves these three.

attributeSTATE_WAIT
= float(os.environ.get('FLYCAST_STATE_WAIT', '30.0'))

Seconds a shutdown save gets to land on disk (env FLYCAST_STATE_WAIT, default 30).

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

Seconds size and mtime must both hold still before a state counts as written.

From env FLYCAST_STATE_STABLE, default 1.5. Long enough that a stalled write does not read as a finished one on a loaded host.

attributeVMU_IMAGE_BYTES
= 131072

The size of a raw VMU image, which is what Flycast mounts: 256 blocks of 512 bytes.

attributeDEFAULT_VMU
= 'vmu_save_A1.bin'

The VMU a memory-card import fills when its name picks none: controller A's first socket.

attributeNVMEM_NAME
= 'dc_nvmem.bin'

The Dreamcast's writable system flash (clock, language, and so on), as the Flycast binary names it.