emulators.shadps4
shadPS4 (PlayStation 4) launcher: binary selection, ROM resolution, and IPC-driven shutdown.
shadPS4 (PlayStation 4) launcher: binary selection, ROM resolution, and IPC-driven shutdown.
shadPS4 has no save states. Persistence is the game's own save data, which
the game commits to plain host files under
<data>/home/<user_id>/savedata/<game_serial>/<slot>/. Save paths are keyed
by the game serial, so shipping the whole home/1000/savedata subtree makes
a save archive restored into a fresh container line up with the titles it
belongs to.
Control plane: shadPS4's IPC protocol (SHADPS4_ENABLE_IPC=true) reads
commands from stdin. We feed RUN then START so the game boots headlessly,
and STOP for a graceful quit (it pushes SDL_EVENT_QUIT, the same path as a
window close). shadPS4 registers no SIGTERM/SIGINT handler, so SIGTERM would
kill the process hard and leave read-write save mounts with their
sce_sys/corrupted marker in place; STOP must come first and SIGTERM is
only the escalation fallback.
shadPS4 has no PKG installer of its own; a .pkg ROM, or a .7z/.zip/
.rar archive holding one, is unpacked with the standalone pkg_extractor
tool into CACHE_DIR, mirroring rpcs3's archive cache
(webstation_broker/emulators/rpcs3.py): extracted once and reused on every
later launch. Those formats are only bootable at all with SHADPS4_CACHE_ENABLED,
since a multi-GB extraction thrown away on every launch buys nothing; with
the cache off resolve_rom_file refuses them and only natively bootable
formats work. An archive is unpacked to a scratch dir first to locate the
.pkg it holds; only pkg_extractor's own output lands in the game dir, so
the cache key is taken from the archive itself rather than the throwaway
scratch extraction.
attributelog= logging.getLogger(__name__)attributeVERSIONS_DIR= Path(os.environ.get('SHADPS4_VERSIONS_DIR', str(Path.home() / '.local/share/shadPS4QtLauncher/versions')))Where the launcher downloads builds, one folder per release (env SHADPS4_VERSIONS_DIR).
Defaults to ~/.local/share/shadPS4QtLauncher/versions.
attributeDATA_DIR= xdg_data_dir('shadPS4')shadPS4's data root, holding the save data the archive is built from.
Not configurable, and deliberately: nothing on shadPS4's command line names it,
so an override would move only the tree the broker dumps and restores and leave
shadPS4 writing its own. That loses saves without reporting anything. launch
exports the root this resolved to instead.
attributeSHADPS4_LOG_PATH= Path(os.environ.get('SHADPS4_LOG_PATH', '/config/shadps4.log'))The emulator log file (env SHADPS4_LOG_PATH, default /config/shadps4.log).
attributeSAVEDATA_SUBTREE= 'home/1000/savedata'Save data under the default PS4 user, relative to DATA_DIR; the whole save archive.
attributeSHADPS4_CONFIG_PATH= DATA_DIR / 'config.json'shadPS4's own config file, config.json under DATA_DIR.
Not configurable, for the same reason as DATA_DIR: shadPS4 looks for it beside
its save data and takes no flag naming it, so an override would leave the broker
pinning a GPU in a file the emulator never opens.
attributeSHADPS4_GPU_ID= os.environ.get('SHADPS4_GPU_ID', 'auto')Vulkan device index pinned into config.json before each launch (env SHADPS4_GPU_ID, default auto).
shadPS4's own gpu_id: -1 (auto-select) can land on a CPU-rendered Vulkan
device (llvmpipe, swiftshader, ...) instead of a real GPU: the game keeps
running (audio, playtime counter) but every frame is presented black, since
software rendering never keeps up with the presentation deadline. auto
picks a real device via _detect_gpu_id, vendor-agnostic; an integer pins
that index directly for a host vulkaninfo cannot read; -1 or KEEP
leaves config.json alone. The choice persists in config.json, so it is
pinned before each launch regardless of which one shadPS4 wrote last.
attributeVULKANINFO_BIN= os.environ.get('SHADPS4_VULKANINFO_BIN', 'vulkaninfo')The vulkaninfo binary used by _detect_gpu_id (env SHADPS4_VULKANINFO_BIN).
attributeROM_EXTENSIONS= ('.zar', '.bin', '.pkg') + _ARCHIVE_EXTSBootable formats: a game folder (eboot.bin inside it), a .zar archive, a raw .pkg, or a .7z/.zip/.rar archive holding one.
attributePKG_EXTRACTOR_BIN= os.environ.get('SHADPS4_PKG_EXTRACTOR_BIN', 'pkg_extractor')The pkg_extractor binary (env SHADPS4_PKG_EXTRACTOR_BIN, default pkg_extractor on PATH).
attributePKG_EXTRACT_TIMEOUT= float(os.environ.get('SHADPS4_PKG_EXTRACT_TIMEOUT', '1800'))Seconds a pkg_extractor run gets before it is considered hung (env SHADPS4_PKG_EXTRACT_TIMEOUT).
attributePKG_EXPANSION_FACTOR= float(os.environ.get('SHADPS4_PKG_EXPANSION_FACTOR', '1.1'))Assumed extracted-size to .pkg-size ratio (env SHADPS4_PKG_EXPANSION_FACTOR, default 1.1).
A PS4 .pkg is already compressed per-file, so pkg_extractor's output lands
close to the package's own size; the margin covers the filesystem overhead of
many small files. It is only an estimate, so _check_expansion reports a
title that outgrows it instead of letting the space guards look like they
held.
attributeARCHIVE_PEAK_FACTOR= float(os.environ.get('SHADPS4_ARCHIVE_PEAK_FACTOR', '2.2'))Assumed peak-on-disk to archive-size ratio (env SHADPS4_ARCHIVE_PEAK_FACTOR, default 2.2).
An archive holds its unpacked .pkg and pkg_extractor's output at the same
moment, so the disk carries roughly twice PKG_EXPANSION_FACTOR at the height
of the run even though only the output survives.
attributeCACHE_DIR= Path(os.environ.get('SHADPS4_CACHE_DIR', str(DATA_DIR / 'extracted')))attributeCACHE_MAX_GB= float(os.environ.get('SHADPS4_CACHE_MAX_GB', '30'))attributeBIN_NAME= os.environ.get('SHADPS4_BIN_NAME', 'Shadps4-sdl.AppImage')The binary looked for inside a release folder (env SHADPS4_BIN_NAME, default Shadps4-sdl.AppImage).
Release folders look like v0.17.0 - Garbage Collector's Edition - 2026-07-30.
The Pre-release folder always carries the newest build and trumps all.