Status: draft v1 · 2026-09-27 · synced with main 2026-10-01
Verification has three layers. Hosted CI has no GPU and never sees game data. Game checks run locally on a maintainer's GPU machine that meets the reference PC tier (PRD §4.4), and only their results are uploaded.
- Environment: Windows runner (pinned
windows-2022) with the pinned MinGW-w64 GCC 15.2 toolchain, downloaded and verified by checksum. Shared bootstrap (submodules, toolchain, ccache env) lives in.github/actions/mingw-setupsoci.ymlandcodeql.ymlcannot drift. - Change detection: a fast
changesjob (ubuntu-latest, ~15 s) diffs the PR/push and setscode_changed. Docs/config-only runs skip both Windows and Python jobs;docs/workarounds.mdcounts as code because the policy check cross-references thePORTPS5_WORKAROUNDregistry. - Caching: ccache key is stable on the toolchain plus build definition (
mingw-gcc15.cmake,mingw-gcc15.sha256,CMakePresets.json,CMakeLists.txt) with a per-run save suffix; the old per-SHA key could never hit and wrote a fresh 1 GB entry every run. MinGW and submodule caches are keyed on their pins. - Build hashes: after tests, CI prints SHA256 of the shipping binaries (
relinker.exe,libSceAgcDriver.prx,agc_shader_replay.exe) to the step summary for provenance. Summary-only: no artifact upload, no game data, missing files warn instead of failing. - Jobs:
| Job | Contents |
|---|---|
| build_and_test | Single Windows job: full configure and build (relinker, every prx module, tools), ctest --preset ci plus the test-count gate (the ci configure preset sets PORTPS5_COMPILE_WARNING_AS_ERROR=ON and PORTPS5_REQUIRE_FFMPEG=ON (#88); the ci test preset runs label unit with 4 jobs and a 300 s per-test timeout, and includes prx_cross_import_check, which runs tools/check_prx_imports.py over the built patched libs and fails when a prx imports a name its provider does not export (#68)), then the golden corpus incrementally (ctest --preset golden and agc_shader_replay --golden; the full build already compiled those targets, so no second runner is needed). Ends with the policy step, the comment linter, and the SHA256 summary. |
| driver-lavapipe | Windows job driver_lavapipe: builds only the targets behind the lavapipe label (warm ccache restored read-only; Vulkan runtime and Mesa downloads cached per pinned version), installs the Vulkan loader (from LunarG's runtime-components zip) and a pinned Mesa lavapipe, both SHA-256-verified before extraction (CPU Vulkan ICD, registered under HKLM\SOFTWARE\Khronos\Vulkan\Drivers, because the loader ignores VK_DRIVER_FILES in the elevated runner process), then runs ctest --preset lavapipe --no-tests=error (label lavapipe, 4 jobs): libSceAgcDriver suites, Recorder and HostImport. Windows because the driver loads vulkan-1.dll and builds only with MinGW. Fixtures GTEST_SKIP when no device exists; ctest counts that as a pass, so the test step runs with -V and fails the job if any suite logs no Vulkan device or VK_EXT_external_memory_host unavailable (the HostImport skip), and a pre-test step fails if the loader or the x64 ICD manifest is missing. No GPU, no game data. |
| unit | Part of build_and_test: ctest over libc, libkernel and relinker tests, including the futex sync tests (Milestone 1). Runs with CTEST_PARALLEL_LEVEL=4: fixtures use timestamp-unique dirs and timing asserts are lower-bounds, so parallel execution is load-safe. |
| recompiler-golden | Part of build_and_test (merged, was a separate runner): replays serialised shader requests through the recompiler: ctest --preset golden (coverage gate plus wave32/64 replay of every decoded class) and agc_shader_replay --golden core/shader/recompiler/tests/golden/corpus (diffs disassembled SPIR-V against checked-in .spvasm goldens). Each module is validated with SPIRV-Tools spirv-val pre- and post-optimizer inside Recompile. The hosted corpus holds only synthetic or hand-assembled RDNA2 shaders written for the project, with no game bytecode. Game-derived shader requests are captured into a local-only corpus on the maintainer machine and replayed by local regression (§2, step 4) with the same tool. |
| policy | Step of build_and_test. Fails on new title-specific code patterns and stray switches: 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 because they verify environment behavior without changing runtime behavior (configuration.md); no title-ID literals in core/ except */tests/, which need realistic IDs as fixture data; no hash-matched kernel tables; the PORTPS5_WORKAROUND registry matches docs/workarounds.md in both directions with mechanism-only key names. Compliance with the rule that docs describe hardware only as the generic reference tier is enforced via PR review checklist. |
| nightly | Scheduled (03:17 UTC) and manually dispatchable. Builds only the targets behind slow-labelled tests and runs ctest --preset slow: full-size fuzz runs too long for the hosted unit job. Today: the uncapped 10^6-operation extent-allocator differential (GuestArenaExtent.FuzzUncapped, ~3 min). The hosted unit job runs a capped variant of the same test, so PRs still cover the invariant. Slow tests are DISABLED_ in the gtest binary and registered with portps5_add_test under label slow. |
| codeql | GitHub CodeQL SAST (C/C++, security-extended query suite). Performs semantic dataflow analysis of the project's own source (core/, tools/) for buffer overflows, integer overflows, use-after-free, and format-string bugs. Third-party submodules are excluded from the build (ANYPS5_ENABLE_SPIRV_TOOLS=OFF) so CodeQL's compiler-interception database never contains third-party code. Results appear in the GitHub Security → Code scanning tab as inline SARIF alerts on PRs. No external service account or token required; uses the automatic GITHUB_TOKEN. Also runs on a weekly schedule so new query packs surface vulnerabilities even with no code changes. |
| gitleaks | Secret and legal-boundary scan of every push and PR diff. Detects private keys, API tokens, and Sony-specific patterns (IDPS keys, .rap/.rif licence content, firmware paths, AES-128 key material) as defined in .github/gitleaks.toml. Extends Gitleaks's built-in provider ruleset. No external token required. |
| doxygen-doc-gate | Runs Doxygen (docs/Doxyfile) on core/libs/prx and core/relinker on every PR and push to main as a required status check. Configured with WARN_AS_ERROR = FAIL_ON_WARNINGS and WARN_IF_DOC_ERROR = YES. Fails if any doc comment contains malformed markup, scanning the entire source tree to report all violations before failing (WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are disabled initially to avoid blocking on inherited pre-existing debt and will be re-enabled incrementally). On failure, build/doxygen_warnings.log is uploaded as the doxygen-warnings artifact. This job does not require the MinGW toolchain and runs on a plain windows-2022 runner with the official Doxygen 1.18.0 zip installed from doxygen.nl (SHA-256 pinned in the workflow). Complements the in-tree Python policy checker (tools/check_comments.py), which enforces PortPS5-specific rules (file-level headers, APS5_VABI doc coverage, TEST() invariant comments) on every changed file. |
| progress-report | Runs tools/progress.py on every PR (no main filter): renders the base and head implementation counts (declared APS5_VABI functions in core/libs/prx, RDNA opcodes vs tools/rdna_isa.txt, counted twice: decoded by the RdnaOpcode enum, and translated, meaning a decoded opcode that core/shader/recompiler/Translation references, an upper bound on real lowering) and posts the delta as a PR comment (progress-comment.yml). Static source scan, no GPU, no game data. Locally, --exports <file> adds an exports block that measures implemented functions against a full per-library export list (for example ps5rs data/stubs.txt; *_nosubmission libraries dropped, _nid_postfix-style suffixes normalised). That file is third-party reference data read from a local path and never committed or used in CI; only counts may be quoted. Full-site render plus badges deploy from main pushes via workflows/progress.yml (needs GitHub Pages enabled). |
| python-quality | Astral toolchain gate for every Python file (tools/, tests/tools/, relinker self-tests): ruff check + ruff format --check and pytest with coverage over tools/ (fail_under = 85, pyproject.toml). Runs on ubuntu-latest via pinned uv (uv.lock committed); versions pinned in pyproject.toml (dependency-groups.dev). CTest keeps running the same suites through stdlib unittest on Windows so hosted unit execution never depends on PyPI. |
- Status as of 2026-10-01: every job above exists in
.github/workflows/.driver-lavapipe(beanportps5-ekx3) first ran green onmainwith PRs #80/#82 (0bb6edf1).mainCI was green through #53 (07b76f75).build_and_testthen failedprx_cross_import_checkon the #68 and #88 merges (eaea7340,1c65844a); PR #94 fixed it by exporting the mount-table API verbatim (beanportps5-sjuv, completed).driver_lavapipestill crashes intermittently inagc_recorder_tests(first seen asRecorderTest.SubmitMakesDeviceWritesVisibleToTheHost; it did not reproduce locally in about 7,350 runs), tracked as beanportps5-3mafuntil a dump is captured.tools/regress.py(conversion layout, launch, results JSON writer, pass rule) landed in PR #76 (beanportps5-3m3u); the frame-check, checkpoint-replay, shader-corpus and save steps of section 2 and the upload are still open. The runtime telemetry core (section 4.3) writes the section 4.1 log, but nothing calls itsStartat process start-up yet, sorunreports a missing log until that wiring lands (beanportps5-w1re). -
driver-lavapipepasses onmain(first green run with PRs #80/#82,0bb6edf1). - Rules: no self-hosted runner on the public repository, and no game data, dumps or saves in any artifact.
Hosted CI has no GPU and no game data, so reliability comes from checks that run on synthetic inputs. Epic bean portps5-7n6b.
| Check | What it catches | Where | Bean |
|---|---|---|---|
| Vulkan validation layers on the lavapipe job | Invalid Vulkan usage that lavapipe tolerates | driver_lavapipe |
portps5-977n |
| Synthetic guest ELF end to end | Broken loader, export tables or prx start-up, found today only with a dump | build_and_test (ctest) |
portps5-ktrt |
| PE structure validation (pefile) | Bad alignment, missing unwind data, unresolved imports in relinker output | python_quality and ctest |
portps5-s9zz |
| Codegen metrics per PR | SPIR-V instruction-count and relinker residual regressions with all tests green | PR comment, like progress-report |
portps5-g0n7 |
| Linux sanitizers (ASan, UBSan, TSan) | Memory errors and data races in host-portable code; MinGW has no sanitizer runtimes | new ubuntu-latest job |
portps5-pbj2 |
| Fuzzing | Crashes on malformed ELF, PM4 or shader input | Linux, time-boxed per PR, longer in nightly |
portps5-axx6 |
clang-tidy and -Wcast-function-type |
Function-pointer casts and calling-convention mismatches across APS5_VABI exports |
build_and_test |
portps5-hpx7 |
| C++ coverage report | Untested modules (report only) | scheduled | portps5-02a4 |
| CPU microbenchmarks | Host hot-path regressions (trend only) | nightly |
portps5-i58o |
Deliverables:
- Vulkan validation layers on the lavapipe job (bean
portps5-977n). - PE structure validation of relinker output (bean
portps5-s9zz). - Deterministic codegen metrics per PR (bean
portps5-g0n7). - C++ coverage report (bean
portps5-02a4). - Synthetic guest ELF end to end (bean
portps5-ktrt). - Unsupported() counts and compat status tiers (bean
portps5-ba7d). - Linux sanitizer job (ASan, UBSan, TSan) (bean
portps5-pbj2). - Fuzzing of ELF, PM4 and shader decoders (bean
portps5-axx6). - clang-tidy and
-Wcast-function-type(beanportps5-hpx7). - Local nightly regression on the maintainer machine (bean
portps5-e6xg). - CPU microbenchmarks with a nightly trend (bean
portps5-i58o).
Game-run checks stay local. A scheduled job on the maintainer machine runs boot, perf scenes and the baseline compare in a locked environment, and records results JSON only (bean portps5-e6xg; perf scenes portps5-52bs). Unsupported() abort counts feed the compatibility status tiers (bean portps5-ba7d). Captured GPU traces and frame references contain game data, so they never reach hosted CI.
tools/regress runs, for each gate title:
- Boot: conversion plus launch, reaching the title screen within a timeout.
- Checkpoint loads: it loads each stored save (one per chapter or boss region), then replays about 60 s of recorded input.
- Frame checks: it compares deterministic frames (menus, the first frames of each FMV) against per-title references. References are keyed by GPU vendor and driver major version. A comparison passes at SSIM ≥ 0.99, so small driver-level differences don't fail the check.
- Local shader corpus: it replays the game-derived shader corpus, which never leaves the machine, through the recompiler and
spirv-val. - Save round-trip: save, quit, relaunch, load.
- Telemetry: it records frame-time stats, stalls, watchdog events, A/V offset and pipeline-creation counts.
Saves, recorded input, reference frames and the local shader corpus all stay on the maintainer's machine. Only metrics and hashes are published.
- One complete start-to-credits playthrough per gate title, with a warm pipeline cache and 1920×1080 or higher output.
- Runtime telemetry is always on. It logs frame times, stalls (gaps of more than 1 s between presents) and crashes. The watchdog flags a softlock when there is no present, or no guest thread progress, for more than 30 s. There is no loading-screen exemption (PRD §4.3 definitions).
- A short clip is captured automatically on failure. It contains game footage, so it stays local and is never attached to issues, PRs or results JSON.
- Pass: average of at least 30 fps, 1% low of at least 20 fps, and 0 crashes and 0 softlocks (PRD §4.3).
Results are uploaded by a local script as a PR to compat/results/, or as a release asset. The schema:
{
"schema": "portps5.results/1",
"commit": "<git sha>",
"run_type": "regression | full_run",
"title": { "id": "PPSA01342", "region": "EU", "patch": "01.004", "name": "Demon's Souls" },
"host_tier": "upper-mid-tier (derived from bench scores)",
"bench": { "cpu_cinebench_r23_multi": 0, "gpu_timespy_graphics": 0 },
"gpu_vendor": "nvidia | amd | intel",
"driver_version": "<string>",
"resolution": "1920x1080",
"pipeline_cache": "warm | cold",
"fps": { "avg": 41.2, "p1_low": 27.5, "min": 18.0, "stalls": 3 },
"av_offset_ms_max": 42,
"audio_underruns_per_10min": 0.2,
"audio_device": "wasapi-default | none",
"spirv_compilations": 0,
"pipeline_creations_after_warmup": 0,
"warmup_ms": 4200,
"capture_split": 12,
"write_faults": 3100,
"config_sha256": "<hash of the resolved config>",
"workarounds_set": [ "copy_kernel_linear_match" ],
"debug_keys_set": [],
"crashes": 0,
"softlocks": 0,
"checkpoints": [ { "name": "boss-2", "result": "pass", "frame_hash_ok": true } ],
"save_roundtrip": "pass",
"result": "pass | fail",
"log_sha256": "<hash>",
"duration_s": 36000
}- Pass rule:
debug_keys_setis empty,crashes= 0,softlocks= 0, everycheckpointsandfmventry ispass,save_roundtripis notfail, and in warm-cache runsspirv_compilations= 0 andpipeline_creations_after_warmup= 0 (tools/regress_metrics.pyfail_reasons). A run that fails any of these reportsresult: "fail". Full runs add the bar in §4.2. Not enforced yet: PRD F3's limit of at most 1 audio underrun per 10 minutes;fail_reasonsrecordsaudio_underruns_per_10minbut never fails on it (beanportps5-2udb). - Derived fields:
audio_underruns_per_10min=n * 600 / max(duration_s, 60)over the summedaudio.underruncounts.fps.min= 1000 / the slowest non-stall frame time. - Check inputs:
checkpoints,fmvandsave_roundtripcome only from--checks-file, which no step writes yet. Until the frame-check and save steps exist,save_roundtripdefaults tonot_run, so everyfull_runfails on it (beanportps5-3m3u). - The results JSON carries no personally identifying hardware detail beyond GPU vendor, driver and tier.
The runtime writes <install>/logs/telemetry.jsonl, one JSON object per line, always on and independent of any [debug] key. tools/regress.py reads only the whitelisted fields below and ignores unknown events and keys, so no game text can reach the results. The first record must be run.start; malformed lines are errors, never skipped, and blank lines are ignored.
ev |
Fields | Used for |
|---|---|---|
run.start |
schema, resolution (WxH), pipeline_cache (warm/cold), audio_device |
header fields |
frame |
dt_ms (time since the previous present), t_ms (monotonic ms since run.start, optional for older runtimes) |
fps, stalls, softlock gaps, tail hang check |
heartbeat |
t_ms (runtime clock, written about once per second by the watchdog thread) |
tail hang check |
softlock |
idle_ms, reason (1 present, 2 guest thread; not consumed) |
watchdog report (no present, or no guest thread progress, over 30 s) |
crash |
none | crashes. No producer yet; crashes are inferred from a missing run.end or a non-zero exit (§4.2). |
audio.underrun |
n (default 1) |
audio_underruns_per_10min |
av.offset |
ms |
av_offset_ms_max (absolute maximum) |
video_latency_ms |
ms |
informational, not consumed |
warmup.end |
warmup_ms |
warmup_ms; later pipeline.create events count. Producer not wired (bean portps5-w1re). |
spirv.compile, pipeline.create |
none | spirv_compilations, pipeline_creations_after_warmup. Producer not wired (bean portps5-w1re). |
run.end |
capture_split, write_faults |
clean end marker and totals |
run.start and run.end are written by Start and Shutdown, which no real run calls yet (§4.3), so capture_split and write_faults stay 0 in real results until that wiring lands.
A survey run ([debug] survey_unsupported) lists everything missing in one pass. Its exit summary is grouped by kind: unsupported exports by library and NID, shader failures by opcode or construct, and rejected draws and dispatches by reason. Each entry carries a count and its first site. The summary is local only. A metrics-only version (counts per kind, the NID or opcode names, which are project data, and no game strings) may go into an issue or a bean. The results JSON of a survey run is always fail, because a [debug] key is set. Use it after the static checks, which are cheaper: the import inventory (portps5-zadg) and the local shader corpus replay (§2, step 4).
- Statistics: stalls (
dt_ms> 1000) are excluded fromfpsand counted infps.stalls.p1_low= 1000 / mean of the slowest 1% (rounded up, at least one frame) of the remaining frame times.duration_sis the span of presented frames, the quantity the 30 minute rule uses. - Softlocks:
softlocksis the larger of the watchdog'ssoftlockevents and the count of present gaps over 30 s, so a silent watchdog cannot hide one. For a run the runner ended at its time limit, more than 30 s between the last present and the lastheartbeat, both on the runtime's clock (never the runner's wall clock), is one more softlock, because no laterframerecord carries that gap. A title that never presented is measured fromt_ms0, with no loading exemption. The check is skipped without heartbeats (older runtime) and when frames carry not_ms, since there is then no common clock. - Crashes:
crashevents, plus one if the title ended by itself withoutrun.endor with a non-zero exit code. A run the runner ends at its time limit is not a crash.runpersists{killed, exit_code, wall_ms}tologs/runner.json, andreportreuses it, so re-reporting a timed run gives the same verdict. - Config:
config_sha256hashes the canonical JSON of<install>/config/global.tomloverlaid bygames/<titleId>.toml;workarounds_setlists enabled[workarounds]keys anddebug_keys_setevery[debug]key present. The runner refuses to run withPORTPS5_DEBUGset, so the hash reproduces from files. Enabling[debug] profilefor a run therefore makes itfail, by the pass rule above. Known gap:preparecopies noconfig/into the install dir (its docstring says it does), soconfig_sha256hashes{}until that is fixed. - Additive fields:
fail_reasons(names of failed rule fields only),fmv(per-FMVpass/failfrom the "FMV played" rule in video-fmv.md).save_roundtripmay benot_run, which fails afull_run.checkpoints,fmventries andsave_roundtripcome from--checks-file, written by the frame-check and save steps. - Full-run pass: all rules above plus average ≥ 30 fps, 1% low ≥ 20 fps,
duration_s≥ 1800, resolution ≥ 1920×1080, warm cache andsave_roundtrip=pass. - Layout:
preparerunsrelinker --windows(never--skip-sce-module, which crashes guest libc++ code), copies the runtime libs and linksapp0to the dump (read-only).runlaunchesgame.exewith the install dir as cwd, sends stdout and stderr to files it never reads, and refuses any path inside the repository.
Run locally (Windows, release preset, C:\mingw64\bin first on PATH; paths are placeholders, quote them):
python tools/regress.py prepare --dump "<dump dir>" --install "<out dir>" --relinker build\release\core\relinker\relinker.exe --libs build\release\core\libs\libs, then python tools/regress.py run --install "<out dir>" --duration-s 2100 --run-type full_run --title-id <id> --region <r> --patch <p> --name "<n>" --commit <sha> --gpu-vendor <v> --driver-version <d> --bench-cpu <n> --bench-gpu <n> --checks-file "<checks.json>". Review the written compat/results/<titleId>/<commit>-<run_type>.json (metrics, hashes, pass/fail only) before opening a PR. Raw logs, stdout and saves stay in <out dir>.
- Core:
include/telemetry/Telemetry.hppis pure and clock-injected (Log,Watchdog,Sampler);src/Telemetry.cppadds the file sink (<install>/logs/telemetry.jsonl, flushed per record), one 1 s polling thread and the process-wide instance. Telemetry is always on and uses no[debug]key or environment variable. - Interface (
include/telemetry/TelemetryRuntime.hpp, verbatim_nid_no_patchC exports, no-ops beforeStart):Start,NotePresent(frame record plus watchdog heartbeat),NoteGuestProgress,Event(numeric-only:spirv.compile,pipeline.create,warmup.end;dialog.openis planned: the dialog modules log text only and do not emit it),SetAudioSource,SetVideoLatencyMs,SetFmvWindow,SetDiagnosticsHook,Shutdown(writesrun.endand releases the log file handle). - Watchdog: once per second it writes a
heartbeatrecord (runtime clock) and checks both heartbeats. No present for more than 30 s, or, onceNoteGuestProgresshas been called at least once, no guest progress for more than 30 s, writes asoftlockevent (idle_ms,reason1 = present, 2 = guest thread), runs the diagnostics hook (per-queue state), writesrun.endand aborts throughUnsupported. It never skips work. Each half stays off until its heartbeat is first seen (NotePresent,NoteGuestProgress), so boot, a long first load or an unwired caller cannot false-positive; a title that never presents is caught bytools/regress.py(silence from run start to the kill).run.endis final: theLogdrops every record after it, and exports are no-ops afterShutdown. - Audio and A/V: the single mixer (PR #45) registers its
GetUnderrunsandGetLatencyMsas the audio source (process-wide, so a registration made beforeStartis kept; the same holds forSetDiagnosticsHook); the sampler emitsaudio.underrunwith per-second deltas, and while an FMV window is openvideo_latency_msandav.offset= audio latency minus video latency (video-fmv.md). - Tests:
telemetry_core_tests(GoogleTest, synthetic clock, no sleeps): record schema, first-present baseline, 30 s boundary, latch, present check gated on the first present,run.endfinality, synthetic stalled thread, underrun deltas, A/V offset.telemetry_runtime_tests: Start/Shutdown lifecycle on disk, nothing afterrun.end, an audio source registered beforeStartis sampled. - Open wiring (bean
portps5-w1re): theStartcall at process start-up (config is now loaded at startup byPortPS5_Config_Startup_nid_no_patch, PR #71, soStartcan follow it), the presenter'sNotePresentandSetVideoLatencyMscalls (AGC driver work in flight), and a guest-threadNoteGuestProgresssite. A watchdog abort ends withsoftlockplusrun.end, whichtools/regress.pycounts as one softlock and no crash.
Target design for the performance track (ROADMAP.md "Performance track", PRD §4.5). Nothing here is implemented yet. All fields are additive to portps5.results/1, and an older runtime that does not write them produces results without them.
- Frame-time percentiles (bean
portps5-rrll):frame_ms: { p50, p90, p99 }over the same non-stall frames asfps. - Baseline compare (bean
portps5-rrll):tools/regress.py compare --baseline <json> --result <json>prints per-field deltas and exits non-zero on a regression. Both results must match on title pin,run_type,host_tier,resolution,pipeline_cache,config_sha256, the build (build: { preset, flags_sha256 }, the CMake preset and a hash of the effective compiler flags) and the run protocol (protocol: { scene, duration_s }, the perf-scene id and its fixed duration). The compare refuses when any of these differs or is missing from either result; only the commit and the measured fields may differ. This is the mechanical form of the rule that a performance claim needs the same build flags and run protocol (.agents/rules/testing.md). Proposed thresholds, to be tuned with run-to-run noise data:fps.avgdown more than 3 %,fps.p1_lowdown more than 5 %, or any violation counter up. The compare reads only results JSON and never uploads. - Frame breakdown (bean
portps5-hfiw): optional numeric fields on theframerecord,cpu_ms(guest frame CPU time until submit),rec_ms(driver record and submit),gpu_ms(Vulkan timestamp queries around the frame's submits, converted withtimestampPeriod),wait_ms(CPU blocked on a GPU fence) andpresent_ms(blocked in present). The results JSON getsframe_breakdown_ms: { cpu, rec, gpu, wait, present }as per-frame means. A device without timestamp support on the queue omitsgpu. The fields are numbers only, so no game data reaches the log. - Violation counters (bean
portps5-aifo): a numeric-onlyperf.violationevent withkind(1 = P1 CPU wait or readback not requested by the guest, 2 = P2 Vulkan object creation per draw/dispatch, 3 = P3 compile on the submit thread, 5 = P5 full compare or copy of guest memory proven unchanged) andn. These are counted only afterwarmup.end. The results JSON getsinvariants: { p1, p2, p3, p5 }. They are reported, not part of the 1.0 pass rule (PRD §4.5). - Run metadata (bean
portps5-g2ll):run.startadds a build preset id, the short commit baked at build time, the Vulkan vendor id, driver version, present mode and resolution scale.tools/regress.pyprefers these over its command-line flags and warns when they disagree, so the compare's matching rule uses the runtime's own values. - Crash producer (bean
portps5-yhj7): a top-level unhandled-exception filter writescrash { code }andrun.endbefore the process ends, socrashesno longer depends on exit-code inference. - Memory (bean
portps5-ybq3): a per-second numericmemevent (VRAM used and budget fromVK_EXT_memory_budget, guest committed bytes, host-import bytes, staging bytes); the results JSON getsmemory_peak_mb. - Stall attribution (bean
portps5-26s5): aframerecord over the stall threshold carries a numericcause(compile, load or I/O, GPU wait, guest), from the breakdown and the violation counters; the results JSON getsstalls_by_cause. - Boot and compile timing (beans
portps5-pk9m,portps5-tg51):boot_ms(process start to first present), andshader.compile_msandpipeline.create_msevents with totals and p99 in the results JSON. - Present latency (bean
portps5-jy7n): whereVK_KHR_present_idandVK_KHR_present_waitexist, theframerecord carriespresent_to_display_ms(number, milliseconds: fromvkQueuePresentKHRfor that frame'spresent_iduntilvkWaitForPresentKHRreturns; omitted when the extensions are missing). - Present latency in results (bean
portps5-jy7n): the results JSON getspresent: { to_display_ms_p50, to_display_ms_p99, interval_jitter_ms }(milliseconds; jitter is p99 minus p50 of the intervals between successive present completions, over the same non-stall frames asfps;tools/regress_metrics.pycomputes them from theframerecords and omitspresentwhen no record carries the field). Reported, not part of the pass rule. - Crash reports (bean
portps5-cmyp): beyondcrash { code }, a locallogs/crash-<t>.txtwith a classified, symbolisable stack (guest image, prx or host, module and offset), the last records from an in-memory ring buffer and a minidump. Only the numericcrashevent reaches the results JSON. - Dialogs (bean
portps5-7qon): each dialogOpenemits the numericdialog.open { kind }event, and no dialog logs guest text outside[debug]tracing.
PortPS5 standardizes on GoogleTest (GTest) and GMock for all unit, integration, and subsystem tests, replacing ad-hoc standalone main() executables.
- Unified Test Suites: Instead of separate executables for every individual test function, tests are grouped into cohesive suite binaries (
kernel_tests,libc_tests,relinker_tests,shader_recompiler_tests,audio_tests) linked againstGTest::gtestandGTest::gmock. - Expression Decomposition: All test assertions use GoogleTest macros (
EXPECT_EQ,EXPECT_NE,ASSERT_TRUE,EXPECT_THAT) so that failures report file, line, expression, and actual vs expected values (including formatted SCE/errno error codes). - Death Testing (
EXPECT_DEATH): Fatal errors incore/call the logging abort path (APS5_ABORT). GoogleTest death tests verify that invalid guest pointers, misaligned addresses, or unsupported parameters abort cleanly with the expected log message without crashing the test runner process. - Hardware/System Mocking (
GMock): Subsystems depending on external hardware (Vulkan physical devices, SDL2 audio streams, DualSense gamepad endpoints) use GMock classes to simulate device state, timing, and error conditions deterministically. - Reference Test Porting & Ecosystem Ingestion: Over 55,000 lines of subsystem tests from KytyPS5, sandbox security/threading tests from SharpEMU, FreeBSD 12 kernel/libc tests (
kqueue,umtx,mmap), Mesa ACO RDNA2 instruction vectors, Wine NTDLL synchronization/memory tests, and shadPS4/RPCS3 container and ATRAC9 media suites are systematically adapted into PortPS5 GoogleTest suites. All Title IDs are scrubbed to generic synthetic constants (e.g.PPSA00000) per legal and policy rules (TESTING.md).