webstation-broker
DeveloperPython referenceemulators

emulators.xenia

Xenia Edge (Xbox 360) launcher: ROM resolution and launch/kill lifecycle.

Xenia Edge (Xbox 360) launcher: ROM resolution and launch/kill lifecycle.

Xenia has no save states and no external control API (no IPC, no socket, no stdin protocol; the only listener is a gdbstub behind --debug, which drives the JIT, not the session). Persistence is the game's own save data, which the guest commits to plain host files under the content tree:

<storage_root>/content/<XUID>/<TITLE_ID>/00000001/<save name>/...

Each save package carries an STFS header in a sibling tree, <TITLE_ID>/Headers/00000001/<save name>, written when the save is created and left alone by later writes. The header is what names the save in the guest's own load menu, so a title's save directory and its header tree only mean anything together; Xenia.save_and_exit restamps whole title directories for that reason.

Guest writes are write-through: every guest NtWriteFile is an immediate host pwrite() with no user-space buffering, so save files are complete on disk the moment the game writes them and survive a hard kill. Xenia installs no SIGTERM/SIGINT handlers (only SIGILL/SIGSEGV/SIGBUS for the JIT), so SIGTERM kills it with default disposition. That skips xenia's own exit path, but that path only saves config and flushes the log, nothing save-related, so the base SIGTERM->SIGKILL stop is the shutdown story. The exit must always be broker-driven anyway: a game exiting to dashboard leaves xenia sitting on a modal dialog instead of exiting, and a guest main-thread exit leaves the process idling with the window open.

The profile tree (content/<XUID>/FFFE07D1/...) ships inside the content subtree: Xbox 360 save paths embed the profile XUID, so profile and saves must travel together for a restore into a fresh container to line up. It is also the one part of the tree the restore overwrites unconditionally, since it is the one part the stale-save clear cannot take out; see Xenia.always_restore.

A profile has to exist before the broker launches anything. Xenia Edge checks its account list as soon as the emulator is initialised and, finding none, raises a native "No Profiles Found" dialog that nobody in the stream can dismiss; without a profile a game cannot save either. Creating one on the desktop signs it in and persists the XUID into xenia-edge.config.toml under the storage root (logged_profile_slot_0_xuid), so every later launch against the same --storage_root signs in silently. docs/content/docs/emulators/setup.mdx carries the user-facing version of this.

Guest system dialogs (sign-in, message boxes, the on-screen keyboard) are drawn inside the game window selkies captures. The controller answers sign-in and message boxes. The on-screen keyboard is a text field with OK and Cancel: the controller can accept the text the game suggests or cancel, but typing anything else takes a keyboard in the browser. Since Xenia Edge commit 19b223d (2026-09-15) removed --headless, nothing answers these dialogs on the player's behalf. Storage device selection is the exception: Xenia picks the HDD itself unless storage_selection_dialog is turned on in its config.

One title per process: loading a second game requires a process restart.

Declared imports (import_spec, place_import) place a save under the signed-in profile's XUID, which _logged_profile_xuid reads out of the config before launch. See docs/content/docs/api/imports.mdx for the shapes taken.

Bootable forms: an XISO (.iso), a bare executable (.xex), an extracted dump (folder with default.xex at its root), or an XBLA / Games on Demand title folder, whose STFS package the resolver digs out of the content-type layout the console uses (see _find_container).

attributelog
= logging.getLogger(__name__)
attributeROM_ROOT
= Path(os.environ.get('ROM_ROOT', '/romm'))
attributeXENIA_BIN
= os.environ.get('XENIA_BIN', '/opt/xenia/AppRun')
attributeDATA_DIR
= Path(os.environ.get('XENIA_DATA_DIR', '/config/xenia'))
attributeXENIA_LOG_PATH
= Path(os.environ.get('XENIA_LOG_PATH', '/config/xenia.log'))
attributeROM_EXTENSIONS
= ('.iso', '.xex')
attributeCONTENT_SUBTREE
= 'content'
attributeCONFIG_NAME
= 'xenia-edge.config.toml'

Xenia Edge's config file, under the storage root; it names the signed-in profile.