Status: draft v1 · 2026-09-27 · synced with main 2026-10-01
This spec covers runtime configuration (file locations, layering, the TOML schema, validation and the PORTPS5_DEBUG variable), the rules for per-title workarounds (docs/workarounds.md), and removing AnyPS5 main's (merged PR #5) APS5_* environment switches.
Offline NP, PSN and trophy behaviour is not configurable, and is specified in save-data.md.
PRD bar owned here: F6 (a per-game TOML keyed by title ID that holds resolution scale, present mode and documented workarounds).
-
AnyPS5
main@e06dbff: noAPS5_*reads and no config file. -
AnyPS5
main@75a8668(incl. merged PR #5): 310 unique quotedAPS5_*names (git grep -ohE '"APS5_[A-Z0-9_]+"' | sort -u) and 427getenv(calls undercore/(git grep -h 'getenv(' -- core | wc -l).- The usual pattern is a function-local
static const bool x = std::getenv("APS5_...") != nullptr;. Each switch is read once, and any value, including0, turns it on. Examples:libSceAgcDriver/Execution/src/Driver.cpp:1693,libSceAgcDriver/Graphics/src/State.cpp:78. - Numeric switches parse with
atoi/strtodand fall back silently. For example,APS5_GPU_WAIT_TIMEOUT_MSdefaults to 1000 (Driver.cpp:4819), andAPS5_TIME_SCALEignores non-positive values (libkernel/Time/Time.cpp:37-46). - 264 of the 314 (module, name) pairs are in
libSceAgcDriver. The rest are spread over the recompiler, libkernel, libc, VideoOut, AudioOut, Ajm, Fiber and Agc.
- The usual pattern is a function-local
-
Present mode is hard-coded to FIFO (
libSceAgcDriver/Execution/src/VulkanDevice.cpp:918,:1550). There is no resolution-scale control. -
Title ID comes from
/app0/sce_sys/param.json(libkernel/AppMetadata/src/AppMetadata.cpp:22-37), so the runtime can select a per-title file. -
No TOML parser is in the tree. The submodules are SDL2, Vulkan-Headers, SPIRV-Headers, SPIRV-Tools, glslang, VMA and LibAtrac9.
-
PortPS5
main(2026-10-01):core/libs/prx/libc/src/Config.cpp(include/config/Config.hpp) parses and validatesconfig/global.toml,config/games/<titleId>.tomlandPORTPS5_DEBUG, reportingfile:line: key: reason; toml++ v3.4.0 is vendored and pinned at3rdparty/tomlplusplus/toml.hpp(TOML_LIB_MAJOR/MINOR/PATCH3.4.0; see build-toolchain.md). Verified still open onmain: the relinker does not copyconfig/at conversion time (beanportps5-c06p), the driver ignoresdisplay.present_mode/resolution_scaleand hard-codes FIFO atExecution/src/VulkanDevice.cpp:507,584(beanportps5-dtwf), and the runtime emits no results-JSON fields:config_sha256,workarounds_setanddebug_keys_setare written bytools/regress.py(PR #76) from the config files in the install dir (verification.md §4.2).core/has noAPS5_string literal and nogetenvoutsideConfig; thepolicystep ofbuild_and_testenforces both. Consumers (PadInput.cpp:27,35fordebug.ignore_host_inputandinput.deadzone,libSceAudioOut/src/AudioOut2Context.cpp:42andlibSceAjm.native/src/Ajm.cpp:42fordebug.trace) guard withIsInitialized()and fall back to defaults, and receive the resolved config after startup (PR #71). The[input]section parsesdeadzoneand a binding table, but the binding table is not consumed (beanportps5-de24). -
PortPS5 startup:
WindowsEntryStubBuilder.cppinvokesPortPS5_Config_Startup_nid_no_patchfrom the loadedlibc.prxafter PRX loading/import binding and before guest initializers or the ELF entry point.libc/src/ConfigStartup.cppreadsapp0/sce_sys/param.jsonrelative to the executable and callsLoader::Initialize. Missing/invalid metadata or invalid configuration fails startup; a missing startup export requires updating libc and fails before guest code. Missing configuration files retain the loader's defaults and warning. Bare executables without libc skip this step. Existing converted executables must be relinked to acquire this startup call.
The 310 switches on main@75a8668 fall into these classes (by name pattern, then reviewed):
| Class | Count | Examples | Disposition |
|---|---|---|---|
Kill switch (APS5_NO_*) |
121 | NO_FILL_HLE, NO_ADJACENT_GENERATION, NO_WRITE_WATCH, NO_PAD_INPUT |
Deleted, together with the path it disables, once its verify mode passes. |
| Alternative behaviour or ordering | 67 | LABEL_* (9), CPU_COPIES, FULL_BARRIERS, IGNORE_DEPTH_TEST, SAVEEXEC_WRITE_FIRST, TIME_SCALE, SPIRV_OPT |
Deleted. One code path is kept. |
| Tuning (sizes, caps, affinity, timeouts) | 35 | HOST_IMPORT_MIB, *_CACHE_ENTRIES, JOB_AFFINITY_MASK, GPU_WAIT_TIMEOUT_MS, FLIP_INFLIGHT |
Becomes a constant or is sized automatically (gpu-driver.md, guest-memory.md). Affinity switches are removed: affinity is recorded, not applied (threading.md). Only HOST_IMPORT_MIB and HEAP_CACHE_MIB survive, as [debug] overrides of the automatic size. |
Forced serialisation (APS5_SYNC_*) |
6 | SYNC_DRAWS, SYNC_FLIP, SYNC_DISPATCH |
Deleted. See the open questions. |
Trace (APS5_TRACE_*) |
39 | TRACE_AUDIOOUT2, TRACE_AJM, TRACE_LABEL |
[debug] trace. |
Capture (APS5_CAPTURE_*) |
3 | CAPTURE_TRACE, CAPTURE_INPUTS, CAPTURE_GPU_COPIES |
[debug] trace. |
Dump (APS5_DUMP_*) |
11 | DUMP_SHADERS, DUMP_IR, DUMP_FRAMES |
[debug] dump. |
| Profile | 2 | PROFILE_DRAW, PROFILE_GPU |
[debug] profile. |
| Validate, verify or watch | 26 | VERIFY_RECIPE, BARRIER_VALIDATE, WATCH_ADDR, SRT_DEBUG |
[debug] validate, [debug] watch. |
That leaves 81 diagnostic switches and 2 size overrides, which survive as typed [debug] values. The other 227 are removed.
This follows the decision table in README.md §Configuration and §Global policy. The configuration files are config/global.toml and config/games/<titleId>.toml, with the sections [display], [input], [workarounds] and [debug]. A typed schema is validated at start-up, and an unknown key is an error. Every [workarounds] key is documented in docs/workarounds.md. APS5_* switches are removed, and kill switches go once their verify mode has passed. Diagnostics and overrides survive only as [debug] values or one PORTPS5_DEBUG variable, and never as behaviour-changing toggles.
Locations and layering. The runtime reads <install>/config/global.toml, then <install>/config/games/<titleId>.toml, then PORTPS5_DEBUG. Later layers override earlier ones key by key. <install> is the directory of the converted executable, and the CLI copies the repository's config/ there at conversion time. global.toml may not contain [workarounds]. PORTPS5_DEBUG may set only [debug] keys, with the syntax key=value;key=v1,v2 (for example trace=audio,ajm;dump=shaders). Booleans there read true/false (also 1/0); watch cannot be set through PORTPS5_DEBUG because its table shape needs a TOML file.
Required top-level keys. schema = 1. In game files, also title_id = "PPSA01342", which must match both the file name and param.json.
Schema. This table is the complete schema; any key not listed here is a start-up error.
[debug] rule. [debug] keys are diagnostic or override only. They are never needed for a passing run, a release run must not set any, and the results JSON records every [debug] key that is set (debug_keys_set, verification.md §4).
| Section.key | Type | Default | Notes |
|---|---|---|---|
display.resolution_scale |
float 1.0–2.0 | 1.0 | Internal render-target scale (gpu-driver.md). Not part of the 1.0 performance bar: the gate is measured at scale 1.0. |
display.present_mode |
enum fifo/mailbox/immediate |
fifo |
Falls back to fifo with a warning if the surface lacks it. |
display.fullscreen |
bool | false | F11 still toggles. |
display.window_percent |
int 25–100 | 60 | Initial window size as a share of the display. AnyPS5 commit 9453f47 hard-codes 60% (libSceVideoOut/include/DisplayWindow.hpp:9). |
input.* |
see input.md | deadzone, mouse_look, mouse_sensitivity, swap_confirm, bindings. |
|
workarounds.<key> |
typed per key | off | Allowed only in game files. The key must be registered in code and documented. |
debug.log_level |
enum error/warn/info/debug |
info |
|
debug.trace |
array of enum | [] |
Categories: audio, ajm, pad, savedata, dialog, np, label, sync, timers, memory, bda, bindless, fiber, exit, … One category per former TRACE_* family. |
debug.dump |
array of enum | [] |
shaders, targets, textures, queue, rejected. IR and frame dumps use debug.recompiler.dump_ir and debug.gpu.dump_frames. |
debug.dump_dir |
path | <install>/dumps |
Must be outside the saves directory. |
debug.profile |
array of enum | [] |
gpu: write the per-frame [FrameTiming] report (about 20 KB of text and an fflush(stdout) per flip) to stdout. Off by default because that write dominates frame time on a console. Per-draw shader profiling uses debug.recompiler.profile. |
debug.validate |
array of enum | [] |
recipes, barriers, copies, shadows, indirect_args. Validation only logs mismatches. It never changes what the runtime produces. |
debug.watch |
array of {addr: u64, write: bool} |
[] |
Replaces WATCH_ADDR and WATCH_WRITE. |
debug.survey_unsupported |
bool | false | Diagnostic survey (README.md global policy): an unsupported draw, dispatch, shader or export is logged once per distinct site (no title strings, guest addresses as module-relative offsets), skipped and counted instead of aborting; a capped summary (per-kind counts plus the first N sites) is written in one call at exit. The run is always fail (§4.2 debug-key rule). Bean portps5-huyy. |
debug.ignore_host_input |
bool | false | For recorded-input replay. Replaces NO_PAD_INPUT. It filters host input and does not change the guest path. |
debug.gpu.host_import_mib |
int | auto | Override of the host-import budget (gpu-driver.md). Replaces HOST_IMPORT_MIB. |
debug.gpu.trace |
bool | false | GPU command tracing (gpu-driver.md). |
debug.gpu.dump_frames |
bool | false | Frame dumps (gpu-driver.md). Replaces DUMP_FRAMES. |
debug.memory.heap_cache_mib |
int | auto | Override of the large-block heap cache cap (guest-memory.md). Replaces HEAP_CACHE_MIB. |
debug.recompiler.dump_ir |
array of string | [] |
Code addresses to dump IR for, or "all" (shader-recompiler.md). Replaces DUMP_IR. |
debug.recompiler.single_lane |
bool | false | Single-lane execution for bisecting (shader-recompiler.md). Replaces SINGLE_LANE. |
debug.recompiler.profile |
bool | false | Per-shader profiling (shader-recompiler.md). Replaces PROFILE_DRAW. |
debug.recompiler.capture |
bool | false | Captures .req files into the local-only corpus (shader-recompiler.md). |
debug.pipeline_cache |
enum on/off/readonly |
on |
Pipeline cache mode (pipeline-cache.md). |
debug.pipeline_cache_verify |
bool | false | Recompiles on a cache hit and compares the SPIR-V (pipeline-cache.md). |
debug.relinker.trace_sse4a |
bool | false | Traces the runtime SSE4a trap (relinker.md). |
debug.threading.dump_futex_owners |
bool | false | Log owner thread IDs of contended futex words on watchdog softlock (threading.md). |
debug.gpu.breadcrumbs |
bool | false | Planned (bean portps5-636k). Per-draw markers for device-loss triage (gpu-driver.md Diagnostics). |
debug.gpu.labels |
bool | false | Planned (bean portps5-ey3w). VK_EXT_debug_utils object names and command labels, numbers and hashes only. |
debug.gpu.capture |
table {frame: u64, count: int} |
none | Planned (bean portps5-bbxe). RenderDoc in-application capture; warns once when RenderDoc is not injected. TOML file only, like watch. |
debug.gpu.validation |
array of enum | [] |
Planned (bean portps5-c5if). core, sync, gpu_assisted, best_practices. |
debug.recompiler.printf |
array of {shader: string, pc: u64, regs: array of string} |
[] |
Planned (bean portps5-7e2a). Injects NonSemantic.DebugPrintf after the instruction at pc; instrumented modules bypass the disk pipeline cache. TOML file only. |
debug.overlay |
bool | false | Planned (bean portps5-h2rv). On-screen frame time and counters. |
Validation. The TOML parser is toml++ (MIT, header-only), vendored and pinned at v3.4.0 (3rdparty/tomlplusplus/toml.hpp, build-toolchain.md).
Each of the following is a start-up error, reported as file:line: key: reason with a non-zero exit before any guest code runs: an unknown section or key; a wrong type, or an out-of-range or unknown enum value; a missing schema, or one newer than the runtime supports; a title_id mismatch; [workarounds] in the global file; a workaround key that is not registered.
Any APS5_* variable in the environment triggers one warning that lists the names and says they are ignored. It is not an error, because stale shells are common.
Code API. A single Config module parses once and exposes typed getters, for example Config::Display().presentMode and Debug::Traces(Trace::Audio). Workarounds are declared with PORTPS5_WORKAROUND(key, type, default, "mechanism"). The macro registers the key with the schema and names the mechanism it toggles. The policy CI job enforces this: no APS5_ string literals and no getenv outside the Config module (which alone may name the APS5_ prefix, to warn about stale variables); test sources under */tests/ are exempt from the getenv check.
Cross-PRX exports. Host configuration symbols under PortPS5::Config::Loader (such as Loader::IsInitialized() and Loader::Get()) are imported by mangled C++ name across dependent replacement system libraries (e.g. libSceVideoOut, libSceAudioOut, libSceAjm.native). To ensure runtime resolution succeeds without NID hashing, nid_patcher preserves PortPS5::Config exports verbatim in libc.prx (docs/spec/build-toolchain.md).
docs/workarounds.md rules. Each entry lists the key; its type and default (the default is always the general behaviour); the mechanism it toggles, in engine-neutral words; why no general fix exists yet; the title IDs and patch pins that set it; the tracking issue; and the removal condition. Key names describe mechanisms, for example copy_kernel_linear_match, and never games. The policy job fails when a key name contains a title name or a title-ID pattern, when a registered key and the docs disagree in either direction, or when a game file sets an undocumented key. A key that no gate title uses at a milestone exit is deleted.
Migration from APS5_*. The fork never contains getenv("APS5_"). Conversion happens as each piece of AnyPS5 main (merged PR #5) is ported in M1:
| Step | Action |
|---|---|
| 1 | Land Config and [debug] first in M1. Wire the policy check (verification.md §1). |
| 2 | Port an AnyPS5 main file. Map every trace, dump, profile and validate read to Debug::* (81 names → about 25 enum values). |
| 3 | For each kill switch or alternative-behaviour switch in the file, keep the default path and delete the other one. If the file's M1 verify run fails without the alternative path, the alternative becomes a candidate [workarounds] key, subject to the rules above. |
| 4 | Replace tuning reads with constants, and add a TODO(auto-size) pointing to the spec that owns the size. |
| 5 | Record every name in a table APS5 name → disposition in the M1 seed, as the audit trail. |
| Peer spec | Keys or contract |
|---|---|
| gpu-driver.md | display.resolution_scale, display.present_mode, debug.gpu.*. Auto-sizing of the former tuning switches (host import, caches, flip in-flight). |
| guest-memory.md | debug.memory.heap_cache_mib. The memory trace category. |
| input.md | The [input] section. |
| audio.md, video-fmv.md, save-data.md | Only debug.trace categories. They have no behavioural keys. Offline NP, PSN and trophy behaviour is in save-data.md. |
| shader-recompiler.md, pipeline-cache.md | debug.dump = shaders, debug.recompiler.*, debug.pipeline_cache, debug.pipeline_cache_verify. Config values never enter pipeline-cache keys, except resolution_scale if it changes pipelines. |
| threading.md | Affinity is recorded, not applied, and not configured. Only debug.trace categories (sync, timers). |
| relinker.md | The CLI copies config/ to <install> at conversion time. debug.relinker.trace_sse4a. |
| build-toolchain.md | toml++ pin. |
| verification.md | The policy job rules. Results JSON records config_sha256 (the resolved config hash), workarounds_set and debug_keys_set. |
| Failure | Handling |
|---|---|
| A config error of any kind | Start-up error with the location. There is no partial start. |
| Missing game file | Globals apply. This is normal. |
| Missing global file | Built-in defaults apply, and one warning is logged. |
| A present mode the surface does not support | Fall back to fifo, with a warning. |
| A debug dump would fill the disk | Dumps stop at a 2 GiB cap per run, and a warning is logged. |
A stale APS5_* environment variable |
Warning. The variable is ignored. |
-
ConfigStartupexercises the production startup export with synthetic metadata and on-disk TOML: per-game and environment GPU profiling enable the first frame report, missing config retains defaults, and invalid config or missing metadata fails without publishing config. -
Unit (hosted):
- Parse a golden file for every key.
- One reject case per validation rule: unknown key, bad type, out of range, workarounds in global,
title_idmismatch, schema too new. - Layering order.
PORTPS5_DEBUGparse and rejection of non-debug keys.
-
policyjob: noAPS5_string literals and nogetenvoutside theConfigmodule (which alone may name theAPS5_prefix, to warn about stale variables); the workaround registry matchesdocs/workarounds.mdin both directions; no title names or title-ID patterns in key names. -
Local regression: each gate title runs with its shipped game file. The results JSON lists the workarounds and
[debug]keys that were set; a pass requires no[debug]keys.
| Milestone | Work |
|---|---|
| M1 | - [ ] Items: - [x] Config, schema, validation and [debug] with unit tests and policy enforcement;- [x] verbatim export of the cross-prx Loader API (IsInitialized, Get) from libc.prx, with unit and death tests (PRs #44, #47);- [x] startup call with the param.json title ID (bean portps5-c06p);- [ ] config/ copy at conversion (same bean);- [ ] display key wiring (bean portps5-dtwf);- [x] results-JSON config fields, written by tools/regress.py from the config files (PR #76). Per-game TOML (F6) is not closed until the open items land. |
| M2 | - [ ] [input] bindings (with input.md; bean portps5-de24). The regression tooling records the config hash (bean portps5-3m3u). |
| M3 | - [ ] Any [workarounds] keys Tomb Raider needs, each with a docs entry. |
| M4 | - [ ] Exit criterion: every [workarounds] key used by a gate title has a docs/workarounds.md entry. |
| M5 | - [ ] Demon's Souls passes with title-specific behaviour only in its documented TOML. |
| M6 | - [ ] The user guide documents every key. docs/workarounds.md is published. |
- Keep one
[debug] serialize_gpuswitch for bisecting ordering bugs? It changes timing, not results, but it conflicts with the rule against behaviour-changing toggles, so it is excluded for now. - A user-override directory, for example
%APPDATA%/PortPS5/config, for players who reinstall. It is not needed for 1.0. - Does
resolution_scalebelong in pipeline-cache keys? Decide with pipeline-cache.md. - Enforce
debug.dump_diroutside the saves directory once save-data.md owns the saves path (M2). Until then the check is non-empty only.