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.