Markdown files are no longer committed

The docs, the README, plans and notes are kept and updated on this machine
only: .gitignore ignores *.md, and the 37 tracked files leave the index
(they stay on disk). No build or test reads them.
This commit is contained in:
Mihawk committed 2026-09-29 20:41:40 -04:00
1 parent a655e10b3e
commit 8396c1bf5d
38 files changed
+4 -13121

No files matched your search

+4
View File
@@ -42,3 +42,7 @@ __pycache__/
# configured copy under build/, which is not committed; an older build script also
# copied it here, and a generated file must never be able to dirty the tree.
config.h
# Markdown (the docs, the README, plans and notes) stays on this machine: it is
# not committed or published. The files are kept and updated locally.
*.md
-138
View File
@@ -1,138 +0,0 @@
# Agent instructions
PS5 RetroArch is a native RetroArch homebrew application for jailbroken
PlayStation 5 consoles. It gives the console a libretro frontend it was never
shipped with, so the library of emulators and games that already runs through
libretro also runs here.
## Read order
Read in this order and stop as soon as you have what you need:
1. This file. Frozen.
2. `docs/PLAN.md` — the gates, the milestone map, the invariants that constrain
the code, and the index of the reference files. About 150 lines, once per
session.
3. `docs/ACTIVE.md` — the volatile state: the current step, the next actions,
the blockers, the last verified runs. Read last, before you start work.
4. On demand only, when the task needs it: `docs/REFERENCE.md` (the step ladder,
the workflow, the environment), `docs/FINDINGS.md` (the evidence behind each
invariant), `docs/TESTING.md`, `docs/DEPLOYMENT.md`,
`docs/TROUBLESHOOTING.md`, `docs/GPU_PATH_CRITERIA.md` (the acceptance
criteria for the Vulkan route), `docs/PHASE_LOG.md` (append-only run logs),
`docs/RELEASING.md` (what a release carries and the order of its steps).
Never read a long log end to end to answer a status question: `docs/ACTIVE.md`
and each log's own summary say what passed.
## Volatility contract
| File | Rule |
| --- | --- |
| `AGENTS.md` | Frozen. Change only when the workflow itself changes. |
| `docs/PLAN.md` | Static. Edit only when a gate, a milestone or an invariant changes. Never record progress here, and keep it short: it is read every session. |
| `docs/ACTIVE.md` | Volatile. Rewrite in place; keep it under about 120 lines. |
| `docs/PHASE_LOG.md`, `docs/FINDINGS.md` | Append-only. New dated entries at the end; never rewrite an existing one. |
| `docs/REFERENCE.md`, `docs/TESTING.md`, `docs/DEPLOYMENT.md`, `docs/TROUBLESHOOTING.md` | Stable. Edit when the specification or the procedure changes. |
| Any other `docs/*.md` | Stable. Normal edits. |
Progress, run results and "done" markers belong in `docs/ACTIVE.md` or
`docs/PHASE_LOG.md`, never in the plan.
## Prompt caching
The agent's context is cached by exact prefix: only what sits *before* the new
material matters. Keep that part byte-identical between turns.
- Append, don't edit. Adding to the end of a file or a conversation is cheap;
changing text already in context invalidates everything after it.
- Never put a date, a version or a "last updated" line at the top of a file that
is read first. A date belongs in the volatile file, which is read last.
- Keep the read order fixed and never reflow, renumber or bulk-rename: a
reorder or a reformat changes the prefix exactly as a rewrite does. Add a new
on-demand document to the list when it is created, once.
- Keep `docs/ACTIVE.md` small: it is the only file expected to change every
session, so its size is paid on every session.
- One fact, one home. Never copy status into the plan; link to `docs/ACTIVE.md`
or a log instead.
- Batch documentation edits. One write-up at the end of a step beats continuous
small edits, which invalidate the cache repeatedly.
- Long files are appended to and searched, never rewritten, never read whole.
## Your task
The task in your prompt is the only task. `docs/ACTIVE.md` describes what the
project is doing; it is context, not an assignment, and its "Next" list is not a
queue. When the prompt and `docs/ACTIVE.md` disagree, the prompt wins: do not
start the active file's next step, and say in your report that you noticed.
## Work loop
One step per commit, and no step is done until it is verified.
1. Read the prompt, then `docs/ACTIVE.md`. Inspect the workspace: it is
authoritative. An earlier turn's narration, a summary or a compacted
conversation is a claim, not a fact.
2. Choose the smallest step that makes real progress, and say what would prove
it before building it.
3. Implement it, then run the gates.
4. Record the evidence: the command, the result, and the artifact it produced.
5. Commit it with that evidence, and write the step up once, in
`docs/ACTIVE.md`, plus a dated entry in `docs/PHASE_LOG.md` when it lands.
Work that cannot be verified here is never committed as if it were: park it in
`parked/` with the plan that would finish it, and say so.
## Gates
| Gate | Purpose |
| --- | --- |
| `format` | Format, lint and static checks |
| `unit` | Unit tests |
| `build` | Build every shipped target |
| `integration` | Integration or end-to-end tests |
| `evidence` | Replay and compare recorded evidence |
`tools/verify.sh` holds the commands — its only home — and runs them in that
order, failing fast. A failing gate is fixed before the next step starts; a red
gate is never carried forward. What each gate means, and how to add a test:
`docs/TESTING.md`.
## Evidence
- Evidence is a committed artifact, not a claim: a machine-readable capture from
the console (a `klog` run record, an ELF symbol/import dump, a staged-tree
manifest) plus the expected result it is compared against. Every step's
acceptance names the artifact and the command that reproduces it, and the
commit message carries both.
- An unexplained failure keeps the gate red. A tolerated one is named in
`docs/ACTIVE.md` with its reason, and removed when the reason is gone.
- Write logs factually: what was run, what it returned, what it proves, with
failures at the same detail as successes.
## Long-running work
Keep one completion objective for work that spans many turns, and let it drive
the rounds.
- State it as something finishable and checkable, not a direction: for example
"a release ZIP whose `eboot.bin` loads as a title on the 6.02 console, draws the RGUI
menu and returns the driver identity recorded in `docs/FINDINGS.md`".
- Each round: inspect the workspace, make concrete progress, verify it, report
what remains. Never restart finished work.
- Mark it complete only with the evidence it names. A blocker is a concrete
condition that persisted across several rounds with the same cause; difficulty,
uncertainty or remaining work is not a blocker.
- After a summary or a compaction, re-read `docs/ACTIVE.md` and inspect the
workspace before acting.
## Working rules
- Commit finished, verified work. Never push and never rewrite published history
unless the human asks.
- Keep generated and environment files out of the diff: `vendor/`, `build/`,
`dist/`, `.deps/`, `.env`, `*.elf`, `*.so`, `klog/`.
- Do not add a toolchain flag, a dependency or a version pin to a shared script
without saying why in the write-up. Never commit a secret, a key or a client's
data. The console address, the FTP credentials and anything read from the
console stay in the ignored `.env` and in a git-ignored capture directory.
-889
View File
@@ -1,889 +0,0 @@
# PPSSPP native Vulkan core: technical implementation and handoff plan
## Table of contents
1. [Decision and completion objective](#1-decision-and-completion-objective)
2. [Scope, permissions and branch handoff](#2-scope-permissions-and-branch-handoff)
3. [Source identities and evidence discipline](#3-source-identities-and-evidence-discipline)
4. [Existing RetroArch foundation](#4-existing-retroarch-foundation)
5. [Platform capability matrix](#5-platform-capability-matrix)
6. [CPU, JIT and memory integration](#6-cpu-jit-and-memory-integration)
7. [Native core build and loader contract](#7-native-core-build-and-loader-contract)
8. [Libretro Vulkan interface and ownership](#8-libretro-vulkan-interface-and-ownership)
9. [Confirmed current driver blockers](#9-confirmed-current-driver-blockers)
10. [Additional graphics risks requiring investigation](#10-additional-graphics-risks-requiring-investigation)
11. [Driver artifact selection and concurrent development](#11-driver-artifact-selection-and-concurrent-development)
12. [Implementation milestones and acceptance gates](#12-implementation-milestones-and-acceptance-gates)
13. [Diagnostics, measurements and failure recovery](#13-diagnostics-measurements-and-failure-recovery)
14. [Console test matrix](#14-console-test-matrix)
15. [Files, commands and proposed deliverables](#15-files-commands-and-proposed-deliverables)
16. [Open questions and first-session checklist](#16-open-questions-and-first-session-checklist)
17. [References and limits of this assessment](#17-references-and-limits-of-this-assessment)
## 1. Decision and completion objective
**The environment is promising enough to start PPSSPP integration. The currently
inspected PS5_Vulkan implementation cannot run the normal upstream PPSSPP Vulkan
renderer unchanged.** There are known rendering requirements that it explicitly
refuses, including a combined depth/stencil attachment needed during device
initialization. This is more specific than saying the workload is untested.
The user requests a native PPSSPP libretro core using the existing PS5 RetroArch
pipeline and their Vulkan driver. This document is a technical handoff, not a
claim that a port has been built or tested. No PPSSPP build, console launch or
driver modification was performed while preparing it.
The implementation objective is:
> A reproducibly built, ABI-checked native `ppsspp_libretro.so` with matching
> metadata and required assets, loaded by the existing native core loader, using
> verified x86-64 JIT execution and PPSSPP's Vulkan hardware renderer against the
> exact statically linked PS5_Vulkan artifact identified in the report; selected
> PSP workloads render correct gameplay with working audio/input, no unexpected
> GPU refusals or command-buffer failures, and return to XMB and another core
> without corruption, deadlock or a crash. The console owner confirms the result.
A bounded initial gameplay milestone is acceptable; document the exact tested
content and unsupported cases. Do not label the core generally compatible or
full-speed from a boot screen or one simple workload.
Distinguish three achievements in all reports:
- **Core/platform bring-up:** native loading, dependencies, memory, CPU execution.
- **Software-rendered PPSSPP with Vulkan presentation:** potentially useful as an
explicitly labelled diagnostic, but does not satisfy hardware-rendered PSP.
- **PPSSPP Vulkan hardware rendering:** GPU commands originate in PPSSPP's renderer
and produce PSP graphics; RetroArch presents/composites the resulting image.
Finishing an entire Vulkan 1.0 milestone is not a necessary scheduling dependency
if the PPSSPP-specific subset can be implemented and verified sooner. Conversely,
a command-count audit or a milestone label is not proof of PPSSPP compatibility.
Vulkan 1.3 is not a prerequisite established by this investigation.
## 2. Scope, permissions and branch handoff
Read `AGENTS.md`, `docs/PLAN.md`, then `docs/ACTIVE.md` in this repository before
implementation. The active file's previous thumbnail/input work is context, not
an instruction to resume that unrelated work. This handoff concerns PPSSPP.
The next implementation agent should create a RetroArch branch **before code
changes**, preferably `codex/ppsspp-native-vulkan`. Inspect the working tree and
existing branches first; do not overwrite changes, reset history or assume the
branch does not exist. Example, only after inspection:
```bash
git status --short --branch
git branch --list codex/ppsspp-native-vulkan
git switch -c codex/ppsspp-native-vulkan
```
The planning task itself does not create that implementation branch. Preserve
this document if it is still uncommitted when switching branches.
The user previously authorized driver fixes provided PS5_Vulkan documentation
is updated and changes are committed in that repository. They also explicitly
warned that they are actively modifying PS5_Vulkan. Therefore:
- Inspect its working tree before any driver work; coordinate overlapping files.
- Do not absorb the user's unrelated modifications into an agent commit.
- Prefer an isolated driver worktree when implementing an independent fix, after
deciding which source state must be included. An ordinary worktree from HEAD
does **not** contain the user's uncommitted changes.
- Driver changes and their evidence belong in PS5_Vulkan; frontend/core changes
belong here. Track the dependency by exact commit and artifact hashes.
- Never silently replace the known-good frontend driver snapshot with whichever
archive happens to be newest.
- Do not push unless the user asks.
Current deployment policy: uploads are authorized, but check that the console is
idle and preserve user data. A new launch requires the user's intervention or a
new explicit authorization. Historical permissions for earlier tests are not an
instruction to launch now. Never terminate another title. Start capture before
an authorized launch and preserve logs before restarting after a failure.
Keep XMB as the default, RGUI selectable, and `video_ps5` registered/selectable as
the CPU fallback. Do not claim a completed Vulkan step by silently selecting a
software renderer or suppressing a driver refusal. Preserve the existing logging
policy: useful logs, no periodic synchronous logging that reintroduces hitches.
## 3. Source identities and evidence discipline
Assessment snapshot, recorded during the September 2026 handoff:
| Component | Inspected identity |
| --- | --- |
| PS5_RetroArch HEAD | `b1dced92108a70ff56c20d0e97f5cfe87797901a` |
| PS5_Vulkan HEAD | `9d3a025f68ad8c0b608b6814bec844aa252f6710` |
| Driver working tree | Modified `driver/ps5vk_image.c`, `src/diagnostics.cpp`, `tools/check-mip-layout.sh`; untracked `jobs/v0-wide2/` |
| Upstream PPSSPP source reviewed | `f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad` in `hrydgard/ppsspp` |
| Local temporary upstream excerpts | `/tmp/ppsspp-current-review/`, with `REVISION`; disposable, not a build input or committed evidence |
The PPSSPP revision above is an **assessment reference**, not an approved release
pin. Confirm which upstream repository/revision will be used for the actual
libretro build. Compare it with the user's older PS5 PPSSPP port before choosing.
Pin sources, submodules, metadata and assets reproducibly; record digests.
Driver restrictions below are working-tree source observations. Recheck them
because development is concurrent. Local source presence, archive contents and
console execution are three different facts. Do not substitute one for another.
Use these evidence classifications:
- **Measured:** named console probe or accepted run, with its scope and artifact.
- **Source-confirmed:** explicit code path or rejection in the inspected revision.
- **Inference:** likely integration consequence that still needs a reproducer.
- **Unverified:** no relevant workload result exists yet.
The Eden probe documents contain earlier failed approaches followed by newer
successful direct-memory results. Read the dated amendments and current evidence;
do not repeat the old anonymous-memory ceiling as the total available RAM.
Some project plan text also predates the accepted Vulkan frontend. Consult current
source and evidence rather than turning stale prose into a new implementation.
## 4. Existing RetroArch foundation
Already accepted by the console owner, within the tested workloads:
- Native frontend title, XMB and selectable RGUI.
- RetroArch Vulkan presentation through statically linked PS5_Vulkan.
- Native stereo audio, controller input, analog menu navigation and remapping.
- Filesystem browsing, configuration loading/saving and writable title folders.
- Native dynamic core loading, metadata discovery and failed-load recovery.
- FCEUmm, mGBA, Snes9x, FBNeo and Genesis Plus GX gameplay in recorded tests.
- Corrected software-frame colour handling and clean close-content transitions.
- Fixed XMB large-list allocation handling and removed periodic logging hitches.
- Latest accepted thumbnail RGBA handling and controller recovery after defaults.
This establishes a reusable frontend, **not** hardware-core Vulkan compatibility.
All currently accepted emulator cores produce software-rendered frames.
Relevant implementation files:
| File | Purpose |
| --- | --- |
| `tools/build-snes9x.sh` | C++ native core build example, source/info pinning, local destructor registry, ABI report |
| `tools/build-mgba.sh`, `tools/build-fbneo.sh`, `tools/build-genesis-plus-gx.sh` | Other native port/build precedents |
| `tools/build-title.sh` | Builds current cores, generates import surface, links driver archives and stages title |
| `tools/check-core.py` | ELF ABI, imports, exports and segment checks |
| `tools/core-imports.py` | Core import aggregation for the title |
| `src/core_loader_ps5.cpp` | Native shared-object mapping, relocation, constructors, teardown |
| `src/core_imports_ps5.cpp` | Runtime import resolution surface |
| `src/core_frame_ps5.cpp` | Existing software core-frame integration; inspect before changing format handling |
| `tooling/native/ps5-core.ld` | Native core linker layout |
| `tooling/native/core_cxx_runtime.cpp` | Core-local destructor handling |
| `tooling/native/app-symbols.map` | Application symbol export policy |
| `patches/series` | Ordered RetroArch frontend patches |
Do not blindly carry existing XRGB8888 software-output conversion patches into
PPSSPP's hardware-image path. Native Vulkan images have their own format, layout
and synchronization contract.
## 5. Platform capability matrix
Evidence resides in the sibling Eden repository under `evidence/` and is indexed
by `docs/PROBING.md`, `docs/CAPABILITY_PROBE.md` and `docs/ACTIVE.md` there.
| Capability | Observed result | Consequence / limit |
| --- | --- | --- |
| Host CPU ABI | Native x86-64 code runs | Suitable architecture for PPSSPP's AMD64 recompiler; no speed guarantee |
| Page size | 16,384 bytes | Use actual page size for allocation/protection boundaries |
| Anonymous RW then RX | Generated function executes | W^X-style JIT path exists |
| Anonymous RW then `mprotect` RWX | Generated function executes | Writable/executable mapping can be obtained through protection change |
| Direct `mmap(RWX)` | Mapping returned, execution faults | Success return does not prove executable permissions |
| JIT patch cycles | 64 protection/write/execute cycles pass | Basic rewriting works; not a PPSSPP self-modifying-code test |
| Executable region | 16 MiB, 1,024 generated functions pass | More than a one-instruction-page demonstration |
| Multithreaded generated code | Four worker threads pass | Basic concurrent execution works; not proof of PPSSPP cache synchronization |
| Signals | SIGSEGV/SIGBUS, alternate stack, `si_addr`, `siglongjmp` exercised | Fault delivery/recovery primitives work; instruction-context backpatching remains untested |
| Flexible memory | 448 MiB configured, 442 MiB available in one budget run | Ordinary anonymous/heap allocations remain constrained |
| Anonymous allocation walk | About 432 MiB before ENOMEM in earlier runs | Process footprint and allocation route matter |
| Direct-memory pool | 12,288 MiB reported | Separate pool accessible through native APIs |
| Direct-memory allocation | Up to 4 GiB mapped, written, released | Large backing allocations possible |
| Simultaneous layout | 4 GiB guest + 512 MiB code + 1 GiB table = 5,632 MiB | Large multi-region allocation and executable cache coexist in that probe |
| Executable direct memory | Map as buffer, then `sceKernelMprotect(0x07)` works | Do not pass execute to initial direct mapping and assume equivalent behavior |
| Direct mapping with execute | Refused with `0x80020016` in layout probe | Allocation/protection order is material |
| Virtual reservation | `sceKernelReserveVirtualRange`: 4 GiB succeeds; 64 GiB fails `0x8002000C` | Intermediate sizes/alignment guarantees, including PPSSPP's 8 GiB request, not established |
| Page-table accounting | Allowance of 16,384 entries reported | Respect accounting; do not equate it uncritically with one generic CPU PTE per page |
The layout result does not prove every byte was stress-tested for hours, that the
entire reported pool is always available, or that graphics/CPU contention is
acceptable. Consult the actual probe implementation before making stronger claims.
The user reports an older PS5 RetroArch+PPSSPP build with working JIT but no GPU
support. Its source/build artifacts were not located or reviewed during this
assessment. Obtain or locate them: they may already solve critical allocator,
ABI, timing, thread or emulator-memory problems. Do not assume its SDK or runtime
objects can be linked unchanged into this project.
## 6. CPU, JIT and memory integration
### 6.1 Upstream entry points to audit
At the reviewed revision:
- `Common/MemoryUtil.cpp`: `AllocateExecutableMemory`, `AllocateMemoryPages`,
`ProtectMemoryPages`, `FreeExecutableMemory`; page rounding and platform policy.
- `Common/MemArenaPosix.cpp`: backing allocation, `CreateView`, `ReleaseView`,
`Find4GBBase`; `shm_open`, `ftruncate`, `mmap`, `MAP_FIXED`, shared views.
- `Core/MemMap.cpp`: guest memory views and arena initialization.
- The selected AMD64 JIT and fault-handler code: follow actual callers of the
functions above in the chosen revision, including code-cache resets/patches.
**Confirmed mismatch:** the normal POSIX executable allocator starts with
`PROT_READ | PROT_WRITE | PROT_EXEC`, unless `PlatformIsWXExclusive()` changes that
policy. Direct RWX mapping is specifically the console path that failed to
execute. Audit the complete allocate/write/protect/execute lifetime; simply
changing one platform boolean is not sufficient without proving all callers.
**Confirmed address-space risk:** `MemArena::Find4GBBase()` on the ordinary 64-bit
POSIX path requests an **8 GiB PROT_NONE anonymous mapping** to find a 4 GiB-aligned
base. If it fails, the inspected source falls back to `0x2300000000`. Anonymous
PROT_NONE reservations consume the constrained pool in the probes. Do not permit
that hardcoded-address fallback to overwrite unknown mappings in the title.
### 6.2 Proposed PS5 memory adapter
Keep platform-specific allocation/protection code small and testable. Reuse the
older working port where compatible; otherwise adapt the appropriate upstream
memory abstraction rather than globally intercepting all `mmap` behavior.
Native APIs worth evaluating, using the installed SDK declarations and Eden
probe calls as the source of exact signatures/flags:
- `sceKernelGetDirectMemorySize`
- `sceKernelAllocateDirectMemory`
- `sceKernelMapDirectMemory`
- `sceKernelMprotect`
- `sceKernelReleaseDirectMemory`
- `sceKernelMunmap` or the correctly matched SDK unmap interface
- `sceKernelReserveVirtualRange`
- `sceKernelVirtualQuery`
- `sceKernelGetPageTableStats`
- `sceKernelConfiguredFlexibleMemorySize`
- `sceKernelAvailableFlexibleMemorySize`
- POSIX `mmap`, `mprotect`, `munmap`, `sysconf`/`getpagesize` where proven appropriate
Names above are an investigation checklist, not a ready-to-compile signature list.
Confirm import availability in this title's SDK and loader. Do not copy Eden's
numeric protection values without preserving their allocation context. The
observed buffer mapping uses `0x33`; adding CPU RWX uses `0x07` through the native
protection call. Derive production constants from the actual headers/probes.
Track each owned region's reservation base/size, mapped base/size, direct-memory
offset/size, current protection, purpose and lifetime. Check size addition and
alignment overflow. Ensure every failed stage unwinds earlier successful stages
and that load/unload cycles return accounting to a stable baseline.
Plan memory by purpose:
| Allocation class | Initial approach | Verification |
| --- | --- | --- |
| Small host objects / runtime stacks | Existing native runtime | Aggregate frontend+core flexible-memory peak |
| Guest RAM / aliases | PPSSPP arena adapter, direct backing if required | Address constraints, alias coherence, release behavior |
| JIT cache | Explicit supported protection sequence | Generated calls, patch cycles, near-call reachability, protection error paths |
| Large CPU caches | Direct memory when justified by measurements | Alignment, ownership and API compatibility |
| Vulkan resources | Existing driver allocation APIs | Do not bypass Vulkan object ownership or confuse CPU direct memory with valid GPU resources |
PSP physical RAM is much smaller than Eden's guest RAM, but PPSSPP's **host
virtual address layout** is a separate constraint. Verify aliases required by
`Core/MemMap.cpp`; several distinct allocations containing copied data are not
substitutes for views of the same backing. An 8 GiB reservation is not 8 GiB of
PSP RAM. Investigate aligned native reservation or a supported alternate arena
path; do not allocate multi-GiB physical memory merely to satisfy an address hint.
The reviewed `NO_MMAP` path uses a different allocation strategy, but enabling it
is only a candidate: validate pointer translation, JIT assumptions, mirrors,
performance and cleanup before selecting it. Do not assume it is a free fallback.
### 6.3 JIT correctness and threading
- Use 16 KiB page boundaries, with explicit checked rounding. Avoid changing the
permissions of unrelated allocations sharing a page.
- x86-64 generally has coherent instruction/data caches, but code patching still
needs correct publication and synchronization. Keep upstream synchronization;
never race a running thread against patching/freeing its instructions.
- Check relative branch/displacement range assumptions between generated code,
dispatch stubs, helper functions and mapped core text. Allocation success alone
does not validate a JIT's placement requirements.
- Verify POSIX threading, condition variables, mutexes, thread-local state,
atomics and the monotonic/high-resolution timer actually used by the core.
- The probe's signal handler resumes through `siglongjmp`; it does not establish
that PPSSPP's fault handler can inspect/update every required machine-context
field for fast-memory backpatching. Treat that as a separate integration gate.
- Do not install a broad signal handler that hides native loader/driver faults.
Define handler chaining/restoration and which address ranges the core owns.
- Exercise repeated cache invalidation, reset, content unload and a second game.
Threads must stop before executable mappings or core text are unmapped.
CPU-only or software-renderer diagnostics may isolate these problems, but report
them as intermediate results. Preserve the accepted frontend GPU path.
## 7. Native core build and loader contract
### 7.1 Reproducible build shape
Proposed deliverables are `tools/build-ppsspp.sh`, `patches/ppsspp/` and narrowly
scoped platform support under `tooling/ppsspp/` if needed. Follow existing scripts'
logic, not another PS5 project's binary pipeline or flags copied wholesale.
Use `.deps/native/ps5-payload-sdk/bin/prospero-*` wrappers explicitly for C, C++,
archive creation and linking. Existing scripts set `PS5_PAYLOAD_SDK`, `PS5_CLANG`,
`CC`, `CXX`, `AR` and `LD`, then repeat compiler selections in upstream build
arguments to prevent a hardcoded host compiler from winning.
Audit the selected PPSSPP build system for a libretro shared target and its
platform probes. Cross-compilation must not execute PS5 test binaries on Linux.
Separate any host code-generation tools from target libraries. Record versions,
source/submodule pins and generated inputs. Avoid network fetches hidden in build
steps after the dependency set has been established.
Inventory the actual enabled dependency graph: C++ runtime, compression/archive
libraries, image/audio codecs, glslang/SPIR-V components, Vulkan headers and any
thread/atomic libraries. Do not assume the desktop executable's entire dependency
set is needed, or that headers-only Vulkan dispatch removes shader compilation
requirements. PPSSPP's reviewed libretro Vulkan callback explicitly initializes
glslang. All target dependencies must be built for this SDK/ABI.
### 7.2 ELF/loader restrictions already in force
`tools/check-core.py` checks:
- ELF64, little-endian, x86-64 ET_DYN, FreeBSD/PS5 ABI identification.
- The required libretro API exports in the dynamic symbol table, including
initialization, content lifecycle, callbacks, serialization and memory APIs.
- `libkernel_web.sprx` required; allowed dependencies currently limited to it,
`libSceLibcInternal.sprx` and `libScePosixForWebKit.sprx`.
- No `libkernel_sys.sprx`, host Linux libc/libstdc++ or accidental host objects.
- 16 KiB-compatible load segments; no writable+executable ELF load segment.
The last restriction concerns the **ELF image**, not an intentionally allocated
runtime JIT cache. Do not weaken ELF validation to enable runtime JIT.
`src/core_loader_ps5.cpp` currently rejects PT_TLS and PT_INTERP; supports a
limited relocation set including `R_X86_64_NONE`, `RELATIVE`, `64`, `GLOB_DAT` and
`JUMP_SLOT`; supports init/fini arrays but rejects legacy initialization paths,
text relocations and unsupported preinitialization. General C++ unwind
registration is absent. Recheck exact implementation before changing it.
Therefore PPSSPP-specific load risks include:
- TLS emitted by PPSSPP, shader compiler libraries or the C++ runtime.
- Exceptions/unwinding, RTTI assumptions and thread-local destructors.
- Unsupported relocation forms, weak imports and symbol visibility.
- Static constructors/destructors that require unavailable imports or outlive
the core after unload.
- Duplicate runtimes or globally interposed C++/Vulkan symbols.
Existing Snes9x builds a core-local destructor registry and uses
`-fno-exceptions -fno-rtti` for that support object. This is a precedent, **not**
proof PPSSPP and all dependencies can globally disable either feature. Inspect
uses first. If loader features are added, write focused malformed-ELF and
lifetime tests; do not turn rejection into silent acceptance.
An ABI-check pass does not resolve every symbol or prove the dynamic loader
supports every section. Inspect `readelf -lW`, `-dW`, `-rW`, `--dyn-syms -W`,
undefined-symbol reports and actual load results.
### 7.3 Packaging, metadata and assets
Proposed core staging matches existing conventions:
```text
build/cores/stage/cores/ppsspp_libretro.so
build/cores/stage/info/ppsspp_libretro.info
dist/PPSA99169/cores/ppsspp_libretro.so
dist/PPSA99169/info/ppsspp_libretro.info
```
Current title staging additionally places a metadata copy beside each core for
older configurations with an empty info-directory setting. Preserve that behavior
when adding PPSSPP to `core_names`, import generation and runtime identity inputs.
Do not add the unfinished core unconditionally to the shipped build before its
build/load gate is ready.
Pin matching metadata from libretro-core-info. Validate display name, supported
extensions and archive handling against the chosen core; do not copy another
core's `.info` or treat discovery as successful loading. PPSSPP typically needs a
support asset tree under the system directory (commonly `system/PPSSPP/`);
verify exact path, capitalization, contents and licenses for the selected revision.
A PSP BIOS dump is not a substitute for that support tree.
In-title `/app0` corresponds to FTP `/data/homebrew/PPSA99169/`. Preserve content,
configuration, saves and existing assets during deployment. Start with a known
supported uncompressed content format for isolation, then test archive handling
explicitly; never dismiss an archive regression by silently changing the user's
content workflow. Keep copyrighted games and private paths out of committed logs.
## 8. Libretro Vulkan interface and ownership
### 8.1 Reviewed upstream flow
`libretro/LibretroGraphicsContext.cpp` chooses a `LibretroVulkanContext` when
requested and negotiates `RETRO_ENVIRONMENT_SET_HW_RENDER`.
`libretro/LibretroVulkanContext.cpp` at the reviewed revision:
1. Advertises `RETRO_HW_CONTEXT_VULKAN` (the constructor names Vulkan 1.0.18).
2. Provides `RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE` with
`RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN` and its interface version.
3. In `create_device`, calls `VulkanLoadFromGetInstanceProcAddr` with the frontend's
function pointer and adopts the frontend instance via `CreateInstanceExternal`.
4. Selects the physical device and calls `VulkanContext::CreateDevice` with the
frontend's required extensions/features.
5. Marks the created device externally owned: the frontend eventually destroys it.
6. Chooses a graphics queue without owning a WSI surface.
7. Obtains `RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE` during context reset.
The context code was fetched at the reference commit; associated presentation
implementation must still be read in full before porting. Do not follow an old
web result for `libretro_vulkan.cpp`: the reviewed revision uses the context and
presentation classes named here, and upstream architecture changes over time.
### 8.2 Required integration audit
The core should consume the frontend-provided dispatch path. The frontend already
links libps5vk statically. Avoid a second independently initialized driver/runtime
inside the core and avoid desktop `dlopen("libvulkan.so.1")` assumptions.
Verify:
- All Vulkan handles passed between core and frontend belong to the same driver
and compatible device/instance ownership model.
- `vkGetInstanceProcAddr` / device dispatch return every actually required entry
point; advertised optional extensions do not conceal NULL function pointers.
- The frontend implements the negotiated interface version and required features.
- Context reset/destroy, device loss and failed creation have balanced ownership.
- The core-created logical device is handed back according to the interface;
no double destroy and no use after frontend teardown.
- Presentation uses the libretro image handoff, expected layout and queue family,
frontend synchronization callbacks and semaphores/fences as defined by the
exact `libretro_vulkan.h` version. Audit `set_image`, queue locking and sync
callbacks where present; do not invent a separate swapchain in the core.
- Any negotiated device replacement preserves RetroArch's own menu/presentation
resources or correctly recreates them.
- PPSSPP workers cannot submit concurrently outside the interface's queue contract.
- Core unload drains work and destroys dependent objects before mappings or
dispatch storage disappear.
Do not assume existing software-core load/recovery tests cover any of this.
A small native hardware-core canary may be worthwhile: negotiate the interface,
render a known offscreen pattern, present it, open/close the menu, unload, reload.
That isolates frontend bridge problems from emulation and shader complexity.
## 9. Confirmed current driver blockers
The following were directly inspected in the current local source. Line numbers
are navigation aids and may move; search the named symbols/refusal strings.
### 9.1 Combined depth/stencil: startup blocker
`Common/GPU/Vulkan/VulkanContext.cpp`, `CreateDevice`, around line 655, selects the
first attachment-capable format from:
```cpp
VK_FORMAT_D24_UNORM_S8_UINT
VK_FORMAT_D32_SFLOAT_S8_UINT
VK_FORMAT_D16_UNORM_S8_UINT
```
It asserts that a usable format was found. The libretro device callback actually
calls this function, so this is not merely a standalone desktop path.
`PS5_Vulkan/driver/ps5vk_image.c` advertises depth attachment capability for
`VK_FORMAT_D32_SFLOAT`, but none of those combined formats in the inspected table.
`driver/ps5vk_draw.c` also explicitly refuses stencil attachments and accepts
only the supported D32 depth-target shape.
Required work: implement at least one compatible combined format end-to-end:
allocation/layout, views/aspects, attachment state, clear/load/store, depth and
stencil testing/writing, transfers needed by the selected renderer and truthful
feature reporting. Advertising a format or substituting depth-only D32 does not
implement stencil and must not be used to bypass startup.
### 9.2 Fixed 4K attachments: fundamental framebuffer blocker
`driver/ps5vk_draw.c` defines target width 3840 and height 2160. Colour and depth
attachment checks reject other extents; the render area must cover the full
target. Related inherited/secondary rendering checks enforce the same shape.
PPSSPP uses offscreen PSP framebuffers and intermediate targets. Native PSP output
is 480x272, but internal framebuffer stride/extent and effect targets vary; do
not special-case only 480x272. A 4K frontend swapchain does not change this need.
Required work: parameterize target layout, pitch, allocation size, surface register
encoding, viewport/scissor and render area handling. Validate multiple small,
non-power-of-two and differently sized targets. Reuse the driver's AddrLib/oracle
checks and existing pixel-readback probes. Audit primary, secondary, legacy
render-pass and dynamic-rendering paths, not only one size check.
### 9.3 Culling, stencil and dynamic state
`driver/ps5vk_pipeline.c`, `ps5vk_draw_refusal`, around lines 800-840:
- Accepts dynamic viewport/scissor and selected depth states only.
- Rejects non-NONE face culling, depth clamp/bias, rasterizer discard and non-fill
polygons in this path.
- Rejects enabled stencil or depth-bounds tests.
PPSSPP's `GPU/Vulkan/PipelineManagerVulkan.cpp` constructs PSP-dependent cull mode,
stencil operations and dynamic `STENCIL_WRITE_MASK`, `STENCIL_COMPARE_MASK` and
`STENCIL_REFERENCE`. These are genuine renderer paths, though individual games
may exercise different subsets. Implement state encoding and reset between
pipelines/draws, with tests that deliberately alternate contrasting states.
Do not infer all rejected optional features are unconditional PPSSPP requirements.
For example, inspect advertised-feature fallback before declaring depth clamp or
logic operations a universal startup blocker.
### 9.4 Blend constants and selective colour writes
The same pipeline function rejects dynamic `BLEND_CONSTANTS` and any colour-write
mask other than RGBA (`0xf`) or none (`0`). PPSSPP pipelines use a PSP-derived write
mask and enable dynamic blend constants when their blend factors require them.
The driver has separately verified constant blend-factor functionality; that is
not equivalent to accepting Vulkan's dynamic blend-constant command/state path.
Implement actual dynamic updates and per-channel masks with correct format
component ordering. Test alpha-only, RGB-only and individual-channel writes over
a known destination, including alternating draw states and menu transitions.
### 9.5 Descriptor sets beyond set zero: conditional limitation
`ps5vk_draw_refusal` rejects pipeline layouts containing descriptor sets beyond
set 0. PPSSPP's draw engine uses combined image samplers and dynamic uniform
buffers; inspect the chosen renderer's complete pipeline layouts, tessellation
and optional paths to establish whether set >0 is actually needed.
This is a **confirmed driver restriction, not a confirmed universal PPSSPP
blocker**. Do not spend time broadening it before recording an actual required
layout or selecting an unavoidable source path.
## 10. Additional graphics risks requiring investigation
These need focused inspection/tests, not unsupported declarations of failure:
| Area | Audit / probe |
| --- | --- |
| Shader generation and compiler | PPSSPP-generated vertex/fragment SPIR-V, UBO accesses, varying locations, integer operations, loops, discard, texture operations |
| Repeated compilation | Driver ACTIVE reports ACO SIGFPE/abort after particular shader sequences; reproduce in one process, including alternating shader families |
| Descriptor churn | Dynamic UBO offsets/alignment/range, sampler counts, updates, pool rollover and lifetime under in-flight work |
| Image formats | Actual texture/render-target/depth format feature combinations, component mapping, packed PSP formats and conversion paths |
| Mipmaps | Multiple levels, non-square extents, view ranges, level selection and filtering; current driver changes touch format/layout work |
| Render-to-texture | Render, transition, sample and render again; PSP framebuffer feedback/copy paths |
| Depth/stencil lifecycle | Independent clears, stencil preservation, write masks and transitions between passes |
| Transfers and readback | Row pitch, offsets, subregions, buffer/image copies, resolves/blits and CPU-visible completion |
| Synchronization | Barriers, visibility, queue submission order, semaphores/fences, upload-buffer reuse and framebuffer reuse |
| Render-pass compatibility | Attachment formats/samples, load/store, secondary command inheritance and render areas |
| Vulkan memory allocator | Memory types/heaps, requirements, alignment, mapped ranges, coherent/noncoherent handling and allocation churn |
| Cache persistence | Pipeline/shader caches, invalidation across driver builds, malformed or incompatible cache recovery |
| Threading | Parallel shader compilation and submission safety; transient workaround must be explicit and measured |
The driver's ACTIVE notes describe an unsigned texture shader that leaves ACO
unable to compile again in that process; running that case last in a separate
probe battery avoids the sequence in diagnostics, not in a real emulator.
PPSSPP compiles many pipelines during play. A stable one-shader demonstration is
insufficient. The precise failing sequence may or may not occur in PPSSPP; capture
real shaders before assuming equivalence.
Shader/frontend compilation and PS5 driver compilation are distinct stages:
PPSSPP generates shader source/SPIR-V using its dependencies; PS5_Vulkan lowers
that shader to GPU code through its compiler. A successful glslang result says
nothing about the latter stage's coverage.
Optional unsupported features must be reported accurately so PPSSPP can choose
its own fallbacks. Do not set feature bits to make negotiation succeed before
semantics exist. Likewise, an audit that accounts for all entry points does not
prove that every valid command argument/state combination is implemented.
## 11. Driver artifact selection and concurrent development
RetroArch uses prebuilt static archives. Rebuilding the sibling driver alone does
not update an already linked title, and a source revision alone does not identify
the executable driver inside that title.
Current `tools/build-title.sh` consumes:
```text
build/driver/ps5/libps5vk.ps5.a
.deps/native/vulkan-runtime/lib/libvk_runtime.ps5.a
build/driver/ps5/libpsbc_driver.ps5.a
.deps/native/psbc/lib/libpsbc_support.ps5.a
```
It also links Mesa utility objects through `tools/build-mesa-util.sh`. Record their
identity and the relevant headers/toolchain. The accepted recent frontend build
retained its normal XMB driver inputs; do not assume it was linked to today's
sibling working tree. Read the corresponding `driver-archives.json` evidence.
The title build has a diagnostic archive snapshot path that verifies source hash
before copying, copied hash, and source hash after copying. Use an equally explicit
stable-input policy for PPSSPP development rather than copying archives during a
concurrent link. Do not enable costly memory diagnostics just to obtain snapshots.
For a driver change: build driver archives in its repository using its documented
procedure; run its relevant gates; record commit plus dirty-diff status; snapshot
all dependent artifacts; relink RetroArch; record frontend/core/driver hashes;
then deploy and read back identity. Do not silently link older archives after a
new driver build fails. Keep the known-good package available for recovery.
## 12. Implementation milestones and acceptance gates
Each milestone is a bounded step, not permission to skip required gates or console
authorization. Record failed attempts as carefully as successful ones. Commit
only what the evidence supports; unverified runtime work stays explicitly pending.
### M0 — Baseline, source choice and branch
- Create the implementation branch, inventory both repositories and accepted
artifacts, locate the old JIT port, choose a source/submodule/asset pin.
- Produce a dependency/ABI inventory and map old platform adaptations to this SDK.
- Acceptance: reproducible inputs and written delta; no claim of a working core.
### M1 — Native shared core build and safe lifecycle
- Add the build script, metadata and required runtime imports/platform adaptations.
- Validate ELF segments, relocations, undefined imports, constructors and teardown.
- Test missing assets, rejected imports and failed initialization without corrupting
the frontend. A test configuration may defer hardware initialization explicitly.
- Acceptance: native load/init/deinit and repeated lifecycle capture. If the chosen
upstream path initializes Vulkan immediately, record that dependency rather than
artificially claiming this step can run before driver startup support exists.
### M2 — PPSSPP memory/JIT integration
- Implement the minimum arena/executable-memory adaptation and verify aliasing,
placement, patching, worker synchronization and failure cleanup.
- Prove the active CPU mode through logs and execution evidence, not the saved
option string alone. Disable JIT intentionally once as a comparison if useful.
- Acceptance: bounded CPU workload and repeated reset/load/unload with stable memory
accounting. Software-rendered diagnostics do not close the final GPU objective.
### M3 — Frontend hardware interface canary
- Exercise the actual libretro Vulkan negotiation, device/queue ownership and image
presentation lifecycle using a minimal offscreen render where needed.
- Acceptance: known pixels, zero unexpected refusals, correct menu overlay,
unload/reload and restored software-core presentation.
### M4 — Minimum driver subset for PPSSPP
- Implement variable target sizes, a combined depth/stencil format, required culling,
stencil/dynamic state and colour-write behavior in independently tested steps.
- Order implementation by the first unavoidable failure, with small driver probes.
- Acceptance: exact pixel/readback comparisons and alternating-state tests; all
claimed advertised capabilities match real behavior. Track remaining gaps.
### M5 — PPSSPP Vulkan first content
- Boot a redistributable PSP homebrew/test workload, then an owner-provided game at
native rendering resolution with conservative settings.
- Capture shaders, first failing Vulkan call/state and graphics timing summaries.
- Acceptance: actual PPSSPP Vulkan GPU work, correct visible content, audio/input,
no command-buffer-ending errors or unexpected driver refusals.
### M6 — Representative gameplay and transitions
- Add workloads exercising 2D/textures, 3D depth/culling, stencil/blend effects,
framebuffer feedback/readback and repeated shader creation.
- Test Quick Menu, Close Content, another PSP game, a different existing core and
native Quit; test cold/warm caches and asset/config failure recovery.
- Acceptance: owner confirms correct colours/effects and clean transitions, with
matching log evidence and bounded memory growth.
### M7 — Performance and release decision
- Measure CPU emulation, render preparation, shader compilation, submission and
presentation separately. Record game target rate, emulation speed, frame-time
percentiles, audio underruns and memory use.
- Do not confuse a 30 FPS PSP game with half-speed emulation or RetroArch's rolling
display refresh estimate with instantaneous throughput.
- Only then consider resolution scaling and optional features. Full-speed/4K is
not an initial acceptance promise.
- Ship only the verified configuration, with limitations and recoverable defaults.
## 13. Diagnostics, measurements and failure recovery
Keep `RetroArch.log`, driver trace and kernel logs useful. Capture these identities
once at startup: frontend hash, core hash/source revision, driver archive hashes,
SDK/build identifiers, CPU mode, renderer, negotiated interface and memory policy.
Use aggregate counters and first-failure records instead of per-draw/texture logs:
- Allocations/frees, current/peak bytes by owner and allocation route.
- Guest/JIT/CPU cache/driver/frontend budgets and allocation-failure request size.
- Pipeline/shader count, compile duration and repeated-compile failures.
- Descriptor pools/sets and transient upload memory high-water marks.
- Frames, CPU emulation time, GPU preparation time, submit/present/wait duration.
- First unsupported Vulkan format/state/command and its relevant parameters.
- Content/menu/context lifecycle transitions with a monotonic timestamp.
On first failure capture the error code, allocation owner, requested bytes,
current/peak totals, frame number, shader/pipeline identifiers, content lifecycle
state and queue/fence context. Preserve the first causal error before secondary
cleanup errors overwrite it. Diagnostics must avoid allocation-heavy error paths
or synchronous periodic flushes during navigation/gameplay.
Native core loading can reject unsupported objects safely; it cannot isolate an
arbitrary fault in a core sharing the frontend process. Do not call that process
isolation. Preserve crash logs, ask whether an unexplained title exit was manual,
and do not automatically restart over evidence.
Raw captures stay in ignored `klog/`; credentials/console addresses stay in ignored
`.env`. Commit sanitized machine-readable evidence and expectations under an
appropriate `evidence/ppsspp-*` directory. Never publish owner ROM filenames,
private paths or console identifiers inadvertently.
## 14. Console test matrix
| Test | Evidence needed | Pass condition |
| --- | --- | --- |
| Core discovery | Metadata, core hash, log | Correct core identity; no false success from `.info` alone |
| Native load | ELF report + live loader log | All imports/relocations resolve; constructors complete |
| Missing asset/content | Failure log + continued navigation | Useful error; frontend remains usable |
| JIT microtest | Generated function results, protection transitions | Correct execution/patching, explicit errors on unsupported paths |
| Guest memory | Alias/address tests, resource accounting | Correct mirrors, alignment and matched cleanup |
| Vulkan negotiation | Device/interface/dispatch log | Shared driver and balanced ownership |
| Small framebuffer | Known pattern/readback | Correct extents, pitch, clipping and colours |
| Depth/stencil | Alternating clear/test/write cases | Expected depth rejection and stencil effects |
| Blend/masks | Known destination and component comparisons | Correct constant blending and selective writes |
| Shader churn | Many sequential/repeated compiles | No ACO crash, stale shader or leaked compilation state |
| Gameplay | Owner observation + logs | Correct visuals, audio/input and identified renderer/CPU mode |
| Menu overlay | Open/close repeatedly | Correct colours, synchronization and no persistent corruption |
| Close Content | Menu then another game/core | Clean state restoration and bounded resource recovery |
| Quit | Exit log + klog | Deliberate clean shutdown, no late worker submissions |
| Long run | Timings, accounting and owner report | No progressive memory growth/stalls; scope/duration recorded |
Retain existing FCEUmm/mGBA/Snes9x/FBNeo/Genesis regressions when changing shared
upload, loader or driver code. Existing menu colour fixes and screenshot/thumbnail
fixes are distinct paths: test both hardware gameplay and saved thumbnails.
## 15. Files, commands and proposed deliverables
Existing verification entry point: `tools/verify.sh`, in the established order
`format`, `unit`, `build`, `integration`, `evidence`. Its build gate currently
builds the shipped cores/title; PPSSPP must be explicitly integrated before that
gate can prove anything about it. Keep command definitions in that script and
its existing subordinate tooling, rather than inventing a second gate runner.
Useful read-only baseline commands:
```bash
git status --short --branch
git log -5 --oneline
git -C ../PS5_Vulkan status --short --branch
git -C ../PS5_Vulkan rev-parse HEAD
bash tools/verify.sh --list
rg -n 'ps5vk_draw_refusal|stencil attachments|PS5VK_TARGET_WIDTH' ../PS5_Vulkan/driver
```
After implementation, proposed commands (the PPSSPP build script does not yet
exist at the time of this handoff):
```bash
bash tools/build-ppsspp.sh
python3 tools/check-core.py build/cores/stage/cores/ppsspp_libretro.so --report build/cores/ppsspp/abi.json
readelf -lW build/cores/stage/cores/ppsspp_libretro.so
readelf -dW build/cores/stage/cores/ppsspp_libretro.so
readelf -rW build/cores/stage/cores/ppsspp_libretro.so
bash tools/verify.sh
```
Consult `docs/DEPLOYMENT.md` and the current `tools/run-title.sh` help before an
authorized console run. Do not paste a guessed launch command from this plan.
Proposed evidence files, adaptable to the existing evidence schema:
```text
evidence/ppsspp-native/abi.json
evidence/ppsspp-native/build.json
evidence/ppsspp-native/loader.json
evidence/ppsspp-native/driver-archives.json
evidence/ppsspp-native/capture.json
evidence/ppsspp-native/expectation.json
```
Add focused host tests for new loader/memory ownership behavior and meaningful
console probes for graphics semantics. Do not count a test that only repeats a
constant from its implementation as evidence of runtime correctness.
## 16. Open questions and first-session checklist
Open questions, in priority order:
1. Where is the user's previous working PS5 PPSSPP JIT source/build, and which
memory/ABI adaptations can be reused without importing its old pipeline?
2. Which PPSSPP/libretro revision is the port target, and how does its hardware
interface differ from the assessed current upstream reference?
3. Does its dependency graph emit TLS or require unwind features the loader lacks?
4. What exact guest-memory aliasing/alignment and JIT placement does that build
require? Can the native reservation API meet it without hardcoded addresses?
5. Which PS5_Vulkan changes have landed since this snapshot? Which source state
and tested archive set will the frontend consume?
6. Which combined depth/stencil format is the best supported hardware/layout path?
7. Does the current RetroArch hardware-core interface work with this driver's
dispatch/device lifecycle, independently of PPSSPP?
8. Which renderer paths require additional descriptor sets, transfers or optional
features beyond the four confirmed graphics blockers?
9. Which redistributable workload and owner-provided games define first acceptance?
10. When will the owner authorize a launch after the first reviewed build is ready?
First session sequence:
- Read the required repository context and this plan; inspect both working trees.
- Create/reuse the implementation branch without disturbing pending changes.
- Locate the old port and record a source comparison before writing new adapters.
- Inventory the chosen build's dependencies, ELF/loader requirements and memory
assumptions. Read the actual libretro presentation implementation and headers.
- Recheck current driver blockers, then select one bounded implementation step.
- State its proof before building; run appropriate gates; record evidence and
update `docs/ACTIVE.md` plus an append-only phase-log entry when it lands.
Do not begin by advertising unsupported driver features, globally disabling error
checks, forcing a 4K PSP framebuffer, replacing the frontend renderer, or trying
random compiler flags until the binary links. Preserve the causal error and fix
the smallest demonstrated requirement.
## 17. References and limits of this assessment
### Local project references
- [Current RetroArch state](docs/ACTIVE.md)
- [Project gates and invariants](docs/PLAN.md)
- [Testing](docs/TESTING.md)
- [Deployment](docs/DEPLOYMENT.md)
- [GPU-path acceptance criteria](docs/GPU_PATH_CRITERIA.md)
- [Native core build example](tools/build-snes9x.sh)
- [Native loader](src/core_loader_ps5.cpp)
- [Core ABI checker](tools/check-core.py)
Sibling paths below assume the existing `PS5_Homebrews` layout. They are local
research references, not vendored dependencies or portable public documentation:
- `../PS5_Eden/docs/PROBING.md`
- `../PS5_Eden/docs/CAPABILITY_PROBE.md`
- `../PS5_Eden/docs/ACTIVE.md`
- `../PS5_Eden/evidence/capability-probe/`
- `../PS5_Eden/evidence/capability-probe-memory/`
- `../PS5_Eden/evidence/capability-probe-budget/`
- `../PS5_Eden/evidence/capability-probe-layout/`
- `../PS5_Vulkan/docs/VULKAN_PROBE_ACTIVE.md`
- `../PS5_Vulkan/driver/ps5vk_image.c`
- `../PS5_Vulkan/driver/ps5vk_pipeline.c`
- `../PS5_Vulkan/driver/ps5vk_draw.c`
### Public primary sources
- [PPSSPP upstream](https://github.com/hrydgard/ppsspp)
- [Reviewed libretro Vulkan context](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/libretro/LibretroVulkanContext.cpp)
- [Reviewed Vulkan device initialization](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/Common/GPU/Vulkan/VulkanContext.cpp)
- [Reviewed pipeline states](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/GPU/Vulkan/PipelineManagerVulkan.cpp)
- [Reviewed executable-memory utilities](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/Common/MemoryUtil.cpp)
- [Reviewed POSIX memory arena](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/Common/MemArenaPosix.cpp)
- [Reviewed guest memory mapping](https://github.com/hrydgard/ppsspp/blob/f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad/Core/MemMap.cpp)
- [PS5_Vulkan public repository](https://github.com/mihawk-99/PS5_Vulkan)
- [libretro core metadata](https://github.com/libretro/libretro-core-info)
No PPSSPP console compatibility, sustained performance, exact dependency list,
complete shader feature coverage or full hardware-interface lifecycle is proven
by this plan. Those are explicitly the implementation and verification work it
hands off. The strong platform probes justify beginning that work; the source
restrictions identify why a core build alone is insufficient.
-311
View File
@@ -1,311 +0,0 @@
# PPSSPP implementation plan: software core first, Vulkan renderer second
This is the implementation plan for adding PPSSPP to this title. It is the
executable counterpart to [PPSSPP_Core_Plan.md](PPSSPP_Core_Plan.md), which holds
the platform analysis, the driver findings and the acceptance gates this plan
inherits. Read that document for *why* each constraint exists; this one says what
gets built, in what order, and what proves each step.
The plan is stable. It records no progress, no dates and no results: the current
step is in `docs/ACTIVE.md`, the run history is in `docs/PHASE_LOG.md`, and the
measurements behind each decision are in `docs/FINDINGS.md`.
## 1. The shape of the work
Two milestones, one core binary:
| Milestone | What runs | Graphics | Depends on PS5_Vulkan |
| --- | --- | --- | --- |
| **Track A** (this plan's first half) | Real PSP emulation: MIPS JIT, software GPU core, audio, input, saves | PPSSPP's CPU rasterizer, 480x272 XRGB8888 handed to the frontend | **No** |
| **Track C** (second half) | The same core on the Vulkan hardware renderer | PPSSPP's `DrawEngineVulkan` against the linked libps5vk | Yes: the driver subset in section 7 |
Track A is a prefix, not a detour. It proves the loader contract, the platform
adaptation, the build pipeline, the memory policy, audio, input and content
handling, and it produces a known-good PPSSPP to diff the Vulkan renderer
against. Its only throwaway decision is the first memory policy (section 5),
which is upgraded inside Track A anyway.
Do not describe a software-rendered run as hardware rendering. The plan's own
three-achievement rule applies: core/platform bring-up, software-rendered PPSSPP,
and PPSSPP Vulkan hardware rendering are separate claims with separate evidence.
### Why this order is available at all
`LibretroGraphicsContext::CreateGraphicsContext()` (`libretro/LibretroGraphicsContext.cpp`)
tries GL, GLES and Vulkan in turn and ends unconditionally at
`LibretroSoftwareContext`. The core option `ppsspp_backend = "none"` maps to
`RETRO_HW_CONTEXT_NONE` (`libretro/libretro.cpp`), which matches none of those
branches, so PPSSPP never calls `RETRO_ENVIRONMENT_SET_HW_RENDER` and never
creates a Vulkan device. `GetGPUCore()` then returns `GPUCORE_SOFTWARE` and
`retro_load_game` wires that into the emulator.
The frontend needs no change: the software frames arrive through the same
`video_cb` XRGB8888 path that the five shipped cores already prove, with
`video_driver = "vulkan"` presenting them. The one runtime consequence is a fixed
480x272 output (`NATIVEWIDTH`/`NATIVEHEIGHT`, `SOFT_BMP_SIZE` in
`libretro/LibretroGraphicsContext.h`), which is already a multiple of the
frontend's 256-byte row padding, so no padded-width work is involved.
## 2. Decisions already made, with their evidence
- **Pin `hrydgard/ppsspp` at `f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad`.** It is
the revision `PPSSPP_Core_Plan.md` and `handoff/PPSSPP_UPSTREAM_LIBRETRO_RESEARCH.md`
were written against, so every finding in them is true of the pinned tree. It is
an assessment reference, not an approved release: M0 of the core plan still has
to confirm the final pin against the owner's older PS5 port when that source is
located.
- **`thread_local` needs no patch.** The SDK toolchain compiles with
`-femulated-tls`, so a `thread_local` variable becomes a `__emutls_v.*` object
with no `.tdata`/`.tbss` and no `PT_TLS` segment. Verified by compiling and
linking a TLS probe with `prospero-clang++` and `tooling/native/ps5-core.ld`:
zero PT_TLS, zero TLS sections, `NEEDED` exactly `libkernel_web.sprx`,
`libSceLibcInternal.sprx`, `libScePosixForWebKit.sprx`, and one undefined
import, `__emutls_get_address`.
**Consequence to protect:** the title must keep resolving `__emutls_get_address`
at link time. It comes from clang's `libclang_rt.builtins-x86_64.a`, which
`tools/build.sh` links only inside its `APP_VULKAN_ARCHIVES` branch. If a future
change drops the Vulkan archives from the link, every core with a `thread_local`
stops linking.
- **One core binary, two runtime configurations.** The backend is chosen per
content load from a core option (`retro_check_backend()` in `libretro.cpp`), so
the software and Vulkan paths are the same `.so`. There is no fork to maintain
and no separate build.
- **The frontend does not change for Track A.** No new patch in `patches/series`,
no change to `core_names` until the ABI gate passes, and `video_driver` stays
`"vulkan"`.
## 3. Files this plan adds
| Path | Purpose |
| --- | --- |
| `tools/build-ppsspp.sh` | the reproducible build: pinned fetch, zero-fuzz patches, cross configure, build, ABI check, staging, `build.json` |
| `tooling/ppsspp/ps5-toolchain.cmake` | FreeBSD/x86-64 cross toolchain on `prospero-*`, no host library search |
| `tooling/ppsspp/CMakeLists.txt` | wrapper project: adds the pinned tree, forces the libretro target and the PS5 feature set |
| `patches/ppsspp/*.patch` | named source changes, one concern per file, applied with `--fuzz=0` |
| `build/cores/ppsspp/` | the fetched tree, its build directory and `abi.json`/`build.json` (ignored) |
| `build/cores/stage/{cores,info}/ppsspp_libretro.{so,info}` | the staging convention every other core uses |
| `evidence/ppsspp-native/` | committed sanitized evidence once console runs exist |
## 4. Step ladder
Each step states its acceptance before it is built, and one step is one commit.
### A1 — the platform branch
PPSSPP has no platform for this ABI: `ppsspp_config.h` is an `#elif` chain over
Windows, Apple, Switch, Android, Linux and OpenBSD with no `#else` and no
`#error`. Compiled as-is, no `PPSSPP_PLATFORM_*` macro is defined and the tree
silently takes generic paths — `MemoryMap_Setup` logs "Hit a wrong path" and
returns false, and `Common/CPUDetect.cpp` leaves its core count uninitialised.
Add the branch on the macro the toolchain actually defines (`__PROSPERO__`), then
fix what the compiler reports. The census is small: `PPSSPP_PLATFORM(LINUX)`
appears in only nine files and `PPSSPP_PLATFORM(OPENBSD)` in one, so PS5 is a
genuinely new platform rather than a Linux alias. Where a BSD-shaped path is
right, follow the OpenBSD precedent.
- Acceptance: the pinned tree configures and compiles past every
`PPSSPP_PLATFORM`-guarded file with no undefined platform macro; the diff to
upstream is one named patch file.
### A2 — the cross build
`tools/build-ppsspp.sh` follows `tools/build-mgba.sh`: pinned revision, fresh
extraction, patches with zero fuzz, `prospero-*` compilers named explicitly, and
a local destructor registry object compiled with `-fno-exceptions -fno-rtti`
(`tooling/native/core_cxx_runtime.cpp`). PPSSPP needs git submodules, so the
fetch is a pinned `git` clone plus `git submodule update --init --recursive`
rather than a codeload tarball; the script records the commit and every submodule
SHA in `build/cores/ppsspp/build.json` so the input set is identified even though
it is not a single digest.
The wrapper project must keep host discovery out of the build: `find_package`
restricted to the SDK root, `OPENGL_LIBRARIES` preset empty so
`find_package(OpenGL REQUIRED)` is never reached, `X11_LIBRARIES` empty, optional
dependencies (FFmpeg, Discord, miniUPnPc, SDL) off, and `VULKAN` left on since the
Vulkan path is compiled regardless. Linking matches every other core:
`-nostdlib -nodefaultlibs -Wl,-z,undefs -Wl,-T,tooling/native/ps5-core.ld` with
`-lkernel_web -lSceLibcInternal -lScePosixForWebKit`.
- Acceptance: `tools/check-core.py build/cores/ppsspp/ppsspp_libretro.so` passes —
25 libretro exports, `libkernel_web.sprx` present, imports limited to the three
allowed modules, 16 KiB load segments, no writable+executable segment, and no
`PT_TLS`. `readelf -lW` and the ABI report are the evidence.
### A3 — the import surface
`tools/core-imports.py` derives the title's native binding table from the staged
cores' undefined symbols. Adding PPSSPP grows that table, and every new symbol
must be exported by a stub of a module the title already links, or the title link
fails with `no public SDK stub exports required symbol <name>`
(`tooling/native/sce_module_writer.cpp`).
Expected new imports, all previously confirmed present in `libkernel_web.so` or
`libSceLibcInternal.so`: `mprotect`, `pthread_*`, `sigaction`, `sigaltstack`,
`shm_open`, `__cxa_*`, `_Unwind_*`, `__emutls_get_address`.
- Acceptance: the title links with PPSSPP in `core_names`, the five existing cores
still pass `check-core.py`, and the added symbol list is recorded as evidence.
The staged core is **not** added to `core_names` before this point.
### A4 — first load on the console
Deploy the staged core beside the five shipped ones and load it with
`ppsspp_backend = "none"`. This is the first proof that the loader accepts a
20 MB C++ core with hundreds of thousands of dynamic symbols and that the title
stays alive when it does.
- Acceptance: the loader log shows the module loaded, initializers ran and the
core reported its identity; the frontend remains navigable; `klog` captured.
- Failure handling: the loader can refuse a malformed or unsupported core, but it
cannot isolate a fault inside core code. A crash is evidence to preserve, not a
reason to relaunch.
### A5 — a game boots
Assets first: PPSSPP needs `system/PPSSPP/` (its own asset files, the `lang`
folder and `flash0` fonts) and `ppge_atlas.zim`; the firmware-derived parts come
from the owner and are never committed. Content starts with a redistributable PSP
homebrew, then an owner-provided game.
- Acceptance: a PSP title reaches gameplay with the software GPU core, audio and
input working, and the run captured. Frame timing is recorded but not yet a
gate; the software rasterizer is expected to be slow on 3D content.
### A6 — lifecycle and the memory policy
Repeated load/unload, content change, menu open/close, and a second game. Then
the memory upgrade (section 5). The `NO_MMAP` bring-up policy caps guest RAM near
32 MiB, so a 64 MiB title is the natural forcing case for the arena work.
- Acceptance: bounded memory growth across cycles, no leak visible in the
title's allocation diagnostics, and a 64 MiB title running on the upgraded
policy.
### A7 — Track A acceptance
The plan's M2 milestone, reached with the software renderer: bounded CPU
workloads, repeated reset/load/unload with stable accounting, the active CPU mode
proven by logs and execution evidence rather than by the saved option string, and
one deliberate interpreter run as a comparison.
## 5. Memory policy, in two steps
The console's numbers are fixed: a 448 MiB flexible pool that **every** anonymous
mapping draws on, including `PROT_NONE` reservations; a 12,288 MiB direct pool
reachable only through the `sceKernel*` APIs; a 16,384-entry page-table
allowance; execute granted by `mprotect` and never at mapping time; and `mprotect`
rounding a non-aligned address instead of refusing it.
PPSSPP's normal POSIX path cannot work in that environment.
`MemArena::Find4GBBase()` needs an 8 GiB `PROT_NONE` anonymous mapping to succeed
before it returns a 4 GiB-aligned base, and on failure it returns the hardcoded
`0x2300000000`; `MemArena::CreateView` then maps each guest view with `MAP_FIXED`
at `base + virtual_address`, which silently replaces whatever occupies that
address. The 8 GiB probe cannot succeed inside a 448 MiB pool, so the dangerous
fallback is the *expected* path, not an edge case.
**Step one, bring-up: `USE_NO_MMAP`.** CMake adds `-DNO_MMAP -DMASKED_PSP_MEMORY`;
`CreateView` returns the pointer without mapping anything, `Find4GBBase` becomes a
single 160 MiB allocation, and guest pointers are `base + (addr & 0x3FFFFFFF)`.
No shm object, no `MAP_FIXED`, no multi-GiB reservation. The guest RAM ceiling
this implies is a known limitation, not a surprise: it is the reason step two
exists.
**Step two, correctness: a PS5 arena adapter.** A small platform memory file that
reserves address space with `sceKernelReserveVirtualRange`, backs guest RAM,
VRAM and the JIT cache with `sceKernelAllocateDirectMemory` +
`sceKernelMapDirectMemory`, obtains execute with `sceKernelMprotect`, tracks each
region's reservation base, mapped base, direct offset, protection and lifetime,
and unwinds every failed stage. The exact prototypes are not in the SDK headers —
they are exported by the kernel stubs but undeclared — so they come from the
probe code in the sibling Eden repository, which already exercised them on this
console.
Both steps are backend-independent: they apply unchanged when the Vulkan renderer
is switched on.
## 6. The JIT and executable memory
The MIPS recompilers allocate a 16 MiB code space (`AllocCodeSpace(1024*1024*16)`
in each backend); `GetJitCacheSize` no longer exists upstream, so that constant is
the whole code-cache budget. `AllocateExecutableMemory` asks for
`PROT_READ|PROT_WRITE|PROT_EXEC` unless `PlatformIsWXExclusive()` is true, and a
direct RWX mapping is exactly the case this console refuses to execute.
So the PS5 platform must report `PlatformIsWXExclusive() == true`, which routes
allocation through RW and adds execute later with `mprotect`, with
`Common/CodeBlock.h` bracketing writes with protect/unprotect. The platform branch
in A1 owns that. The cost is measured and known: 108 microseconds per
map/write/RX/call/munmap cycle on this console against 3.5 on the development
host, so code-cache churn is expensive and worth keeping out of steady state.
PPSSPP's fast-memory handling installs a `SIGSEGV` handler with `SA_SIGINFO` and
`SA_ONSTACK`, reads the machine context and rewrites the faulting program counter
(`Common/ExceptionHandlerSetup.cpp`, `Core/MemFault.cpp`). The SDK exports
`sigaction`, `sigaltstack`, `getcontext`, `makecontext` and `swapcontext`, so the
primitives exist; the loader must not be covering those signals, and the SDK's
machine-context layout is the thing to verify with a focused probe before fast
memory is trusted.
## 7. The upgrade path to the Vulkan renderer
Nothing in Track A is discarded except the memory policy's first step. Switching
on hardware rendering means:
1. Set `ppsspp_backend = "vulkan"` (and keep `video_driver = "vulkan"`).
2. Land the driver subset PPSSPP actually requires, in this order: attachments at
arbitrary extents; a combined depth/stencil format; cull mode and front face;
triangle strips and fans; then colour write mask, stencil state, dynamic blend
constants and meaningful pipeline barriers.
3. Prove the frontend's `retro_hw_render_interface_vulkan` bridge with a minimal
hardware-core canary before PPSSPP is the thing being debugged, because
PPSSPP's device creation asserts when the driver has no combined depth/stencil
format and the bridge has otherwise never been exercised.
4. Keep `ppsspp_backend = "none"` as the documented recovery configuration. A
driver regression then costs a core option, not a rebuild.
The driver details, including what **not** to build because PPSSPP never asks for
it — descriptor sets beyond zero, geometry and tessellation shaders, dynamic
rendering, logic operations, depth clamp, dual-source blend — are in section 9 of
`PPSSPP_Core_Plan.md` as amended by this plan's findings. The honest rule for
both sides: advertise a capability only when its semantics exist, because PPSSPP
takes the fast path whenever the feature bit is set and a silently dropped
feature is a correctness bug, not a fallback.
## 8. Evidence and gates
- `tools/verify.sh` gains nothing new. The PPSSPP build becomes part of the
`build` gate only when step A3 lands, by adding `ppsspp` to `core_names` in
`tools/build-title.sh`; before that the build script is run directly and its
ABI report is the evidence.
- Every step's evidence is an artifact: the ABI report for A2, the title link and
the import delta for A3, loader and frontend logs for A4, gameplay capture for
A5, allocation accounting for A6.
- Raw captures stay in the ignored `klog/`; sanitized machine-readable results go
to `evidence/ppsspp-native/`. Owner content, ROM filenames, console addresses
and firmware-derived assets never enter the repository.
- The five shipped cores are regression-tested whenever the import surface, the
loader or shared upload code changes. A PPSSPP step that breaks one of them is
not landed.
## 9. Open items to close before or during A1-A3
| Item | Why it matters | Where it closes |
| --- | --- | --- |
| Final upstream pin | the assessment pin is not an approved release, and the owner's older PS5 port may carry reusable adaptations | before the first public build |
| The owner's older JIT port | may already solve arena, ABI and timing problems; must be located or declared lost | A1 |
| PSP content and `system/PPSSPP` assets | acceptance impossible without them; firmware-derived files come from the owner | A5 |
| SDK machine-context layout | fast memory rewrites the faulting PC from `ucontext_t` | A4, by probe |
| `__emutls_get_address` retention | every core with a `thread_local` depends on the title's clang builtins archive | A3, by the title link |
## 10. Non-goals
- Building or vendoring PSP firmware, games or PPSSPP's firmware-derived assets.
- A second graphics backend, a private GPU path, or driver code in this
repository: the driver lives in PS5_Vulkan and is consumed as released
archives.
- Claiming hardware rendering, full-speed gameplay or general PSP compatibility
from a software-rendered run, a boot screen or a single title.
- Changing the shipped default away from the five accepted cores until PPSSPP
passes its own acceptance steps.
-412
View File
@@ -1,412 +0,0 @@
# PS5 RetroArch 🎮
**Native RetroArch for jailbroken PlayStation 5 consoles, rendering through
[PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan)'s port of Mesa's RADV
Vulkan driver.**
Maintained by [Mihawk](https://github.com/mihawk-99). Based on
[RetroArch / libretro](https://github.com/libretro/RetroArch), with a native PS5
application foundation derived from
[ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight).
The current build launches as a homebrew title, presents XMB through RetroArch's
Vulkan video driver, and runs eight native libretro cores, three of them (PSP,
GameCube/Wii and PlayStation 2) rendering on the PS5's GPU. Input, stereo audio,
configuration persistence and content browsing have been verified on a console.
This is an active development project; the tested paths below do not imply
complete core compatibility or Vulkan conformance.
**Latest release: v0.5.0-alpha.5** — see the [release notes](docs/releases/v0.5.0-alpha.5.md).
It is the first release on RADV; [v0.4.0-alpha.4](docs/releases/v0.4.0-alpha.4.md)
was the last on ps5vk, the project's first driver.
## Table of contents
- [Current status](#current-status)
- [Available cores](#available-cores)
- [Graphics and native runtime](#graphics-and-native-runtime)
- [Roadmap](#roadmap)
- [Build from source](#build-from-source)
- [Install and file locations](#install-and-file-locations)
- [Testing and troubleshooting](#testing-and-troubleshooting)
- [Documentation](#documentation)
- [Authors and acknowledgements](#authors-and-acknowledgements)
- [License and third-party terms](#license-and-third-party-terms)
## Current status
| Feature | Status |
| --- | --- |
| Native title startup and Quit | ✅ Working, including splash dismissal and clean native exit |
| Vulkan video output | ✅ Menu and software-core frames presented through RADV, Mesa's Vulkan driver, linked into the title (PS5_Vulkan's port, reporting Vulkan 1.4). ps5vk remains a build option |
| XMB | ✅ Default menu, with icons, fonts and background rendering |
| RGUI | ✅ Alternative menu |
| Native controller input | ✅ Buttons, left-stick menu navigation and button/axis binding capture |
| Native audio | ✅ `audio_ps5` stereo PCM output, audible channel test and buffering diagnostics |
| Filesystem and configuration | ✅ Directory browsing, configuration loading/saving and FTP-writable application folders |
| Core loading | ✅ Native shared-core loader, official `.info` discovery and recovery from rejected loads |
| Content loading | ✅ Tested games and archives with the cores below. Load Content opens two roots: **INTERNAL**, the title's folder, and **EXTERNAL**, the console's `/mnt`, where USB drives and extended storage mount. The title's sandbox hides `/mnt` for now, so EXTERNAL is empty |
| Colour and menu transitions | ✅ Corrected pixel uploads; Quick Menu/Close Content/next-game transitions I verified on the console |
| Hardware-rendered cores | ✅ PPSSPP, Dolphin, LRPS2, Beetle PSX HW, Mupen64Plus-Next (ParaLLEl-RDP) and Azahar render on RADV through their Vulkan renderers, with JITs where they have them, at up to 18× internal resolution; PPSSPP's MSAA works |
| Shader cache | ✅ RADV keeps compiled pipelines in `radv-shader-cache/`: a game's next start reads them back instead of compiling |
| Save states and fast-forward | ✅ Save/load states (including `--entryslot`) and fast-forward, tested with PPSSPP and mGBA |
| 120 Hz output | ✅ 120 Hz by default where the display offers it; the refresh is measured, and a display that stays at 60 Hz gets 60 Hz |
| Stability | ✅ On RADV: every core with a game through boot, the menu, Close Content and a reload, with and without Threaded Video, and a 10-minute PPSSPP soak with 25 menu toggles: no crash ([release checks](docs/PHASE_LOG.md)) |
| CPU video fallback | ✅ `video_ps5` remains registered and selectable |
| Development diagnostics | ✅ `retroarch.log`, startup/GPU trace, kernel captures and optional buffered frame timing |
The Alpha 5 checks recorded no crash and no kernel fatal signal, and full-speed
audio windows once each game had booted. These results apply to the captured
tests, not every possible workload. See
[active state](docs/ACTIVE.md) and [committed evidence](evidence/) for exact builds,
test coverage and known exceptions.
## Available cores
The title build includes these cores and their official metadata. FCEUmm,
mGBA, Snes9x, FBNeo, Genesis Plus GX, Beetle Saturn, VICE, MAME and DeSmuME
**render emulated games in software**; RetroArch uploads their frames and presents
them through Vulkan. PPSSPP, Dolphin, LRPS2, Beetle PSX HW, Mupen64Plus-Next and
Azahar **render on the GPU** through RADV, with their Vulkan renderers.
Each core's PS5 defaults are its highest graphical settings that hold full speed
in the games I tested: the most internal resolution the core offers, unless a
lower one is the most that keeps full speed (DeSmuME).
| Core | Systems covered by the core | Console verification in this port |
| --- | --- | --- |
| [FCEUmm](https://github.com/libretro/libretro-fceumm) | NES / Famicom | ✅ Gameplay, audio and input; subsequent shared menu-transition fixes verified. [Evidence](evidence/native-core-loading/) |
| [mGBA](https://github.com/libretro/mgba) | Game Boy, Game Boy Color, Game Boy Advance | ✅ GB/GBC/GBA loading, corrected colours and clean menu/next-game transitions. [Evidence](evidence/mgba-native/) |
| [Snes9x](https://github.com/libretro/snes9x) | SNES / Super Famicom | ✅ Tested gameplay, colours, audio/input and menu transitions; not every special chip or video mode. [Evidence](evidence/snes9x-native/) |
| [FinalBurn Neo](https://github.com/libretro/FBNeo) | Supported arcade boards, including Neo Geo and Sega System 16/32 | ✅ Tested arcade games using both native 32-bit and converted 16-bit output; not every board or ROM set. [Evidence](evidence/fbneo-native/) |
| [Genesis Plus GX](https://github.com/libretro/Genesis-Plus-GX) | Mega Drive / Genesis, Master System, Game Gear, SG-1000, Sega CD | ✅ Genesis gameplay and clean transitions, which I confirmed on the console. Other Sega systems and disc/BIOS paths still need separate acceptance. [Evidence](evidence/genesis-plus-gx-native/) |
| [PPSSPP](https://github.com/hrydgard/ppsspp) v1.20.4 | PlayStation Portable | ✅ God of War: Ghost of Sparta and Yu-Gi-Oh! GX Tag Force at 10× internal resolution (4800×2720), 16× anisotropy: correct picture, full speed at 120 Hz, save states, fast-forward, and closing and reopening games. MSAA renders on RADV (it needs render pass 2, which ps5vk lacked). |
| [Dolphin](https://github.com/libretro/dolphin) 2609 | GameCube, Wii | ✅ Wind Waker (an hour), Resident Evil 4 (30 minutes), Super Smash Bros. Melee, Mario Kart Wii and Rogue Leader, with the JIT and fast memory, save states and closing and reopening games. Rogue Leader's attract sequence still dips to 72–85% (see the release notes). |
| [LRPS2](https://github.com/libretro/LRPS2) (PCSX2) | PlayStation 2 | ✅ The God of War II and Final Fantasy X demos and GTA San Andreas at 6× internal resolution on the Vulkan hardware renderer, full speed, with multi-threaded VU1 and save states. Needs your own BIOS in `system/pcsx2/bios/`. |
| [Beetle PSX HW](https://github.com/libretro/beetle-psx-libretro) | PlayStation | ✅ Crash Bandicoot at 16× internal resolution on the Vulkan renderer, 32-bit colour, PGXP (no wobbling polygons), full speed, and closing and reopening the game. The disc image is read into memory at load. It runs with its built-in OpenBIOS; your own BIOS (`scph5501.bin` and the others its metadata lists) in `system/` is used when present. |
| [Mupen64Plus-Next](https://github.com/libretro/mupen64plus-libretro-nx) | Nintendo 64 | ✅ Mario Kart 64 with ParaLLEl-RDP at 8× upscaling and ParaLLEl-RSP, both JITs on, full speed after boot, and closing and reopening the game. |
| [Beetle Saturn](https://github.com/libretro/beetle-saturn-libretro) | Sega Saturn | ⚠️ Loads, then needs your own BIOS in `system/`: `mpr-17933.bin` (US/EU) or `sega_101.bin` (JP). Without it the game refuses to load and the menu stays usable. Gameplay not yet tested. |
| [VICE](https://github.com/libretro/vice-libretro) x64sc | Commodore 64 | ✅ A `.d64` disk game at full speed, and closing and reopening it. |
| [MAME](https://github.com/libretro/mame) 0.289 | Arcade | ✅ Metal Slug 3 from a 0.289 non-merged set, BIOS in the same folder: full speed, and closing and reopening it. Raster games render at their native size and are scaled on the GPU. Vector games are drawn at 4K by MAME's alternate renderer (not yet tested on the console). Sets must match 0.289. |
| [DeSmuME](https://github.com/libretro/desmume) | Nintendo DS | ✅ Pokémon Diamond at 5× (1280×960) with the JIT and eight rasterizer threads, full speed, and closing and reopening it. 6× measured 93–95%. |
| [Azahar](https://github.com/azahar-emu/azahar) | Nintendo 3DS | ✅ Mario & Luigi: Superstar Saga + Bowser's Minions at 18× internal resolution (the most Azahar offers) on Vulkan, with asynchronous shader compilation and the JIT, full speed after boot, and closing and reopening it. Decrypted games only. |
Use **FBNeo for Sega System 16/32 arcade sets**, rather than Genesis Plus GX.
FBNeo needs compatible arcade sets and receives its ZIP/7z archives intact.
Archive support and BIOS requirements vary by core.
Core binaries must be built for **this native pipeline and SDK**. A desktop `.so`
or a core from a different PS5 RetroArch distribution is not automatically
compatible. No games or BIOS files are bundled.
## Graphics and native runtime
```text
Software core → video callback → RetroArch Vulkan video driver
→ RADV, linked into the title → PS5 display
XMB / RGUI ──────────────────────┘
PPSSPP, Dolphin, LRPS2 (hardware cores) → Vulkan through RetroArch's HW context → RADV
```
[PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan), which I also maintain,
is the separate GPU-driver project used here. Since v0.5.0-alpha.5 the title
links its port of RADV: Mesa's Vulkan driver and ACO compiler, unchanged but
where the console differs, over a PS5 winsys, built from my Mesa fork
[PS5_Mesa](https://github.com/mihawk-99/PS5_Mesa). It reports Vulkan 1.4, and
its conformance is that project's milestone: the full Khronos CTS runs on the
console. Up to v0.4.0-alpha.4 the title linked ps5vk, the project's first
driver, which `PS5_VULKAN_DRIVER=ps5vk` still builds.
The driver is **linked into the title**. Updating a driver checkout does not
update the linked code: rebuild and redeploy the RetroArch title against the
intended driver artifacts. A `libvulkan.so.1` left in the title folder by an
earlier release is ps5vk's and is not used.
This repository supplies the frontend/platform integration, native audio and
input backends, core loader, build scripts and console validation. Core-side
pixel adapters preserve the renderer's buffers while matching the frontend's
upload format. The CPU video backend remains available as a fallback; the
current default is `video_driver = "vulkan"`, `menu_driver = "xmb"`.
PPSSPP's JIT runs: its code memory is mapped read-write and then made
executable, and its fast-memory fault handler reads the console's own signal
context layout. The port builds PPSSPP v1.20.4 with one patch
(`patches/ppsspp/ps5-port.patch`) and FFmpeg 3.0.2 for game videos. It starts
with the settings I test with (10× internal resolution, 16× anisotropy, auto
max-quality filtering, hardware transform, software skinning, no frameskip, no
speed hacks); an existing `PPSSPP.opt` is set aside once as
`PPSSPP.opt.before-ps5-profile`. The native loader has explicit limits, including
no TLS or general exception-unwind registration, and it waits for a core's
threads to finish before unmapping the core. See the
[runtime contract](docs/REFERENCE.md#native-in-process-core-loader).
## Roadmap
**✅ = verified for the stated scope. ❌ = pending implementation or acceptance
in this port, even if upstream RetroArch already offers the feature.**
### Frontend and platform
- ✅ Native startup and clean exit.
- ✅ GPU presentation through PS5_Vulkan; selectable CPU fallback.
- ✅ XMB, with RGUI retained.
- ✅ Native input, analog menu navigation and remapping.
- ✅ Native stereo audio and buffering diagnostics.
- ✅ Filesystem browsing, configuration persistence and content loading.
- ✅ Native core loading, metadata and failed-load recovery.
- ❌ Save RAM and save-state persistence verified across restarts and core changes.
- ❌ Core-option persistence and per-game/per-core overrides fully validated.
- ❌ BIOS/system-file coverage, disc swapping and multi-disc acceptance tests.
- ❌ RetroAchievements and netplay; networking is disabled in the current frontend build.
- ❌ User Slang shader presets and multipass effects validated on PS5_Vulkan.
- ✅ 120 Hz output where the display offers it, with a 60 Hz fallback chosen by measuring the refresh.
- ❌ No core losing speed while a shader compiles. On RADV (Alpha 5), PPSSPP's compiles on its own threads no longer cost it speed, and a game's second start reads its pipelines from the cache; Dolphin's first start of a game still compiles its ubershaders at a cost (see the release notes).
- ✅ Save states and fast-forward, including PPSSPP.
- ❌ 4K output/upscaling, VRR and HDR validated in this application.
- ❌ Low-latency features, runahead and sustained per-core performance measurements.
- ❌ Broader compatibility testing and release qualification.
### Cores
| Status | Core / milestone |
| --- | --- |
| ✅ | FCEUmm — NES |
| ✅ | mGBA — GB / GBC / GBA |
| ✅ | Snes9x — SNES |
| ✅ | FBNeo — tested arcade games |
| ✅ | Genesis Plus GX — tested Genesis gameplay |
| ❌ | Beetle PCE — PC Engine / TurboGrafx-16, SuperGrafx and CD; next proposed addition |
| ❌ | Stella — Atari 2600; candidate |
| ❌ | PicoDrive — add Sega 32X coverage; candidate |
| ✅ | MAME 0.289 — arcade; tested games only |
| ✅ | Beetle PSX HW — PlayStation, Vulkan renderer at 16× |
| ✅ | Mupen64Plus-Next — Nintendo 64, ParaLLEl-RDP at 8× |
| 🚧 | Beetle Saturn — Sega Saturn; needs a gameplay test with a BIOS |
| ✅ | VICE x64sc — Commodore 64 |
| ✅ | DeSmuME — Nintendo DS at 5× |
| ✅ | Azahar — Nintendo 3DS, Vulkan at 18× |
| ❌ | EXTERNAL storage (USB, extended storage) readable from inside the title's sandbox |
| ✅ | PPSSPP — PSP, Vulkan rendering and JIT; tested games only |
| ✅ | PPSSPP MSAA — render pass 2 and depth/stencil resolve, on RADV |
| ✅ | Dolphin — GameCube and Wii, Vulkan rendering and JIT; tested games, long play and the enhancement profiles |
| ✅ | LRPS2 — PlayStation 2, Vulkan hardware renderer at 4K; tested games only |
| 🚧 | LRPS2 — upstream PCSX2's newer renderer fixes, 8× internal resolution and texture replacement |
Future entries are development targets, not a promised release order. Hardware
rendering introduces new Vulkan requirements beyond presenting software frames;
PPSSPP is the first core that exercises them.
## Build from source
The current build uses Linux host tools and the project's cached public PS5 SDK.
Start with `bash tools/doctor.sh` for host-tool checks. You also need `curl`,
`patch`, `pkg-config`, ELF utilities such as `readelf`, and working host C/C++
compilers with sanitizer support for the tests. The target scripts currently use
`/usr/bin/clang` through the SDK wrappers.
Prepare and build [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan) following
its own instructions, normally as a sibling directory:
```text
workspace/
├── PS5_RetroArch/
├── PS5_Vulkan/ # The RADV release archive, its link recipe and dependencies
├── PS5_Mesa/ # My Mesa fork, which PS5_Vulkan builds RADV from
└── PS5_PayloadSDK/ # My payload SDK fork and its platform layer, at a pinned revision
```
The cores that needed changes for the console build from my forks of them
(PS5_LRPS2, PS5_BeetlePSX, PS5_Mupen64Plus, PS5_BeetleSaturn, PS5_VICE,
PS5_MAME, PS5_DeSmuME, PS5_Azahar with PS5_Dynarmic), each pinned by revision in
its build script. The script uses the sibling checkout when there is one, and
`github.com/mihawk-99/<fork>` otherwise.
The title build consumes PS5_Vulkan's RADV release archive
(`tools/build-radv.sh release` there, built from PS5_Mesa at the revision it
pins) and links it with that project's `tools/radv-link.sh`; it does not build
the driver for you. `PS5_VULKAN_DRIVER=ps5vk` links ps5vk's driver, Vulkan
runtime and shader-compiler archives instead, and `RADV_ARCHIVE` names another
RADV archive. `PS5_VULKAN_DIR` can select an alternative checkout for the
build; some host tests currently require the sibling layout above. Driver
dependencies and setup requirements are documented in the driver repository.
From the RetroArch repository root:
```bash
make deps
export PS5_PAYLOAD_SDK="$PWD/.deps/native/ps5-payload-sdk"
export PS5_CLANG=/usr/bin/clang
bash tools/fetch-retroarch.sh
bash tools/build-title.sh # Create the artifacts inspected by the host tests
bash tools/verify.sh
```
The five gates are **format → unit → build → integration → evidence**. The build
pins RetroArch 1.22.2, fetches core sources/metadata with checked hashes, builds the
frontend and all fifteen cores, and stages the native title in `dist/PPSA99169/`.
The initial dependency/source fetch requires network access.
For an already configured checkout:
```bash
bash tools/build-title.sh # Build/stage the frontend and all shipped cores
make genesis-plus-gx # Build and ABI-check one core only
# Other core targets: fceumm, mgba, snes9x, fbneo
bash tools/build-ppsspp.sh # The larger cores have scripts of their own:
# build-ppsspp.sh, build-dolphin.sh, build-lrps2.sh,
# build-beetle-psx.sh, build-mupen64plus.sh,
# build-beetle-saturn.sh, build-vice.sh,
# build-mame.sh, build-desmume.sh, build-azahar.sh
```
When adding or updating a core, rebuild the title too: the frontend's native
import table and build identity depend on the shipped core binaries. Source
patches live in `patches/`; fetched and generated trees stay in ignored
`vendor/`, `.deps/`, `build/` and `dist/` directories.
## Install and file locations
The verified deployment is a **homebrew title folder**, not a retail package.
Copy the complete `dist/PPSA99169/` tree to the title location used by your
configured homebrew launcher. The current validation setup uses
`/data/homebrew/PPSA99169/`. This project does not install a jailbreak or launcher.
`/app0` is the running application's mount point. Over FTP, use the corresponding
title folder instead:
| Purpose | Under `/data/homebrew/PPSA99169/` | In RetroArch |
| --- | --- | --- |
| Native cores | `cores/` | `/app0/cores/` |
| Core metadata | `info/`, with compatibility copies in `cores/` | `/app0/info/` |
| Games | `content/` | `/app0/content/` |
| BIOS/system data | `system/` | `/app0/system/` |
| PS2 BIOS (your own dump) | `system/pcsx2/bios/` | `/app0/system/pcsx2/bios/` |
| Live configuration | `config/retroarch.cfg` | `/app0/config/retroarch.cfg` |
| Save RAM | `savefiles/` | `/app0/savefiles/` |
| Save states | `savestates/` | `/app0/savestates/` |
| RADV shader cache | `radv-shader-cache/` | `/app0/radv-shader-cache/` |
RADV creates `radv-shader-cache/` itself, open to FTP like the other folders;
deleting it only makes the next start compile again. `ps5vk-shader-cache/` and
`sce_module/libvulkan.so.1`, left by releases up to v0.4.0-alpha.4, are not used
since v0.5.0-alpha.5 and can be deleted.
For FBNeo, use `system/fbneo/` for its system files. Genesis Plus GX's Sega CD BIOS
filenames belong in the configured `system/` root, as listed by its metadata.
The application creates its managed writable folders and seeds live settings
only when no live configuration exists. Ordinary scripted updates preserve user
content and saved settings. Existing settings can therefore keep RGUI selected
even though XMB is the packaged default. Back up user files before any clean
removal; `--clean` removes the entire title folder.
See [deployment](docs/DEPLOYMENT.md) for FTP setup, verified readback and test runs.
Console details belong in the ignored `.env`, based on `.env.example`.
## Testing and troubleshooting
A successful build proves neither gameplay nor correct rendering. Core acceptance
includes native loading, gameplay, colour checks, audio/input, Quick Menu →
Close Content, and loading another game. I record my own visual confirmation on
the console alongside the logs; a camera can miss refresh-synchronous flicker.
The v0.5.0-alpha.5 release passed all five host gates with 74 Python tests;
52 recorded captures replay successfully. Exact results and limitations are in
[ACTIVE](docs/ACTIVE.md), rather than implied by a core's upstream feature list.
For reports, include the core, game-file format, relevant settings, reproduction
steps and whether the application was closed manually. Preserve `retroarch.log`
and `trace.txt` before reopening: the frontend log is replaced on a new launch.
The test tools retain kernel captures in ignored `klog/`. Redact private paths,
console addresses and credentials before sharing logs.
Old `gpu-buffers-*.bin`, `gpu-stages-*.bin` and `gpu-tables-*.bin` files are temporary
rendering diagnostics from earlier investigations. They are not required runtime
assets and can be removed. Keep the normal development logs when reporting bugs.
## Documentation
| Document | Purpose |
| --- | --- |
| [Active state](docs/ACTIVE.md) | Current verified build, known errors and acceptance limits |
| [Reference](docs/REFERENCE.md) | Native loader, core contracts, source pins and platform details |
| [Testing](docs/TESTING.md) | Host gates and console acceptance procedures |
| [Deployment](docs/DEPLOYMENT.md) | Build staging, FTP locations, updates and capture workflow |
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Recorded symptoms, causes and fixes |
| [Findings](docs/FINDINGS.md) | Technical observations and evidence behind decisions |
| [Phase log](docs/PHASE_LOG.md) | Dated development and console-test history |
| [Releasing](docs/RELEASING.md) | What a release carries (licences, component manifest, source archives) and the order that keeps tag, ZIP and source in step; [what each past release was built from](docs/releases/SOURCE_CORRESPONDENCE.md) |
| [Evidence](evidence/) | Machine-readable captures and expected results |
| [Contributor/agent instructions](AGENTS.md) | Scope, verification, attribution and commit rules |
## Authors and acknowledgements
This port builds on substantial upstream and PS5 homebrew work. Credits below
identify project authors and teams; their repositories retain the full contributor
lists and original notices.
### Frontend, platform and graphics
| Project / author | Contribution |
| --- | --- |
| [Mihawk](https://github.com/mihawk-99) — [PS5_RetroArch](https://github.com/mihawk-99/PS5_RetroArch), [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan), [PS5_Mesa](https://github.com/mihawk-99/PS5_Mesa), [PS5_PayloadSDK](https://github.com/mihawk-99/PS5_PayloadSDK) | This native RetroArch port and core integration; the PS5 Vulkan drivers (the RADV port and ps5vk); the Mesa fork with the PS5 winsys; the payload SDK fork and its platform layer |
| [RetroArch / libretro contributors](https://github.com/libretro/RetroArch) | Frontend, libretro API, menus, video pipeline and shared libraries |
| [BlackBearReloaded — ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight) | Project starting point and reference for native PS5 input and audio integration |
| [BlackBearReloaded — PS5 Native App Boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate) | Underlying native title tooling, ELF/FSELF conversion and runtime-shim foundation |
| [John Törnblom and ps5-payload-dev contributors](https://github.com/ps5-payload-dev/sdk) | Public PS5 Payload SDK, toolchain and API stubs; [PacBrew](https://github.com/ps5-payload-dev/pacbrew-repo) ports infrastructure |
| [John Törnblom / ps5-payload-dev — websrv](https://github.com/ps5-payload-dev/websrv) | Reference for per-core fetch/build/stage scripts; this port uses a separate native title pipeline |
| [BlackBearReloaded — ps5-opengl](https://github.com/blackbearreloaded/ps5-opengl) | Shader-compiler and graphics foundations consumed by PS5_Vulkan; not the active RetroArch video backend |
| [Mesa contributors](https://gitlab.freedesktop.org/mesa/mesa) | RADV, the Vulkan driver the title renders through, with its ACO compiler, NIR, the Vulkan runtime and utilities |
| [Khronos Group](https://github.com/KhronosGroup/Vulkan-Headers) | Vulkan API headers and [specification](https://github.com/KhronosGroup/Vulkan-Docs) |
| [RetroArch assets contributors](https://github.com/libretro/retroarch-assets) and the [M+ Fonts project](https://mplusfonts.github.io/) | Packaged XMB assets and font; original notices retained |
| [LLVM / Clang contributors](https://github.com/llvm/llvm-project), [zlib authors Jean-loup Gailly and Mark Adler](https://github.com/madler/zlib) | Compilation and compression tooling |
### Emulator cores
| Core | Authors / maintainers credited by upstream |
| --- | --- |
| [FCEUmm](https://github.com/libretro/libretro-fceumm) | FCEU Team, CaH4e3 and contributors |
| [mGBA](https://github.com/libretro/mgba) | endrift and contributors |
| [Snes9x](https://github.com/libretro/snes9x) | Snes9x Team and contributors |
| [FinalBurn Neo](https://github.com/libretro/FBNeo) | Team FBNeo and contributors |
| [Genesis Plus GX](https://github.com/libretro/Genesis-Plus-GX) | Charles MacDonald, Eke-Eke and contributors |
| [PPSSPP](https://github.com/hrydgard/ppsspp) | Henrik Rydgård and contributors |
| [Dolphin](https://github.com/dolphin-emu/dolphin), [libretro/dolphin](https://github.com/libretro/dolphin) | Dolphin Emulator Project and contributors; libretro core maintainers |
| [PCSX2](https://github.com/PCSX2/pcsx2), [LRPS2](https://github.com/libretro/LRPS2) | PCSX2 Dev Team and contributors; libretro LRPS2 maintainers |
| [libretro core-info](https://github.com/libretro/libretro-core-info) | Metadata maintainers and contributors |
## License and third-party terms
This repository's own code is **GPL-3.0-or-later** ([LICENSE](LICENSE)). Most source
files carry a copyright and SPDX notice; the ones that do not (for example
`src/memory_ps5.cpp` and the build scripts in `tools/`) are under the same licence.
Code inherited from BlackBearReloaded's ps5-native-app-boilerplate and ProsperoLight
is Copyright (C) 2026 BlackBearReloaded, GPL-3.0-or-later. The generated
`sce_module/libc.prx` is described in [runtime/README.md](runtime/README.md).
Every built title folder carries `licenses/`: the licence texts each part requires,
and `components.json`, which ties every executable file to the source revision it
was built from ([tooling/notices/components.json](tooling/notices/components.json),
[docs/RELEASING.md](docs/RELEASING.md)). Releases up to v0.5.0-alpha.5 were
published without it; [docs/releases/SOURCE_CORRESPONDENCE.md](docs/releases/SOURCE_CORRESPONDENCE.md)
records what each was built from.
The cores keep their own licences, and they differ:
| Licence | Cores |
| --- | --- |
| GPL-2.0-or-later | FCEUmm, PPSSPP, Dolphin, Beetle PSX HW, Beetle Saturn, Mupen64Plus-Next (with MIT and LGPL parts), VICE, DeSmuME, Azahar (Dynarmic is 0BSD), MAME (as a whole; many files BSD-3-Clause) |
| GPL-3.0-or-later | LRPS2 (PCSX2) |
| GPL-2.0-only | RPCS3, in development here and in no release |
| MPL-2.0 | mGBA |
| Non-commercial licences | Snes9x, FinalBurn Neo, Genesis Plus GX: they may not be sold or used commercially, and FBNeo's forbids asking for donations for a project that uses its code |
Assets and fonts keep their licences too: the XMB theme is CC-BY-4.0 with the M+
font licence, PPSSPP's fonts are OFL-1.1, and Dolphin's `Sys` files carry theirs.
This is an independent homebrew project, not affiliated with or endorsed by Sony
Interactive Entertainment, the Khronos Group or the libretro project. PlayStation
and PS5 are Sony trademarks. Vulkan is a registered trademark of the Khronos Group
Inc.; the RADV port this title uses is not a Khronos-conformant product (see
[PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan)). RetroArch is the libretro
project's name and logo, used here to name the frontend this port is built from.
-30
View File
@@ -1,30 +0,0 @@
XMB Monochrome Theme
====================
Guidelines
----------
### Palette
* The icons have only one color, pure white (#ffffff).
* No semi transparent parts except for anti-aliasing purpose.
### Layout
* The shape should be easily recognizable.
* The shapes should be, when possible, aligned to a 64x64 grid, but exported as 256x256 PNGs, to downscale nicely.
* The shapes should be included in a circle of radius 31px (except when the shape is itself a circle, then we make it smaller), to insure consistent sizes.
* Some special icons have a superelliptic shape with a substracted symbol. They have an associated content icon figuring the same symbol. Symbols should match.
### Style
* The border radius are either 1px, or 2px.
* Lines are 2px.
* The level of detail is reduced, we try to represent every button, but we can void representing stickers or color changes.
### Export
The PNGs are better exported by the old Inkscape v0.48.5. Later versions causes a premultiplied alpha issue with our mipmaping and blending code.
### Theme Font
* This theme uses the [M+ P Type-1 Regular](http://mplus-fonts.osdn.jp/design.html#mplus_p1) typeface by the [M+ Fonts Project](http://mplus-fonts.osdn.jp/) covered under the [Free License](http://mplus-fonts.osdn.jp/about-en.html#license).
-131
View File
@@ -1,131 +0,0 @@
# Active work
_Updated: 2026-09-29_
## Now: v0.5.0-alpha.5 released, the first on RADV; Dolphin's first start next
**Released.** Build `f96bdb0e` (release notes `docs/releases/v0.5.0-alpha.5.md`):
the title links ../PS5_Vulkan's RADV release archive by default (PS5_Mesa
`cedb774`, built by ../PS5_Vulkan `5d8f37d`), SDK fork `95c08f2`, LRPS2 `6d14775` with
its port patch, the same tree as the fork's `main` at `9c2eea4` (6x). `PS5_VULKAN_DRIVER=ps5vk` still builds ps5vk,
which v0.4.0-alpha.4 (build `870bd1bb`) shipped. On RADV the release battery
(every core with a game, the menu, Close Content and a reload, Threaded Video on
half) passed three times as the driver changed, and PPSSPP's ten-minute soak once
(docs/PHASE_LOG.md, 2026-09-28). PPSSPP's MSAA works, RADV's shader cache is in
`radv-shader-cache/`, and the title heap has an arena for each thread. The
console's installed title is still v0.4.0-alpha.4's build.
**Dolphin round (after v0.4.0-alpha.4, on ps5vk):** the PS5 defaults are 6x, Async
(UberShaders), 16x AF and GPU texture decoding (patches/dolphin, e905c4e): RE4,
Melee, Wind Waker and Mario Kart Wii at 100% after boot. ../PS5_Vulkan R94
compiles pipelines on six threads at once; R95 writes tiled uploads a run at a
time. Rogue Leader still dips (attract 71-87%, gameplay 93-96%): Dolphin's MMU
emulation and its threads waiting on each other, not the driver.
**Seven more systems (2026-09-28, docs/PHASE_LOG.md):** Beetle PSX HW (16x),
Mupen64Plus-Next (ParaLLEl-RDP 8x), Beetle Saturn (needs a BIOS), VICE x64sc,
MAME 0.289 (vector screens at 4K, raster native), DeSmuME (5x) and Azahar (18x),
each from my fork at a pinned revision. They all pass the battery, and every
tested game runs at full speed. Load Content lists INTERNAL and EXTERNAL
(patch 0097). The installed title is build `4be9e814`.
Still open for them: EXTERNAL (`/mnt`) is empty inside the title's sandbox
(USB and extended storage need the title jailbroken; etaHEN's jailbreak call is
one way), Saturn gameplay with a BIOS and a vector game at 4K in MAME. The
forks are published on github.com/mihawk-99 at the pinned revisions, so a
checkout without the sibling forks builds from there.
**Now: RPCS3 as a libretro core** (docs/RPCS3_PORT.md). The game under test
is GTA IV (BLES00229 1.00, kept as `dev_hdd0/game/GTA4`) since 2026-09-29, and
the goal is 4K at a stable, stutter-free 60 fps, measured from a save state in
its first playable scene. It boots (a FIFO in local memory, the ZCULL late
fault, its folder's name), installs 3.3 GB to `dev_hdd0` on its first start,
saves and loads states, and with the fork's frame-rate patch for 1.00 runs at
50-51 fps at 300% (54 at 100%) where it was held to 30; the guest RSX is 98%
busy and the SPU threads meet on `vm::writer_lock`, which is next. Every game
in RPCS3's patch database now starts with its frame-rate patch ("Frame-rate
patches", on by default), and RPCS3's settings are core options by section. A
title's writes are held to about 2 MiB/s once a 1.3 GiB burst is spent; RPCS3's
files in `/app0` then go through the console's FTP server, another process
(about 6 MiB/s for GTA IV's install, 8x faster, byte for byte the same). Earlier
(2026-09-28): God of War HD (NPUA80490) reached gameplay at 100% and 300% and
passed the ten-minute run and the Close Content + reload battery; my acceptance
of its 300% screenshot is still open.
**Dolphin transitions (paused 2026-09-28 for RPCS3):** Rogue Leader's dips are
not shader compiles. The emulation thread spins in `sched_yield` inside a
libkernel lock for about half its slow time, and runs the MMU slow path for
the rest (docs/PHASE_LOG.md). Next: name that lock with the sampler's stack
scan, then compile-thread priority, the boot INI save, the GPU-thread cost and
the MMU fast path.
**Open:**
1. Dolphin's first start of a game on RADV, from an empty shader cache: Wind
Waker's first two 10 s windows at 83% and 92%, where ps5vk had 86% and 99%.
The time is Mesa's NIR optimisation of Dolphin's ubershaders
(../PS5_Vulkan/docs/RADV_PHASE.md).
2. Rogue Leader in Dolphin (measured on ps5vk; not yet on RADV): I chose to make Dolphin itself faster on the PS5 --
its MMU slow path, where 16 KiB host pages limit page-table fastmem, and its
GPU-thread cost -- rather than underclock the emulated CPU (75% reached
83-100% but changes the game's timing). Queued while the driver's Vulkan 1.4
plan is decided (../PS5_Vulkan/docs/VULKAN_1_4_PLAN.md).
3. The CPU's transfers on the GPU (CP DMA for linear copies, vk_meta for tiled
ones), with the golden comparisons updated for the new submission shapes.
4. The 60 Hz fallback on a display that stays at 60 Hz (the tester's trace).
5. LRPS2: the upstream PCSX2 port in ../PS5_LRPS2's working tree, 8x as the PS5
default, the lighting compared with the native picture.
**Test runs and core options.** RetroArch here uses per-core options
(`global_core_options = false` in my config), so a run's `core_options_path`
is ignored and each core reads `config/<core>/<core>.opt`. A test that needs
its own options sets `global_core_options = "true"` with its own path; my
per-core files are never edited.
## Accepted baselines
- Native byte order on driver `8b311e3`: I accepted the colours with the red/blue
compensation gone (`evidence/driver-1.0-native-byte-order/`).
- XMB: large-list allocation failures fixed (`601e575`), crash-free navigation
(`docs/XMB_LIST_SAFETY.md`, `evidence/xmb-safe-list-run/`).
- Gameplay: Genesis Plus GX colours, sound, controls and transitions accepted;
FCEUmm, mGBA, Snes9x and FBNeo evidence in `native-core-loading/`, `mgba-native/`,
`snes9x-native/`, `fbneo-native/`.
- Thumbnail and configuration-reset fixes (patches 0081/0082).
- PPSSPP (God of War: Ghost of Sparta, Yu-Gi-Oh! GX Tag Force at 10x): picture,
full speed at 120 Hz, save states, fast-forward, close and reopen.
## Named errors and remaining limits
- First frontend snapshot has five intentional missing-core recovery errors and two
archive-extraction failures; I identified the content as Sega System 16 & 32,
which belongs to FBNeo. No general archive fix is inferred.
- Console verification does not cover every supported system, BIOS/disc/CHD, NTSC and
interlace options, SRAM/state round trips, long-run A/V or performance. Zero backend
errors is not proof of uninterrupted audio across all workloads.
- Upstream compiler warnings (old zlib prototypes, Tremor's long-to-int abs conversion,
an unused blip helper's missing return) are neither suppressed nor treated as
acceptance.
- The loader still rejects TLS, legacy init/fini, nonempty preinit and unsupported
relocations/dependencies; cores share the frontend process, so a rejected load is safe
but an arbitrary core fault is not isolated.
- The withdrawn path has the console history; the native byte order does not yet. A
colour regression would show as red and blue exchanged in the menu, in core frames or
in thumbnails, and is the first thing to check on the next run.
## Preserved platform and file locations
XMB stays default and RGUI selectable. Vulkan remains the presentation route; video_ps5
stays selectable as CPU fallback. Native audio/input/binding, config saving, FTP
permissions and directory browsing remain. Deployments preserve live settings/games.
FTP base `/data/homebrew/PPSA99169/` corresponds to in-title `/app0`. Use content/,
system/, cores/, config/retroarch.cfg, savefiles/, savestates/. Genesis Plus GX Sega CD
BIOS files use system/ root; FBNeo uses system/fbneo/. See `docs/DEPLOYMENT.md`.
## Operating notes
Uploads are authorized; check console idle before deployment. A launch without args.txt needs my
intervention. Start klog first, preserve old logs, and capture allocation, frontend and
trace logs. Never interrupt another title. No new core or unrelated driver work is
assigned by this file.
-240
View File
@@ -1,240 +0,0 @@
# Deployment
How code reaches the place it runs, and how the run is observed. Two rules shape
everything here: **deployment is not verification** — shipping is safe because
the same artifact was already verified, not because the console looks fine
afterwards — and **this repository does not configure a console**. It builds a
homebrew title folder and uploads it to a console that is already set up.
## Environments
| Environment | What it is | Who may change it |
| --- | --- | --- |
| Local | the development machine: host tests, cross-build, staging | anyone |
| Console | a jailbroken PS5 on the local network, with its own loader | the agent, when the prompt names the console |
| Release | the ZIP attached to a tag, and the console a stranger installs it on | humans, deliberately |
The agent never writes outside `/data/homebrew/` on a console it was pointed at,
never installs or removes a title registration, and never touches a console whose
address is not in the ignored `.env`. Firmware, exploit and loader configuration
are the console owner's, not this project's.
## The artifact
Build once, stage once, promote the same tree. The artifact is
`dist/<TITLE_ID>/`: the signed application image `eboot.bin`, the runtime the
title carries in `sce_module/libc.prx`, and the metadata under `sce_sys/`
(`param.json`, `icon0.png`, `pic0.dds`, `pic1.dds`, `snd0.at9`) — exactly what the
console's loader reads, and nothing that only exists on this host. There is no
separate payload file: this is a title, and the console runs its `eboot.bin`.
The build is reproducible from a clean checkout at the committed revision:
`tools/fetch-retroarch.sh` names the upstream revision and checks the fetched tree
against that commit, `patches/series` is committed, and the artifact's file list,
sizes and sha256 digests are recorded in `dist/<TITLE_ID>/manifest.sha256`, which
`tools/check-manifest.sh` verifies. Anything that cannot be reproduced is not part
of the artifact (`vendor/`, `build/`, `.deps/`, `klog/`).
## Shipping a change
1. All gates green at the revision being shipped (`tools/verify.sh`).
2. `bash tools/build-title.sh` produces `dist/<TITLE_ID>/` and records its
manifest; `bash tools/check-manifest.sh` proves the folder is that manifest —
every file present and unchanged, nothing extra, `eboot.bin` a fake self, and
`param.json` naming the folder it sits in.
3. `python3 tools/deploy-title.py` uploads the tree over the console's FTP
service, each file under a temporary name and promoted only after its transfer
completes, the image and the metadata last; it then reads every file back and
compares digests.
4. `tools/console-run.sh <TITLE_ID>` starts the title through the resident control
payload and captures the console's log from a marked position.
5. The run's distilled capture becomes the step's evidence under
`evidence/<step>/`.
Selecting a console is configuration, never a committed value:
| Variable | Default | Purpose |
| --- | --- | --- |
| `PS5_HOST` | required | console address, from the environment or `.env` |
| `FTP_PORT` | `2121` | the console's FTP service |
| `KLOG_PORT` | `3232` | the console's kernel-log service |
| `PS5_CTL_PORT` | `9111` | the resident control payload's port |
| `PS5_FTP_USER`, `PS5_FTP_PASSWORD` | unset | credentials, if the console's FTP service wants them |
| `DEPLOY_DRY_RUN` | `0` | `1` builds and prints the target without networking |
`tools/deploy-title.py` refuses to run without a host and refuses a host that is
not an address or hostname. It publishes `dist/<TITLE_ID>/` and then reads every
file back and compares its sha256 with the file here, because this console's FTP
service has served a file's old bytes under its new name and listed a stale size
for a file it had already replaced: a size check passed on content that was not
there. Rollback and removal are the same command family: `python3
tools/deploy-title.py --clean` removes only this title's directory.
## Rollback
`tools/deploy-title.py --clean` removes `/data/homebrew/<TITLE_ID>/` and leaves
the previous release ZIP as the fallback: extracting the previous release over the
same path restores it in one step. A change that cannot be rolled back this way —
anything that writes outside the title folder — is not shipped without a written
reason in `docs/PHASE_LOG.md`.
Removal is deliberately named **undeploy**, not uninstall: FTP removal does not
unregister a title from the console's shell database, and this project does not
do that either.
## Configuration and secrets
- Configuration lives in `retroarch.cfg` inside the title folder, seeded from
`config/retroarch.cfg` in this repository. Keys that a step adds are recorded
in `docs/REFERENCE.md` with their default.
- The console's address and credentials live in the ignored `.env`, never in a
committed file, never echoed into a log, and never pasted into `docs/`. The
committed `.env.example` holds placeholders and the dry-run defaults.
- Nothing in this repository is signed, and no key, ticket or account credential
is ever created here.
## Observing a target run
- `tools/console-run.sh <TITLE_ID>` captures the console's kernel log for the run:
it checks what the console is already running, starts the listener, records its
position in the stream, launches, and judges only the lines after that mark. The
raw capture stays in the ignored `klog/`; the distilled record is what gets
committed.
- The resident control payload comes from the console tooling already in use in
`../PS5_Vulkan`: it answers one command per connection (`ping`, `procs`,
`launch <TITLE_ID>`, `kill <TITLE_ID>`, `restart <TITLE_ID>`) so a run is
scripted rather than clicked. Keeping it out of this repository is deliberate:
one console agent, one owner.
- A run is identified by the console's own boot session plus the git revision
being deployed. Record both next to the result: the same run id recurs across
restarts and means nothing on its own.
- A capture is only kept when the run it describes is named by the step's
acceptance line. Anything else is a scratch run and belongs in `klog/`.
## Sharing the console
The console runs one title at a time, and two sessions cannot both use it: a
launch from a second session replaces whatever the first was running. This
machine's console is shared with the PS5_Vulkan work, so **ask before every
launch and every upload** — the rule the console's owner set, and the reason
`tools/console-run.sh` prints what it is about to do before it does it, and
refuses to launch into a busy console with `0x80940010`.
Two habits follow from that:
- Prepare everything that can be prepared offline — the build, the staged tree,
the checks — and touch the console only for the part that genuinely needs it.
- Treat a run as a claim about the revision it was launched from. The launch
records the revision in `evidence/<step>/capture.json`, so a run that belongs
to a different revision is visible rather than implied.
## Buffered GPU timing with development logs
`tools/run-title.sh --gpu-profile 60 --watch 80` opts into a bounded in-memory
capture, then retrieves the profile and the current `retroarch.log` alongside
its kernel/trace capture. A normal run omits `--gpu-profile`. The runner checks
console availability before upload and again before launch, and validates the
frontend log's build identity to reject stale logs. See `docs/GPU_TIMING.md` for
measurement definitions and `tools/analyze-gpu-profile.py` for replay.
## Native audio test
`tools/run-title.sh --no-build --audio-test --watch 45` uses the ordinary build
with a one-shot console control file; no diagnostic binary is left installed.
It requires at least a 20-second watch window, announces alternating low-level
left/right tones, captures and validates `audio-test.json`, then captures the
normal frontend log after the test's port is closed and reopened by RetroArch.
The owner must separately confirm audible playback. Omit `--audio-test` for
ordinary launches; the runner clears any stale audio control file in either case.
## Where to put configurations, cores and content
`/app0` is the running title's mount path, not an FTP directory. For this title,
FTP uses `/data/homebrew/PPSA99169/` as the corresponding base directory:
| Use | FTP path | In RetroArch |
| --- | --- | --- |
| Cores | `/data/homebrew/PPSA99169/cores/` | `/app0/cores/` |
| ROMs/content | `/data/homebrew/PPSA99169/content/` | `/app0/content/` |
| BIOS/system files | `/data/homebrew/PPSA99169/system/` | `/app0/system/` |
| Live settings | `/data/homebrew/PPSA99169/config/retroarch.cfg` | `/app0/config/retroarch.cfg` |
| Save files/states | `savefiles/`, `savestates/` under the FTP base | `/app0/savefiles/`, `/app0/savestates/` |
The native frontend creates these directories. It installs the packaged
`retroarch.cfg` seed only when the live config does not exist. Ordinary updates
replace packaged files but do not delete the user's live config, cores, content
or saves. Full `--clean` deployment/removal is destructive to the entire title
folder, including these user files: back them up before explicitly requesting it.
“Load Content” starts at `/app0`, while “Load Core” starts at `/app0/cores`.
A directory can correctly be empty: this filesystem step installs no emulator
cores or ROMs. A core must be built for this PS5 port; copying a desktop `.so`
does not make it loadable. USB/data roots appear only when the title can open
them; FTP visibility alone does not prove title access to an external mount.
The seven app-managed directories use owner-authorized `0777`; `0775` still
denied actual FTP uploads on this console. Starting the current build repairs
older `0755`/`0775` directories.
`python3 tools/check-ftp-write.py` verifies upload/readback/cleanup in every managed
folder with uniquely named disposable files and records the result under `klog/`.
It does not overwrite cores, ROMs, saved settings or other existing files.
## Built-in core artifacts
The title build stages FCEUmm under `cores/fceumm_libretro.so` and its metadata
under `info/fceumm_libretro.info`, with an identical copy beside the core for
saved configurations whose core-info path is empty. The manifest and FTP
readback cover all three files.
`make fceumm` builds the core alone without uploading or running anything.
Runtime acceptance remains separate from the artifact checks; see
`docs/ACTIVE.md` for the current console result. No game or BIOS is bundled.
After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
For a no-game native loader/recovery diagnostic, use
`tools/run-title.sh --no-build --core-test --watch 30` after the host gates pass.
For manual core/game testing, launch with `--no-build --watch 180` (also
`--no-deploy` if the same build is already verified on the console). The owner
selects the game; collect `retroarch.log`, `trace.txt` and the kernel capture.
The loader's supported ELF/runtime contract is in `docs/REFERENCE.md`.
`make mgba` builds and checks the mGBA shared core without uploading it. The title
build includes `mgba_libretro.so` alongside FCEUmm and installs its `.info` in both
metadata locations. Deploy the rebuilt title with the new core: its import table
covers all shipped cores. Use `--core-test=mgba` for the optional loader diagnostic.
Place user GB/GBC/GBA games in `content/` (or another browsable user folder), select
mGBA manually, and capture the gameplay log before closing/reopening the title.
No ROM or BIOS is included in the build.
`make snes9x` builds and ABI-checks Snes9x without uploading. The title build
stages snes9x_libretro.so and official metadata alongside the existing cores;
rebuild the frontend too, since its import table includes C++ runtime bindings.
Use `tools/run-title.sh --no-build --core-test=snes9x --watch 180` for loader and
recovery diagnostics followed by manual SNES archive loading. Preserve existing
config/content; verify gameplay colours, audio/input, Quick Menu, Close Content
and a subsequent game before accepting the run.
`make fbneo` builds and ABI-checks the full native FBNeo core. Rebuild/deploy the
frontend alongside it so the new native imports are available. Official metadata
is staged in info/ and cores/; user-supplied `.info` files are references only.
Use `tools/run-title.sh --no-build --core-test=fbneo --watch 240` for diagnostics
and a manual gameplay/colour-transition run. Place matching user arcade archives
in content/ and BIOS files in system/fbneo/ under the FTP base above. Keep each
ZIP/7z archive intact: FBNeo loads it directly. Deployment preserves those user
folders and the live configuration. No BIOS, ROM or sample data is bundled.
`make genesis-plus-gx` builds and ABI-checks Genesis Plus GX. Rebuild/deploy the
frontend too: it binds the core's imports and includes the core in build identity.
Use `tools/run-title.sh --no-build --core-test=genesis_plus_gx --watch 240` after
host gates pass. Select Genesis Plus GX and a user game/archive manually; verify
colours, sound, controls, Quick Menu, Close Content and the next game/core.
Use content/ for games and the configured system/ root for Sega CD BIOS filenames
listed by the official metadata (not system/fbneo/). Existing games, BIOS and live
configuration are preserved. Other supported Sega systems need separate tests.
-2933
View File
File diff suppressed because it is too large. Load diff
-98
View File
@@ -1,98 +0,0 @@
# The GPU path: acceptance criteria
Scoped before any code is written, because the previous path taught this project
what "done" has to mean before the work starts rather than after. Every criterion
names how it is verified, and the verification is a command or an artifact - not an
opinion.
## What this replaces, and what it must not break
The title renders today through `video_ps5` (`src/video_ps5.cpp` +
`src/display.cpp`): RGUI's 320x240 RGB565 framebuffer is converted and scaled on the
CPU, one pixel at a time, into a tiled 1920x1080 framebuffer in direct memory, and
handed to the display with `sceVideoOutSubmitFlip`. The menu is visible on the
console. **That path stays working throughout.** Every criterion below is additive:
if the GPU path is not yet better, `video_ps5` is still the driver that runs, and
the menu is still on screen.
## A - the 40 refusals, cleared
The linked libps5vk refuses this frontend's commands, and a refusal ends recording
with an error at `vkEndCommandBuffer`, so the command buffers never submit and the
screen stays black while every call returns success. The maintainer identified all
three sources and the frontend-side fix for each.
| # | Criterion | Where | How it is verified |
| --- | --- | --- | --- |
| A1 | No pipeline is created with `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP` | `vendor/retroarch/gfx/drivers/vulkan.c:2620`, `:2649`, `:2765` - the three sites that select a strip topology | `grep -c TRIANGLE_STRIP` over the configured `gfx/drivers/vulkan.c` is 0 in any `input_assembly.topology` assignment, and the trace has **zero** `only triangle lists without primitive restart` lines |
| A2 | The RGUI upload does not use a storage image | `vulkan_copy_staging_to_dynamic` chooses compute when `dynamic->format != staging->format` (`:1099-1104`); the compute path writes a `VK_DESCRIPTOR_TYPE_STORAGE_IMAGE` at binding 3 (`:1150-1156`) | the trace has **zero** `descriptor type 3 has no proven table entry` lines |
| A3 | Every sampler the frontend creates uses clamp-to-edge on all three axes | `vendor/retroarch/gfx/drivers_shader/shader_vulkan.cpp:2034-2036` (hardcoded REPEAT) and `:2087-2108` (preset-driven, mapping to repeat / mirrored-repeat / clamp-to-border / mirror-clamp) | the trace has **zero** `sampler address modes` lines |
| A4 | The refusal count is zero, not merely lower | all of the above | `grep -c '^vulkan: ' /app0/trace.txt` is **0** over a full run |
A2 is settled as the RGB8888 route. RGUI itself still produces packed RGBA4444
for the Vulkan driver; the port expands that data to full-range R,G,B,A bytes and
allocates both staging and dynamic textures as `VK_FORMAT_R8G8B8A8_UNORM`.
Matching formats select `vkCmdCopyBufferToImage`. The unused storage-image compute
upload shader is not compiled, since its descriptor is unsupported. This preserves
the agreed 32-bit texture route without claiming RGUI produces native 8-bit colour.
## B - the menu reaches the screen through the GPU
| # | Criterion | How it is verified |
| --- | --- | --- |
| B1 | The title runs on `video_vulkan`, not `video_ps5` | `/app0/trace.txt` shows RetroArch's Vulkan driver initialising, and `ps5_init entered` (this project's own driver) is **absent**, because only one video driver runs |
| B2 | The frame is presented through libps5vk's swapchain | `vkQueuePresentKHR` returns `VK_SUCCESS` and the driver's flip marker is reached; a presentation count appears in the trace |
| B3 | No command buffer ends in error | the trace carries no refusal line (A4) **and** no `vkEndCommandBuffer` failure is reported; this is the specific fault that produced a black screen with successful presents before |
| B4 | The console owner confirms RGUI is visible, rendered through the GPU | the owner's confirmation, which is the only witness for "on the screen" this project accepts |
## C - it is actually better, measured
The reason for doing this at all. A GPU path that renders the menu no faster than
the CPU path is not worth the swap, so it has to be shown to be faster.
| # | Criterion | How it is verified |
| --- | --- | --- |
| C1 | The per-frame CPU cost of presentation is gone | the per-pixel loop in `src/video_ps5.cpp`'s `ps5_frame` does not run under Vulkan; evidenced by the absence of the driver's frame marks and by the frontend's own frame timing |
| C2 | Frame rate at the menu is at least as good as the CPU path | measured frame count over a fixed `--watch` window, compared against the CPU path's number for the same window |
| C3 | A real core runs at its target rate | load a libretro core, run it for a fixed window, and record frames presented versus frames expected. This is the number the whole exercise is for |
| C4 | The result is reversible | `video_ps5` remains registered and selectable, so a regression in the GPU path can be undone by one line in `tools/apply-port-patches.py` |
C3 needs a core in the title folder. If no core is available when the rest is done,
C3 is recorded as **not measured** rather than inferred - the previous rounds
established what happens when an unverified assumption is treated as a result.
## D - the invariants hold
| # | Criterion |
| --- | --- |
| D1 | Driver changes require explicit user authorization and are documented and committed in `../PS5_Vulkan`. The user granted that authorization on 2026-09-19 for this rendering defect. |
| D2 | `tools/verify.sh` passes: format, unit, build, integration, evidence. |
| D3 | The port's changes to upstream remain named edits in `patches/series`, applied by `tools/apply-port-patches.py` to `build/ra-conf` only; `vendor/retroarch` is never edited. |
| D4 | Every step is verified by a command or an artifact, committed with its evidence, and written up in `docs/ACTIVE.md` and `docs/FINDINGS.md`. |
## Out of scope
- Audio. The frontend already reports `Failed to initialize audio driver. Will
continue without audio.` and that is unchanged by this work.
- Shaders, filters, overlays and the XMB menu, beyond what comes free with the
Vulkan driver. A1-A4 gate on the menus this project actually renders.
- Input. The pad driver already opens and delivers presses; nothing here changes it.
- Asking the maintainer for the `V0-sampler` or `V0-topology` probes. Every
criterion above is reachable from this side. If A3 turns out to need repeat or
mirrored-repeat sampling rather than clamp-to-edge, that is a scope change and
comes back to the `docs/ACTIVE.md` open list rather than being assumed.
## Order of work
1. A1, A2, A3 as named patches, then one run. A4 is the gate: zero refusals.
2. Only then B - switch the compiled default to `vulkan`, one run, owner confirms.
3. Only then C - measure, because measuring before the path works measures nothing.
If A4 cannot be reached, the work stops there, the CPU path stays, and what was
learned is recorded. The menu on the screen is not put at risk for an optimisation.
## Current evidence
Current acceptance and artifacts live in `docs/ACTIVE.md`; dated runs are in
`docs/PHASE_LOG.md`. Successful Vulkan calls establish completion, while captured
pixels and the console owner's confirmation establish visible rendering.
-93
View File
@@ -1,93 +0,0 @@
# Buffered Vulkan frame timing and development logs
Normal launches keep logging enabled but do not run the profiler. Keep
`/app0/retroarch.log` for frontend/core/menu lifecycle, INFO/WARN/ERROR messages;
`/app0/trace.txt` for build identity, startup milestones, assertions and driver
refusals/API errors; and host `klog/` captures for kernel failures. Errors are
not buffered away. Successful button transitions and periodic frame/render
markers are routine chatter and no longer write files. Startup probes remain
bounded to their initial calls.
Patch 0058 reasserts the frontend file logger after config/argument processing:
this port's argument rebuilding previously lost the launcher's logging request.
Each frontend startup/core reload records the title's input-derived build identity
in `retroarch.log`. The run tool refuses to accept a log without the expected
identity. This is distinct from the identity check on the uploaded executable.
## Capture and reproduce
Build with `bash tools/verify.sh`, then:
```sh
bash tools/run-title.sh --no-build --gpu-profile 60 --watch 80
python3 tools/analyze-gpu-profile.py klog/gpu-profile-<run-stamp>.tsv
```
The runner requires the shared console to be idle before uploading, checks again
before launching, clears stale capture controls, and closes its title afterward.
It saves `retroarch-<stamp>.log`, `gpu-profile-<stamp>.tsv` and the kernel capture
in ignored `klog/`. Trace output stays in the run transcript. Extract only the
last `bss check=` substring from appended traces and verify its build identity.
`--gpu-profile` accepts 1..60 seconds. Allow at least 15 extra watch seconds for
startup and reporting. The runner writes `/app0/gpu-profile.txt`; title startup
consumes this one-shot opt-in. The runner also removes stale controls on later
runs, including runs without profiling. Neither control nor results are deployed
as part of the normal title. There is no image readback hook.
The profiler skips 120 completed callbacks, then collects nanosecond records in
a bounded 8,192-frame array and aggregates five-second windows **without file
I/O**. When the requested interval finishes, it stops sampling and writes one
buffered `gpu-profile.tsv` report. That reporting pause is outside the sample
and occurs only in an opted-in diagnostic run. A killed/failed run may have no
report; a full buffer is explicitly flagged and rejected by the analyzer rather
than being presented as a complete measurement.
## Meaning of the measurements
`src/vulkan_trace.cpp` uses `CLOCK_MONOTONIC` around real Vulkan function pointers
and around `vulkan_frame`, from its initial diagnostic marker through the return
from the context's swap callback. Arguments and results remain unchanged.
Measurements assume the title's single-threaded video loop and ordinary menu
path, not threaded video or BFI/multiple presentations per callback.
Each `gpu timing:` window in the result gives completed frames, measured seconds,
FPS, and average/maximum milliseconds per frame. TSV records preserve every
frame's phase times and presentation-completion interval in nanoseconds.
| Field | Measurement |
| --- | --- |
| `interval` | Previous completed callback through this completed callback. |
| `outside` | Time between callbacks excluding the menu texture-update callback: menu/input/pacing work and the previous callback's tail. |
| `texture` | Menu `vulkan_set_texture_frame` between video callbacks, including resource creation/reuse, conversion and memory operations. |
| `prepare` | Video callback wall time minus the four explicitly timed API categories below; includes command recording, uploads and other calls. |
| `end` | Real `vkEndCommandBuffer` calls inside the callback. |
| `submit` | Real `vkQueueSubmit` calls, including synchronous driver work/waits. |
| `present` | Real `vkQueuePresentKHR` calls, including display waits. |
| `wait` | Real `vkWaitForFences` and `vkAcquireNextImageKHR` calls. |
| `present_interval` | Time between successful synchronous `vkQueuePresentKHR` returns. |
`outside + texture + prepare + end + submit + present + wait` equals `interval`
for each recorded frame. Maxima can belong to different frames and cannot be
added. An incomplete callback invalidates the next interval instead of charging
an unmeasured gap to it.
These are CPU-observed elapsed times, not CPU utilization or GPU timestamps.
This driver's present call returns after its flip marker reaches VideoOut, so
return intervals provide useful presentation-completion cadence. They are not
hardware scanout timestamps or exact missed-vblank counts. The analyzer reports
intervals above 20/25 ms and rounded estimates of extra 60 Hz intervals, explicitly
labelled as estimates. Small scheduling jitter around 16.67 ms is not proof of a
missed refresh. A cadence near 16.683 ms corresponds to about 59.94 Hz.
The menu's "estimated screen refresh rate" averages observed frame intervals
(`video_monitor_fps_statistics`); startup delays can initially depress it. It
is separate from the driver's declared 60 Hz display mode. This measurement
cannot guarantee other cores/workloads, nor compare 4K Vulkan with the 1080p CPU
fallback as if they rendered the same number of pixels.
Fake-clock tests cover phase partitions, presentation spacing, warmup, delayed
output, capacity limits, stop behavior and dispatch/result preservation. Analyzer
tests reject truncated/error captures and inconsistent timing partitions. Run
`bash tools/verify.sh` for the complete gates. Current results and remaining
limitations belong in `docs/ACTIVE.md` and the committed evidence.
-126
View File
@@ -1,126 +0,0 @@
# LRPS2 in the PS5 title: requirements, gaps and plan
LRPS2 is libretro's PCSX2 core. The goal is a production-quality PS2 core in
this title (PPSA99169), rendering through ../PS5_Vulkan. It must run God of War
II (demo), Ratchet & Clank (demo) and Final Fantasy X (demo, Besaid) correctly
at 4K internal resolution (6x) with maximum settings, at full speed, and stay
stable over long sessions. This page is Phase 0: what the pinned core needs,
what the console and driver give it today, and the order of the work. Results
go to docs/PHASE_LOG.md as they come. The Vulkan half of the gap list and the
driver rounds are in ../PS5_Vulkan/docs/LRPS2_GAPS.md.
## Source and workflow
- **Pinned revision:** my fork ../PS5_LRPS2 (github.com/mihawk-99/PS5_LRPS2),
main at `9c2eea4cbd76ad6a4fb2340c95a354eb9a63500f` (2026-09-26). It is
libretro's LRPS2 with an AArch64 port and a C89 x86 emitter, among other
work; the x86-64 recompilers are upstream PCSX2's.
- **Every change to the core is made in the fork**, committed on its `main`
branch and pushed. `tools/build-lrps2.sh` clones the pinned revision (from
../PS5_LRPS2 when it is beside this repository, from GitHub otherwise) and
builds it as it is: there is no patch on top. After a change in the fork,
the pin moves to the new revision. Until 2026-09-28 the port was a branch of
the fork (`ps5-port`) carried here as `patches/lrps2/ps5-port.patch` against
`6d14775`; that tree is the same as `9c2eea4`'s.
`LRPS2_DEV=1` builds the fork's working tree directly while I edit it.
- The build follows the other native cores: `tooling/lrps2/ps5-toolchain.cmake`
(FreeBSD x86-64 through the SDK's prospero-clang), libc shims where the core
calls what the console lacks, `tools/check-core.py`'s ABI check,
`tools/core-imports.py`'s import table, and the core stamp.
## What the core needs
### Rendering
- **Hardware renderer (Vulkan)**, the shipping path. It needs a Vulkan 1.1
instance and device, `VK_KHR_sampler_mirror_clamp_to_edge` (enabled
unconditionally), R16G16B16A16_UNORM as a colour target, and a sampled
D32_SFLOAT_S8_UINT. It relies on barriers inside render passes for its
feedback loops and uses a dozen optional extensions. The details and the
driver rounds are in ../PS5_Vulkan/docs/LRPS2_GAPS.md.
- **paraLLEl-GS**: a Vulkan compute renderer needing most of the 1.2 feature
set. It is used for Profile 5's compute stress only, after the rest.
- **Software renderer**: "Software (SW)" rasterises on the CPU and asks the
frontend for no hardware context (the core's `libretro_select_hw_render`).
Phase 2 boots with it, so the CPU side is proven apart from the driver. Its
rasteriser is JIT-compiled with xbyak into the code area below.
- The GS thread is the frontend's own: `retro_run` drains the GS ring
(`MTGS::MainLoop`) and every Vulkan call happens there.
- Shaders are GLSL, compiled at run time by the bundled glslang, with a
pipeline and SPIR-V cache in `system/pcsx2/cache`.
### CPU emulation and the platform
| Area | What the pinned core does |
|---|---|
| Recompilers | x86-64: EE (R5900), IOP (R3000A), microVU0/1, the VIF unpack dynarec, and the software GS's xbyak JIT. The multi-ISA sources are built for SSE4.1, AVX and AVX2 and chosen at run time by CPUID; the console's Zen 2 has all three. |
| Addressing from JIT code | The C89 emitter encodes a global either RIP-relative or as a sign-extended 32-bit absolute, and aborts on anything else (`c89emit.h`, `E_MODRM_ABS`). The code area, the core's own data and guest main memory must therefore be within 2 GiB of each other. Upstream finds such a spot near the executable on Windows only; elsewhere it takes whatever `mmap` returns. |
| Main memory | 320 MiB (`HostMemoryMap::MainSize`): EE RAM, scratchpad and ROMs; IOP memory; VU memory; a bump allocator. It is a shared-memory object (`memshm_create`, i.e. `shm_open`) so the fastmem window can map its pages. |
| Code area | 305 MiB, read-write-execute (`CodeSize`): EE 64, IOP 32, VIF0/1 8 each, mVU0 64, mVU1 64, VIF unpack 1, software GS 64. |
| Fastmem | A 4 GiB window (`memshm_area_create`) into which guest pages of main memory are mapped at their guest virtual addresses, with `mprotect` on single pages. Faults are backpatched to slow paths. |
| Code protection | EE RAM pages holding recompiled code are made read-only (`mmap_MarkCountedRamPage`), and a write fault invalidates their blocks. |
| Page size | `__pagesize` is 4 KiB on x86 and 16 KiB on Apple AArch64. The vtlb coalesces four 4 KiB guest pages into one host page when the host page is 16 KiB (`vtlb.cpp`, the `__pagesize != VTLB_PAGE_SIZE` paths). |
| GS memory | 4 MiB of GS local memory through `memfd_create`/`shm_open`, mapped several times back to back so reads wrap (`GS.cpp`). |
| Faults | libretro-common's faulthandler on SIGSEGV (and SIGBUS on Apple and AArch64); FreeBSD x86-64 reads `mc_rip`. |
| Threads | The EE and IOP on the core's CPU thread (created at load, paused and resumed around `retro_run`, save states and option changes); MTVU (VU1) on its own thread by default; the GS on the frontend thread; the CDVD reader thread; software GS worker threads. |
| Audio | SPU2 produced on the CPU thread; batched to the frontend at the end of `retro_run`. |
| Files | libretro VFS. BIOS in `system/pcsx2/bios`. Memory cards in `system/pcsx2/memcards` (shared) or per content. Cache in `system/pcsx2/cache`. Discs as ISO, CHD, CSO, ZSO, GZ, CUE and ELF, with M3U lists. `block_extract` is set, so the frontend never unpacks an archive for it. |
| Bundled dependencies | Everything is in the tree: libretro-common (CHD, zstd, LZMA, deflate, LZ4, FLAC, PNG, YAML, rthreads, fastjmp, faulthandler, memmap), glslang, the Vulkan headers and xbyak. The built-in GameDB is generated at build time. OpenGL (glad), DEV9 networking and USB are not needed. |
## The console against those needs
| Need | The console, from the Dolphin and PPSSPP ports | Plan |
|---|---|---|
| Shared memory for main memory, fastmem and GS memory | Every view of a shared-memory object is charged to the title's flexible memory (about 450 MB for the whole title), which Wind Waker's mirrors exhausted. Direct memory is a separate pool of several GiB, and one allocation maps at any number of addresses (Dolphin's MemArena). | A PS5 backend for libretro-common's `memshm_*`, `memshm_area_*` and `memreserve*` in the fork: direct memory for the objects, and `sceKernelReserveVirtualRange` for the reservations. The fastmem window and GS memory both come from it. |
| Address layout | `0x2_0000_0000`-`0x2_FFFF_FFFF` is the driver's GPU window. An unhinted `mmap` lands in it. Dolphin's arena is at `0x10_0000_0000` and PPSSPP's range at `0x6_0000_0000`. The title's core loader maps a core wherever `mmap(nullptr)` puts it (Dolphin landed at `0x2_0199_8000`). | One 2 GiB neighbourhood above the GPU window for the core image, the 305 MiB code area and the 320 MiB main memory. That needs the loader to place a core at a chosen base, which is a general loader change, and hints in the core for the two areas. The 4 GiB fastmem window goes anywhere outside the GPU window (it is reached through a register). |
| Executable memory | A mapping asked for with `PROT_EXEC` does not execute; one mapped read-write and then `mprotect`ed executable does (Dolphin and PPSSPP). | The code area is mapped that way at its hint. |
| Flexible memory | About 450 MB for the whole title, with Dolphin running leaving roughly 238 MB free. Anonymous mappings are charged in full when made. | 305 MiB of code does not fit beside RetroArch. I will measure what a game really uses, then size the areas to fit (they are upstream's generous defaults) or place code in direct memory if the console executes it. Either way the result is in the log. |
| Page size | 16 KiB. | `__pagesize` 16 KiB for the PS5, using the vtlb's coalescing path. That path so far has run only on AArch64 Apple, so the x86 recompilers' page-protection users (`mmap_MarkCountedRamPage`, the mVU dispatcher's size) need checking against it. |
| Faults | A fault on a reserved, unmapped page can arrive as SIGBUS. The console's machine context has 48 bytes before `uc_mcontext` that FreeBSD's lacks (measured by the platform probe). | Take both signals. My SDK fork's `sys/_ucontext.h` declares the 48 bytes, so the FreeBSD branch's `uc_mcontext.mc_rip` is right as written. |
| Threads, timers, audio, input, FTP-visible files, 120 Hz pacing | Solved by the title for Dolphin and PPSSPP: `core_threads_ps5` (sampler registration and stack sizes), `audio_ps5`, `input_ps5`, `permissions_ps5`, and the pacer. | Reused as they are; the CPU thread and MTVU get the sampler hooks. |
| Content | The Ratchet & Clank demo is a `.7z`, which LRPS2 does not open and the frontend will not unpack for it. The FFX demo is the European release, so PAL at 50 Hz. God of War II's demo is an ISO. | I convert the 7z once, on the console's own storage, beside the original and leaving it untouched. Full speed for FFX is 50 fps, and its pacing on the 120 Hz output gets its own check. |
## Plan
1. **Phase 1 — build:** `tools/build-lrps2.sh`, the toolchain file, libc shims,
the import table, ABI check and stamp; packaged into the title with its info
file. Loading it on the console is the first test.
2. **Phase 2 — platform, on the software renderer:** memory map and the
`memshm` backend, the loader's placement, JIT memory, the fault path,
threads, audio and input; then the BIOS; then each game to a playable scene,
with a save state there as the baseline (in a test directory; my own states
and memory cards are never touched); then memory cards and save states.
3. **Driver rounds R81-R84** (see the Vulkan page), started in parallel with
Phase 2, since they are what stands between the hardware renderer and its
first frame.
4. **Phase 3 — hardware renderer:** first frames, then correctness against
desktop LRPS2 at the same revision on the same states (GS dumps where a frame
needs bisecting), then R85-R87 as the profiles make them matter.
5. **Phase 4 — Profiles 0-12**, with any missing option exposed cleanly. The
upscale option listed 1x, 2x, 4x and 8x; the fork adds 3x, 5x and 6x.
Sharpening, anti-aliasing and TV effects are the frontend's slang shaders:
LRPS2 has none of its own.
## Status (2026-09-26)
Phase 1 is done: the core builds from the pinned revision and the committed
patch, passes the ABI check and is staged in the title. Phase 2's platform work
is done: the code area and guest memory come from my SDK fork's platform layer,
and the recompilers have upstream's 305 MiB again (docs/PHASE_LOG.md, the SDK
fork's adoption). The BIOS boots on both renderers.
Phase 3 has started. The hardware renderer draws on the driver's R81-R91 and
runs God of War II's demo, Final Fantasy X's demo and San Andreas at 6x
(2880x2160) at full speed, with MTVU on by default on the PS5
(docs/PHASE_LOG.md, "LRPS2's hardware renderer at 4K"). What is left of it:
- the comparison with desktop LRPS2 at the same revision and settings, which is
where San Andreas' doubled radiosity haze at 6x is settled;
- FFX's pacing on the 120 Hz output: a PAL game presents at 50 Hz, two and
three refreshes a frame. VideoOut offers no 50 or 100 Hz output to switch to
(../PS5_Vulkan R92: only 59.94 and 119.88 Hz), so the cadence is the pacer's
to keep even;
- the Ratchet & Clank demo, once its .7z is converted on the console's own
storage;
- memory cards and save states in a test directory (Profile 10), then Phase 4's
profiles.
-179
View File
@@ -1,179 +0,0 @@
# XMB allocation diagnostic build
The opt-in observer records allocation lifetimes without changing allocation
policy or suppressing Vulkan errors. Allocation-policy fixes apply independently
of this switch; the accepted XMB list fix is described in
`docs/XMB_LIST_SAFETY.md`. PS5_Vulkan is read only for this work.
## Build and host verification
```sh
PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh
```
The normal build (variable absent or `0`) compiles out the observer. The diagnostic
adds `PS5_MEMORY_DIAGNOSTICS` to the title and frontend definitions and
`--wrap=posix_memalign` to the existing malloc/calloc/realloc/free wrappers because
Mesa's default Vulkan host allocator uses aligned native allocations. Mode and
archive contents participate in the runtime build identity.
The diagnostic build copies the four driver archives into
`build/memory-diagnostic-inputs/` and links those copies. `archives.json` records
their SHA-256 digests. A copy that changes during the read is rejected. This does
not rebuild or write into the sibling project; it also does not imply the copied
archives are the same driver as an earlier console run. Mesa utility sources are
still read from the configured driver source tree, and the resulting object
bytes participate in the identity.
Output is `dist/PPSA99169/`. Preserve `build/llvm-pie.elf`, `build/title.map`,
`build/title_build_identity.h`, the manifest, and the archive digests before any
subsequent build. Caller addresses must be decoded against the exact matching ELF.
Host tests exercise allocation/free accounting, mapped/native realloc migration,
failed realloc retention, foreign pointers, aligned allocation semantics,
concurrency, metadata saturation, five-second sampling with an injected clock,
logging while the wrapped allocator fails, and 10,006 repeated failures producing
only five failure records with the clock controlled by the test. Vulkan mocks check that image and
queue-idle observers preserve arguments, return values and repeat registration.
These tests do not establish console stability or identify the memory consumer.
The five-second report is synchronous in the presentation path: caller aggregation
scans 131,072 tracking slots while holding the observer mutex, then writes the
summary and caller rows. It can disturb frame pacing. Use the normal default
`PS5_MEMORY_DIAGNOSTICS=0` build for responsiveness comparisons after capturing the
allocation evidence; RetroArch.log, trace logging and passive klog remain available.
The option controls the observer, not the XMB allocation fix.
## Manual console test — only when the owner is ready
Uploads are authorized after checking the console is idle. A launch requires
owner intervention; do not change live settings or stop another title automatically.
Start a passive klog listener before the owner's reproduction and preserve the
existing logs first. Deploy the diagnostic using the project's normal folder
procedure, preserving live configuration and user content. The owner launches it.
1. Leave XMB idle for 30 seconds.
2. Browse rapidly for 30 seconds, then leave it idle for 30 seconds. Repeat.
3. Repeat the RGUI to XMB transition that triggered the crash. Note the approximate
elapsed time of each phase, including whether switching required restarting.
4. On a crash, leave the title closed until logs have been downloaded.
Download `/data/homebrew/PPSA99169/memory-diagnostics.log`, `retroarch.log`,
`trace.txt`, and the live configuration, along with the kernel capture. Keep raw
files under ignored `klog/`; they may contain user paths and process information.
In the title the same new log is `/app0/memory-diagnostics.log`. It appends a
`session` line with build identity and PID each launch rather than erasing the
previous run. No runner or deployment behavior is changed by this diagnostic.
## Reading the capture
`initial`, `sample`, `failure-summary` and `final` rows contain:
- `native_bytes/count/peak`: requested live bytes, live count and peak live bytes
from observed ordinary native allocations.
- `aligned_bytes/count/peak`: the same for `posix_memalign`; this is also native
memory, not a separate heap. Native pressure includes both native and aligned.
- `mapped_bytes/count/peak`: requests routed through large-buffer mappings and the menu-object slabs.
These exclude mapping headers, page rounding, slab size-class padding and
allocator overhead; they are requested bytes, not mapped physical footprint.
- `failures`: cumulative failures; the first four `failure` rows immediately
record operation, requested bytes, alignment (or element count for calloc
overflow), error and caller return address (`pc`). Further details and failure
summaries are limited to one per five seconds, even when presentation stops.
`failure_records` and `failure_suppressed` expose that limit. The `error` field
is a native error return or observed errno; a failed allocator may leave stale
errno, so a NULL result alone does not establish a particular error code. `calloc-overflow` is an arithmetic rejection,
not measured heap exhaustion.
- `dropped`: allocation records omitted because the fixed table was full. Any
nonzero value makes live totals incomplete for the rest of that session.
- `foreign_frees/reallocs`: pointers whose allocation was not observed (including
records dropped on saturation). Never inspect private libc allocation headers.
- `image_create/destroy/failed`: successful frontend Vulkan image creations,
non-null destruction calls and failed creations. These include images other
than menu textures and exclude calls internal to the driver.
- `idle_begin/end/failed`: queue-idle calls started, returned and returned failure.
A missing completion in a failure summary helps locate the failing operation.
Every five seconds while Vulkan presents, a summary and up to eight largest live
allocation caller groups are emitted. Groups contain `pc`, requested live bytes
and count; `site_records_omitted` reports overflow of the separate 2,048-site
aggregation table. They are not full stack traces. No frame means no periodic
sample; rate-limited allocation failures still write synchronously. Clean frontend return
writes `final`; a crash need not.
### First-failure allocation owners and XMB state
The first observed allocation failure additionally emits, once per process:
- `first-failure-caller`: up to 16 largest live caller groups **per route**,
with `route=0` native, `1` mapped and `2` aligned. These separate rankings keep
large mappings from hiding the native consumers. Counts/bytes are live requests,
not cumulative allocation traffic. Groups beyond the top 16 are not printed;
`site_records_omitted` separately indicates aggregation-table overflow.
- `first-failure-xmb`: the last cached numeric context and insertion progress,
current node counters, and native requested bytes/count at failure.
- `first-failure-xmb-history`: the last eight boundary events, oldest first,
including native requested bytes/count as observed at each event. Navigation
itself only updates a fixed in-memory ring: there is no per-entry file I/O.
`tab` is the horizontal category index, `kind` the configured source's
`XMB_SYSTEM_TAB_*` enum (UINT_MAX for a custom tab). Optional compiled features
affect enum numbering; decode against the matching configured source. `current`,
`old` and `horizontal` are the latest observed selection-list, animation-list and
horizontal-list sizes. `list_size` refers to the list at the recorded operation.
`index` is the insertion offset, copied-entry count for copy phases, or selection
index for cache/populate phases. No strings, entry names or content paths are
captured. A zero phase means no XMB context has been observed yet.
Phases: 1 cache begin, 2 destination selected, 3 copy begin, 4 copy end,
5 clear begin, 6 clear end, 7 before insert, 8 populated. Clear boundaries
surround XMB node cleanup; callbacks/entry strings are freed subsequently by
the menu layer. Sizes are observations, not pointers reread from an allocation
failure callback. In particular, clear-end size can still be the old entry
count, and current/old sizes remain cached until the next context hook.
`ms` is the last stage observation's timestamp; the enclosing `failure` row
gives the actual failure time. For failure on another thread this is the last
observed frontend state, not an assertion that that thread was rebuilding XMB.
`nodes` counts successful XMB node allocations/copies minus observed node frees;
`created`, `copied` and `freed` are cumulative. `unmatched_frees` marks incomplete
node accounting. This supplements allocator ownership; it does not replace it
or classify GPU allocations. The failure path never calls frontend code or
dereferences cached frontend pointers. Existing failure rate limits still apply;
the expanded snapshot is not repeated during the failure storm.
Host coverage wraps the eight-event ring, injects insertion context, separates
same-address callers across all three routes, caps output at 48 owner rows, and
checks that 10,006 failures emit only one expanded snapshot and five ordinary
failure records (<20 KiB total in the test). It also verifies that navigation
hooks write no bytes and normal-build C macros do not evaluate their arguments.
The linked diagnostic inspection checks that the actual frontend XMB object
references all three hooks. These checks are not console acceptance.
The first console run showed why failure logging must be bounded: menu-entry
allocation retries produced 12,793 failure records and summaries (5.4 MB) before
the owner closed a frozen title. That run is not a valid performance measurement,
and rate limiting alone does not resolve the underlying allocation failures.
Logging uses stack formatting, a preopened descriptor and `write`, without stdio
or heap allocation. A fixed 131,072-entry pointer table plus state bytes uses
about 4.13 MiB of static storage. Mutexes, table operations, scans and synchronous
writes add overhead, so use this build to diagnose lifetimes, not benchmark FPS.
The allocation route and payload size remain unchanged.
Coverage is title/core/static-library references intercepted by the linker.
Allocations performed internally by system libraries, direct mappings outside the
existing large-buffer wrapper, GPU direct memory, and allocator bookkeeping are
not measured. Requested live bytes are not the console's total memory usage.
For a `pc` from this title, subtract the image load base recorded in klog (normally
`0x400000`) and one byte for a return address, then use
`llvm-addr2line -C -f -e <matching-symbols.elf> <offset>`. A fault instruction
address is decoded without the one-byte adjustment. Keep symbols locally; never
publish raw memory addresses or local paths as sanitized evidence by accident.
Compare settled idle samples from the same session. Sustained growth is a reason
to investigate retained allocations; it alone does not prove a leak. Stable
observed totals plus allocation failures leave unobserved native usage,
fragmentation, heap limits and corruption open. The crash's Mesa null dereference
while reporting an allocation failure remains unfixed by this instrumentation.
-4552
View File
File diff suppressed because it is too large. Load diff
-105
View File
@@ -1,105 +0,0 @@
# PS5 RetroArch: the plan
A native RetroArch homebrew application for jailbroken PlayStation 5 consoles.
This file is the top-level plan: the gates that decide whether a milestone is
done, the milestone map, the invariants that constrain the code, and the index
of the documents that hold the detail. It is read every session, so it stays
short and never records progress — the state is in `docs/ACTIVE.md` and the runs
are in `docs/PHASE_LOG.md`.
## The gates
A milestone is done when all four hold for the step that closes it. They are
ordered: a later gate never excuses an earlier one.
1. **It runs.** `tools/verify.sh` is green on this host, in order: the sources
are formatted and linted, the host unit tests pass, and the PS5 target builds
from a clean tree.
2. **It is correct.** The cross-built artifact is structurally what the console's
loader and RetroArch's own dynamic loader require: PS5 objects that import
`libkernel_web.sprx` and never `libkernel_sys.sprx`, libretro cores that
export `retro_api_version`, `retro_init`, `retro_run`, `retro_load_game` and
`retro_get_system_info` in the dynamic symbol table, and no host Linux object
staged by mistake.
3. **It works on the target.** The staged tree is uploaded to the console,
launched from the console's own homebrew launcher, and the run reaches the
step's definition of working — the menu drawn, the core loaded, the frame
presented — with the console's `klog` captured for the whole run.
4. **It is provable.** The console capture is distilled into a committed
artifact under `evidence/<step>/`, and `tools/verify.sh evidence` replays it
against the committed expectation on this host and exits non-zero on a
difference. A claim with no artifact is not a pass.
## Milestone map
| Milestone | What it delivers | Detail |
| --- | --- | --- |
| M0 | The contract: filled documentation, the pinned environment, a green gate runner, and the bootstrapper that fetches and cross-builds the pinned upstream RetroArch | `docs/PHASE_LOG.md` |
| M1 | The console shell: a RetroArch payload ELF that loads on the console, takes over the screen, shows its identity and exits cleanly | `docs/REFERENCE.md` |
| M2 | Video and input: RetroArch draws through our own platform context driver onto the graphics backend, and the controller reaches its input driver | `docs/REFERENCE.md` |
| M3 | Cores and storage: core scan and load from the application's own directories, content loading, saves, save states and configuration persistence | `docs/REFERENCE.md` |
| M4 | Audio, mapping and release: audio output, input mapping, menus usable at 1080p, and a release ZIP with an evidence-backed acceptance run | `docs/REFERENCE.md` |
Each milestone is a sequence of steps, one commit each, in `docs/REFERENCE.md`.
A step that turns out to be two steps is split before it is started, not after
it is half-built.
## Invariants
These constrain every change. Breaking one is a design decision, and a design
decision is written down in `docs/REFERENCE.md` and `docs/FINDINGS.md` before
the code that depends on it.
- **Upstream stays upstream.** RetroArch is fetched at a pinned version and
digest into `vendor/`, which is never committed and never hand-edited; every
change to it is a file in `patches/`, ordered in `patches/series`, and
regenerated by `tools/fetch-retroarch.sh`. A fix that cannot be expressed as a
patch belongs in `platform/`, not in `vendor/`.
- **The toolchain is named everywhere.** Every compilation of PS5 code goes
through `$PS5_PAYLOAD_SDK/bin/prospero-*` — in the environment *and* as make
variables — so a makefile that hardcodes `CC = gcc` cannot silently produce a
host Linux object that stages cleanly and loads nowhere.
- **The loader contract is checked, not assumed.** Every shipped ELF and core is
verified after the build: it imports `libkernel_web.sprx`, it does not import
`libkernel_sys.sprx`, and its exported libretro entry points are present in
the dynamic symbol table. Linking is not loading.
- **No direct hardware access.** RetroArch code talks to the console through the
public payload SDK, and to the GPU only through a released graphics backend:
the OpenGL 3.3 Core stack in `../ps5-opengl-sdk-0.2.0` until the Vulkan driver
in `../PS5_Vulkan` reaches its 1.0 rung, then that driver. No AGC register
writes, no `sceGnm*` command submission, no firmware offsets, and no private
GPU structures in this repository.
## Where the truth lives
| Question | Document |
| --- | --- |
| What is being built, and why | this file |
| What is happening right now | `docs/ACTIVE.md` |
| What a step is, and how it is accepted | `docs/REFERENCE.md` |
| What was measured, and what it forces | `docs/FINDINGS.md` |
| What happened, run by run | `docs/PHASE_LOG.md` |
| How to verify, ship and debug | `docs/TESTING.md`, `docs/DEPLOYMENT.md`, `docs/TROUBLESHOOTING.md` |
If a question has no home, add it to the table and create the document; do not
answer it in two places.
## What is deliberately not planned
- The exploit, the kernel payload and the console's configuration. This project
builds an application; it does not jailbreak a console, and it never writes to
a console it was not pointed at.
- The GPU driver. Graphics on this console are separate projects in
`../PS5_Vulkan` and `../ps5-opengl-sdk-0.2.0`; this project consumes their
released artifacts and reports what RetroArch needs from them.
- The homebrew launcher, the payload loader and the title registration flow. The
console's own loader starts this application; this repository does not install
or configure a console.
- Retail-style packaging: signing keys, `sce_sys` beyond a homebrew title's own
metadata, and store submission.
- A software-rendered fallback as the product. The headless and null drivers
exist to prove the frontend underneath the graphics work; the shipped
application renders through a graphics backend named in `docs/REFERENCE.md`.
Scope grows by changing this file, in its own commit, with the reason in the
message. It never grows by a step quietly doing more than its milestone says.
-123
View File
@@ -1,123 +0,0 @@
# The 10.01 system modules: what they tell us
I have an unpacked copy of a console's system modules on this machine,
outside every repository (a folder named for firmware 10.01). This page records
what comparing their export tables with our SDK and our shims established, and
whether the copy is worth using. It names exported functions and modules only.
Nothing from the copy is in any repository, and the reader I used lives in my
scratch space.
## What is there
332 files, all x86-64 ELF images with the FreeBSD ABI and readable dynamic
tables:
| Folder | Contents |
|---|---|
| `common/lib` | 277 modules: libkernel in three variants (`libkernel`, `libkernel_web`, `libkernel_sys`), `libSceLibcInternal`, and the Sce libraries (VideoOut, AudioOut, Pad, Agc, Sysmodule, ...) |
| `priv/lib` | 26 system-private modules |
| `sys` | system processes |
| `vsh` | system applications |
Each module's symbols are named by NID: an 11-character hash of the function
name, the same derivation our own ELF writer uses for our imports
(tooling/native/sce_module_writer.cpp). With the module's library table, an
export reads as "NID, library, module". I computed NIDs for every name in the
payload SDK's stub libraries, the SDK headers,
tooling/native/runtime/imports.txt and the functions our shims replace, and
looked each up.
**Versions.** The copy is 10.01. My console runs 12.09:
`sceKernelGetSystemSwVersion` reports `12.090.001` in our own capability probe
(../PS5_PayloadSDK, platform/evidence/probe-2026-09-25), and
`kern.sdk_version` reports
0x10010000. So everything below is a hint, and each behaviour that matters was
measured on the console.
## What it shows that the SDK does not
**The SDK's stubs are almost all real.** Every one of the 3,015 names in the
SDK's `libSceLibcInternal` stub is exported by the console's libc. Of the 1,111
names in the `libkernel_web` stub, all but `sceKernelGetPrefixVersion` and
`sceKernelIccGetThermalAlert` are exported (the same two are missing from the
other variants). The other stubs we could link are complete except for
system-only functions nobody here calls: six system-cursor functions in
VideoOut, 17 crash-report functions in SystemService, 40 settings getters in
UserService and two device-control functions in AudioOut.
`libScePosixForWebKit` and `libSceGLSlimVSH` are not in the copy, so their
stubs cannot be judged from it; the SDK routes `arc4random`,
`arc4random_buf`, `getaddrinfo`, `freeaddrinfo`, `opendir` and `readdir`
through the former, and our titles measured `arc4random` resolving to nothing.
**Why our libc gaps are gaps.** For the functions our shims replace
(src/libc_shims.c, src/ps5_directory.cpp, tooling/lrps2/ps5-libc-shims.cpp,
the cores' patches):
| Function | Exported by | So |
|---|---|---|
| `gmtime_r`, `localtime_r`, `utimensat`, `futimens`, `dirfd`, `faccessat`, `readlinkat`, `clock_nanosleep`, `if_nameindex`, `arc4random`, `arc4random_buf`, `arc4random_uniform` | no module in the copy | no stub fix is possible: shims |
| `umask`, `statfs`, `fstatfs`, `openat`, `unlinkat`, `fchmodat`, `fstatat`, `mkdirat`, `renameat`, `fchownat`, `linkat`, `symlinkat` | `libkernel_sys` only | our titles import `libkernel_web` by design (docs/PLAN.md), so these resolve to nothing; shims |
| `statvfs`, `fstatvfs` | libc | exported, but libc builds them on `statfs`, which only `libkernel_sys` carries; our titles measured `statvfs` faulting inside libc (2026-09-24): shim |
| `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir` | libc | exported; our titles measured the console refusing `opendir`, so enumeration goes through `getdents`, which `libkernel_web` exports |
| `gmtime`, `posix_memalign`, `aligned_alloc` | libc | real exports |
So the question has a clear answer: no gap is a stub routing mistake that
a stub fix would replace. Each is either absent from the system, present only
in the variant our titles do not import, or exported but faulting or refused.
The shims stay, once, in the platform layer.
**The memory interface.** Everything the platform layer needs is exported by
all three libkernel variants and named in the SDK's libkernel stubs, but not
one `sceKernel` function is declared in the SDK's headers:
- direct memory: `sceKernelAllocateDirectMemory`,
`sceKernelAllocateMainDirectMemory`, `sceKernelReleaseDirectMemory`,
`sceKernelCheckedReleaseDirectMemory`, `sceKernelMapDirectMemory`,
`sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`,
`sceKernelGetDirectMemorySize`, `sceKernelAvailableDirectMemorySize`,
`sceKernelDirectMemoryQuery`, `sceKernelGetDirectMemoryType`;
- protection: `sceKernelMprotect`, `sceKernelMtypeprotect`,
`sceKernelQueryMemoryProtection`, `sceKernelVirtualQuery`, and POSIX
`mprotect`;
- virtual ranges: `sceKernelReserveVirtualRange`,
`sceKernelSetVirtualRangeName`, `sceKernelMmap`, `sceKernelMunmap`,
`sceKernelBatchMap`, `sceKernelBatchMap2`;
- memory pools: `sceKernelMemoryPoolExpand`, `...Reserve`, `...Commit`,
`...Decommit`, `...Batch`, `...Move`, `...GetBlockStats`;
- flexible memory: `sceKernelMapFlexibleMemory`,
`sceKernelMapNamedFlexibleMemory`, `sceKernelReleaseFlexibleMemory`,
`sceKernelAvailableFlexibleMemorySize`,
`sceKernelConfiguredFlexibleMemorySize`;
- shared memory for JIT code: `sceKernelJitCreateSharedMemory`,
`sceKernelJitCreateAliasOfSharedMemory`, `sceKernelJitMapSharedMemory`,
`sceKernelJitGetSharedMemoryInfo`. In `libkernel_web` these are exported
only under the `libkernel_jvm`, `libkernel_ps2emu` and `libkernel_psmkit`
libraries. Our probe measured `sceKernelJitCreateSharedMemory` refused to
the title (0x80020001), so executable code does not go this way.
Across our repositories the kernel functions we call are all among these, and
nine files plus two core patches declared their own prototypes for them. The
machine-context offset was carried in five places: the crash reporter, the
sampler, and the Dolphin, PPSSPP and LRPS2 ports.
## Verdict
Useful, as an index, for three things: deciding which exported functions are
worth probing, explaining why an import resolves to nothing (which module and
variant exports it), and confirming that the SDK stubs are not the problem.
Not useful for behaviour: the copy is two firmware versions behind my console,
and an export says nothing about whether a title may use it --
`sceKernelJitCreateSharedMemory` is exported and refused, `opendir` is
exported and refused. Every behaviour the platform layer relies on is
established by our own probe instead (../PS5_PayloadSDK, platform/docs/PROBE.md).
## Where the platform layer lives
In my fork of the payload SDK, `../PS5_PayloadSDK`, which every project pins by
revision. The fork's `sys/_ucontext.h` declares the console's 48 extra bytes,
so `uc_mcontext.mc_rip` is the faulting instruction and no project carries an
offset. Its `platform/` builds `libps5platform.a` and installs
`include/ps5platform` with the SDK. That covers the kernel declarations, the
libc gaps above, the executable-code allocator and direct-memory shared memory,
with host tests. Its `platform/tools/setup-sdk.sh` installs the fork's revision
over the v0.42 release binaries.
-657
View File
@@ -1,657 +0,0 @@
# PS5 RetroArch: reference
Stable reference. Edit when the specification changes. This file holds the step
ladder, the workflow that turns a step into a commit, and the environment facts
every session needs. It does not hold progress, run results or next actions:
those are `docs/ACTIVE.md` and `docs/PHASE_LOG.md`.
## M0: the contract
| Step | What | Acceptance |
| --- | --- | --- |
| M0.1 | Fill the documentation contract: this file's ladder, the gates in `docs/PLAN.md`, the environment table below, and the procedures in `docs/TESTING.md`, `docs/DEPLOYMENT.md` and `docs/TROUBLESHOOTING.md` | `grep -rno '{{[A-Z0-9_]*}}' AGENTS.md docs tools` returns nothing, and every command named in a gate exists as a file in `tools/` |
| M0.2 | Pin the toolchain and the upstream source: `tools/doctor.sh` proves the SDK and every host tool, `tools/fetch-retroarch.sh` fetches the pinned revision and checks the tree it got against that commit | `tools/doctor.sh` exits 0 and names each resolved path; `tools/fetch-retroarch.sh --check` prints the pin and what is present, and a second run is a no-op |
| M0.3 | Compile the frontend's own sources, to prove the toolchain end to end: `tools/retroarch-sources.sh` asks RetroArch's build which objects a link needs, `tools/build-retroarch.sh` compiles them | `tools/build-retroarch.sh` reports how many sources it compiled and archives them into `build/ra/libretroarch.a`; every source it could not compile is named in its report |
| M0.4 | Stage the title: `sce_sys/param.json`, the presentation assets, the runtime and the signed image in the layout the loader reads | `bash tools/build-title.sh` writes `dist/<TITLE_ID>/` with `eboot.bin`, `sce_sys/` and `sce_module/libc.prx`, and records `manifest.sha256`; `bash tools/check-manifest.sh` verifies it and `tools/verify.sh integration` passes on it |
| M0.5 | The gate runner: `tools/verify.sh` runs format, unit, build, integration, evidence in order and fails fast | `tools/verify.sh` exits 0 on a clean tree and exits 1 at the first red gate; `tools/verify.sh --list` prints the five commands |
## M1: the console shell
| Step | What | Acceptance |
| --- | --- | --- |
| M1.1 | A minimal PS5 payload that prints its identity through the SDK's stdio and exits | The ELF loads on the console and the captured `klog` holds the identity line; `evidence/m1.1/` carries the capture and the expected line |
| M1.2 | The platform driver that owns a title's lifetime: user service, splash screen, and an exit path that does not return from `main` | Console run shows the splash hidden by our call and a clean close from the home screen; the capture is committed |
| M1.3 | VideoOut bring-up behind a project-owned seam: acquire buffers, present a solid frame, release on shutdown | A frame is presented on the console and the run's capture names the resolution it received (`docs/DEPLOYMENT.md`, "Observing a target run") |
| M1.4 | RetroArch's frontend enters the platform driver and reaches its own main loop in a headless configuration | The console run logs the frontend's own version and driver identity, and the host build of the same step passes `tools/verify.sh` |
## M2: video and input
| Step | What | Acceptance |
| --- | --- | --- |
| M2.1 | Choose the backend with evidence: build against the installed OpenGL 3.3 Core package and list the entry points RetroArch's GL driver resolves against what the package exports | `tools/check-gl-exports.sh` prints the resolved and the missing lists; either the missing list is empty, or it is quoted in full in `docs/FINDINGS.md` and the step closes as a decision to wait for PS5_Vulkan's rung |
| M2.2 | A platform video driver and its matching display context: the console's display is opened, the swapchain or context is created from the backend's public entry points, and nothing else in this repository touches the GPU | The console run shows RetroArch's own driver identity line and the resolution it received; the capture is committed under `evidence/m2.2/` |
| M2.3 | The menu is drawn: the first visible interface, on whichever backend the driver provides | A console run presents a menu frame; the capture holds the driver name, the swapchain extent, the present count and a frame digest. A framebuffer-only driver cannot satisfy this — `docs/FINDINGS.md` records why the display context is what draws a menu |
| M2.4 | Controller input through the payload SDK's pad API into RetroArch's input driver | The capture records the buttons pressed and the menu's reaction, from the mapping table written in this file |
| M2.5 | A libretro core loads and runs one frame of content, headless first and then presented | The core's own identification line, the frame digest and the present count sit in one capture under `evidence/m2.5/` |
## M3: cores and storage
| Step | What | Acceptance |
| --- | --- | --- |
| M3.1 | Core discovery and installation: the scan path, the `.info` metadata and the load path | A console run lists the staged cores and loads one by name; the capture names the core and its version |
| M3.2 | Save and save-state paths on the console's own storage, with the write-through discipline `docs/DEPLOYMENT.md` describes | A state written on the console is read back after a relaunch, and both runs are captured |
| M3.3 | Configuration persistence: `retroarch.cfg` round-trips through the console's storage without losing keys | The diff between the seeded config and the config read back after a settings change names only the changed key |
| M3.4 | A second core of a different class (a non-dynarec, non-GL core) loads, runs and exits | Two core names appear in two captures, each with its own content digest |
## M4: audio, mapping and release
| Step | What | Acceptance |
| --- | --- | --- |
| M4.1 | Audio output through the payload SDK's audio API, with the buffer discipline documented in `docs/FINDINGS.md` | The console run reports the audio format it opened and the frames written; a captured run holds no underrun line |
| M4.2 | Input mapping: buttons, sticks, the menu combo and the PlayStation confirm/cancel convention | A capture records each mapped button's effect once, from a written mapping table in this file |
| M4.3 | The menus at 1080p: readable text, bounded frame time, and no layout that depends on a mouse | A console run at the target resolution with the frame-time record committed |
| M4.4 | Release: the ZIP, its `SHA256SUMS`, and one acceptance run end to end | The release artifact's digest, the run's capture and the expected lines are committed under `evidence/m4.4/` |
Acceptance is the contract. A step is done when its acceptance line is satisfied
word for word, and the evidence it names is committed. A step whose acceptance
was wrong is corrected in this file, in its own commit, before the code that
depends on it lands.
## The workflow
| Phase | What it means |
| --- | --- |
| Spike | Time-boxed, throwaway, never committed as product code. What it learned goes to `docs/FINDINGS.md`. |
| Step | One commit, one acceptance line, verified before it lands. |
| Milestone | Its steps are all done and its gate in `docs/PLAN.md` holds. |
### Turning a step into a commit
1. Restate the acceptance line, and name the artifact that will prove it.
2. Build the smallest thing that can satisfy it.
3. Run the gates in `AGENTS.md`; fix what fails.
4. Commit with the evidence: what was run, what it returned, what it proves.
5. Write the step up once in `docs/ACTIVE.md`; append a dated entry to
`docs/PHASE_LOG.md`.
### Splitting and parking
- If a step needs two independent proofs, it is two steps. Split it in
`docs/PLAN.md`/this file first, then build the first one.
- If a step cannot be verified in this environment (it needs the console, a
loader the console does not have yet, or the Vulkan driver before its rung is
reached), implement it and park it in `parked/` as a patch plus the
verification plan. It is not merged and it is not "done".
## The environment
*Verified on this host with `tools/doctor.sh`. Every session otherwise
re-derives these, at full price.*
| Thing | Value |
| --- | --- |
| Host | CachyOS (Arch-based) Linux, x86-64, bash 5.3, Python 3.14 |
| Host compiler and formatter | clang / clang-format / clang-tidy 22.1.8, `llvm-ar`, `ccache` |
| PS5 SDK | `$PS5_PAYLOAD_SDK`, default `/home/mihawk/ps5-payload-sdk`, unpacked 2026-09-17; environment from `$PS5_PAYLOAD_SDK/toolchain/prospero.sh` |
| PS5 compiler | `prospero-clang` 22.1.8, target `x86_64-sie-ps5`; sysroot `$PS5_PAYLOAD_SDK/target` |
| Upstream source | RetroArch 1.22.2 tarball, fetched into the ignored `vendor/`, patched per `patches/series` |
| Build | `bash tools/build-title.sh` → `dist/<TITLE_ID>/eboot.bin`, signed and manifested; `bash tools/verify.sh build` runs the same command |
| Test | `tools/verify.sh unit` (host-native, GoogleTest) and `tools/verify.sh integration` (shell and Python against the staged tree) |
| Format and lint | `tools/verify.sh format` (`clang-format --dry-run --Werror`, `bash -n`, `python3 -m py_compile`, attribution and JSON checks) |
| Run locally | host-native frontend against the headless driver: `build/host/retroarch --features` and the unit suite; no console is touched |
| Target | A PS5 title folder: a signed fake-self `eboot.bin` (x86-64) with `sce_module/libc.prx` and `sce_sys/`, which is what the console's loader runs |
| Graphics on the target | the relocatable OpenGL 3.3 Core package from `../ps5-opengl-sdk-0.2.0` today, and the Vulkan driver from `../PS5_Vulkan` once it reaches rung 1.0 — see "The graphics backend" below; neither is rebuilt here |
| Vulkan headers for our build | `../ps5-opengl-sdk-0.2.0/third_party/Vulkan-Headers/include`, the vendored copy the local driver builds against; the payload SDK's sysroot ships none |
| Console tools | the resident control payload and `ps5_console.py` from `../PS5_Vulkan` (`launch`, `kill`, `klog` on port 3232); address and credentials come from the ignored `.env` |
| Source of truth for deps | `pin/sources.txt` (versions and SHA-256) and `patches/series` (the patch order) |
## Versions and pins
Anything pinned is pinned here, with the reason. Nothing is pinned in two
places, and nothing is upgraded without a line in `docs/PHASE_LOG.md`.
| Pin | Version | Why |
| --- | --- | --- |
| RetroArch source | 1.22.2, `sha256 245ef18c8fa8fbd9fbb5eb25cf43e17c6aace2f95c1ed99873cbd794012bb232` | The newest upstream tag that carries the Vulkan context driver and the frontend the port needs; the digest is the tarball's own, so a moved tag is caught |
| PS5 payload SDK | `$PS5_PAYLOAD_SDK`, unpacked 2026-09-17 | The only toolchain that produces PS5 payload ELFs; the version is recorded in `pin/sources.txt` so a build is reproducible from a checkout |
| Vulkan driver | from `../PS5_Vulkan` at the revision recorded in `pin/sources.txt` | The driver is this project's graphics backend; a revision that changes a driver entry point must be a deliberate, recorded bump |
| Graphics backend (current) | `../ps5-opengl-sdk-0.2.0`, release 0.2.0, consumed through its installed package | It is the only graphics stack with a recorded hardware acceptance run on this machine; the driver package is never rebuilt here |
| GoogleTest | host-only, fetched and digest-verified into `.deps/test/` | Unit tests run on the host; it is never linked into a PS5 artifact |
## The graphics backend
Two local projects provide graphics; this repository builds neither. Which one a
step uses is recorded in the step's acceptance line, and the choice is evidence
driven, not stylistic.
| Stage | Backend | How this repository consumes it | What it decides |
| --- | --- | --- | --- |
| Now | OpenGL 3.3 Core from `../ps5-opengl-sdk-0.2.0` | its relocatable package, through `ps5-opengl-core33.mk` or its `ps5-opengl-core33.pc`; headers are EGL, GL and KHR | RetroArch's OpenGL path, with the frontend rendering through our own platform context driver |
| After PS5_Vulkan rung 1.0 | the Vulkan driver from `../PS5_Vulkan` | its released static archive; Vulkan headers come from the SDK's vendored copy | RetroArch's `vulkan` context driver, which is the target backend for the shipped application |
Rules that hold either way:
- The application never opens a GPU device directly. It issues EGL/GL or Vulkan
calls through RetroArch's own context driver, so the backend can be swapped
without touching the frontend.
- Whichever backend is in use is named in the run's capture, and a step that
changes backend is its own step with its own acceptance run.
- A backend that fails the step is a finding in `docs/FINDINGS.md` with the exact
call that failed, not a reason to add private GPU access here.
The first M2 step settles the OpenGL question with evidence rather than
assumption: RetroArch's GL driver resolves its entry points through `glsym`, and
a backend that exports fewer functions than the driver asks for fails at load
time. `tools/check-gl-exports.sh` compares the backend's exported symbol list
with the driver's request list and reports the difference; if the difference is
not empty, that difference is the M2 finding and PS5_Vulkan's rung becomes the
blocking dependency for a presented frame.
## Shipping as a PPSA title
The console needs an `eboot.bin` in a title folder with `sce_sys/param.json`
carrying a `PPSA#####` id. This project produces one through
`../ps5-native-app-boilerplate-main`'s pipeline, which is the path already proven
on this console: ProsperoLight, built the same way, starts when placed.
| Piece | Where it comes from |
| --- | --- |
| the tooling that links and signs `eboot.bin` | `tooling/`, taken from the native pipeline and committed as part of this project |
| the loader-visible runtime module | `runtime/libc.prx`, checked against its own digest manifest |
| the title's identity and launcher assets | `sce_sys/`, written by the scaffold from `title/` |
| the application | RetroArch's sources, compiled by `tools/build-retroarch.sh`, linked with `src/` |
The pipeline's builder compiles `src/**/*.{c,cc,cpp}` with fixed flags
(`-std=c11`, `-std=c++20`, `-O2 -Wall -Wextra -ffunction-sections
-fdata-sections`) and links the result with its own CRT, C++ runtime, AGC link
stubs and version script. Two consequences shape this project:
- **RetroArch is reached by include path, not copied in.** Its tree is dozens of
directories with its own configure step, so `tools/build-retroarch.sh` compiles
it separately with the same compiler and the generated `config.h`, and `src/`
holds only this project's code — the entry point and the video driver.
- **Extra link inputs arrive through `APP_STATIC_ARCHIVES`.** That is how a
graphics backend is added when a step needs one; the Vulkan driver's archive is
the intended first use.
## Reference material
Two directories hold material this project did not write, and they are different
kinds of thing:
| Path | Committed | Rule |
| --- | --- | --- |
| `reference/` | yes, with a `PROVENANCE.txt` naming the source, the revision and every file's digest | read-only. It is what a claim was measured against; a change to it is its own step, and the digests are re-recorded in that step |
| `vendor/` | no, ignored | build-time only. `tools/fetch-retroarch.sh` recreates it from the pin on every clean build, so nothing in it can be relied on to persist |
Neither is edited to get a step unblocked. A change we want in either one is a
patch in `patches/` or a documented decision, never a quiet edit.
## Style
- C and C++ follow the upstream RetroArch style inside `platform/` and the
project's own `clang-format` configuration: 4-space indent, 80 columns, braces
on the same line for control flow. Run `tools/verify.sh format` before staging.
- Shell scripts start with `#!/usr/bin/env bash` and `set -euo pipefail`, quote
every expansion, and exit 2 on a usage error and 1 on a check failure — the
convention the console tools in `../PS5_Vulkan` already use.
- Generated code, vendored dependencies and build output are never edited by
hand and never reformatted: `vendor/`, `build/`, `dist/`, `.deps/`, `*.elf`,
`*.so`, and everything under a git-ignored capture directory. `reference/` is
committed but equally read-only: it is the baseline a measurement was taken
against.
## The port
RetroArch on this console is a video driver of this repository's own, presented
through the display layer in `src/display.cpp`. Two menus are available and the
choice is a backend choice, not a preference:
| Menu | Display context | What it needs |
| --- | --- | --- |
| RGUI | none — `menu/drivers/rgui.c` references `dispctx` **zero** times | `poke->set_texture_frame`, `viewport_info`, `video_driver_supports_rgba` |
| XMB, Ozone, MaterialUI | required — XMB dereferences `dispctx` 72 times | a GPU backend: every entry in `gfx_display_ctx_drivers[]` is a GPU API and there is no software one |
So RGUI is the first target: it rasterises the menu into its own framebuffer and
hands that one texture to the video driver, which means it runs over VideoOut with
no GPU stack at all. XMB comes after, over `../PS5_Vulkan`
(284 `ps5vk_` symbols in `libps5vk.ps5.a`) once the frontend is already proven.
The mandatory surface is small, and the rest is optional:
| Interface | Mandatory | Notes |
| --- | --- | --- |
| `video_driver_t` | `init`, `suppress_screensaver`, `alive`, `frame`, `ident`, `poke_interface` | `suppress_screensaver` is called with no NULL check; the rest are NULL-guarded |
| `video_poke_interface_t` | `set_texture_frame` | RGUI pushes its menu framebuffer through it |
| `gfx_ctx_driver_t` | not needed at all | `video_driver_init_internal` never touches one |
Field order matters: the struct is initialised positionally, and `overlay_interface`
sits inside `#ifdef HAVE_OVERLAY` before `poke_interface`, so an initialiser has to
account for it.
## XMB menu assets and build selection
XMB uses RetroArch's existing Vulkan display driver; no new graphics backend or
runtime dependency is added. Configure enables XMB and retains RGUI; upstream's
default selection prefers XMB when Ozone/MaterialUI are disabled. The seed config
matches that selection. Configure arguments are fingerprinted before feature
flags are derived so an existing configured tree cannot silently retain RGUI-only
objects. `ASSETS_DIR` is `/app0`, since platform_unix appends `/assets` itself.
The title currently selects the null platform frontend, so that Unix initializer
does not execute: patch 0063 seeds `/app0/assets` directly in configuration
defaults and records the resolved XMB paths and load result at context reset.
`assets/xmb/source.json` pins the official libretro/retroarch-assets revision
73106363e14e34c08a5854b4cfbc29f184e3b783 and hashes the shipped subset: all 120
fixed menu icons named by RetroArch 1.22.2's xmb_texture_path, the monochrome M+ 1p
font, attribution and licenses. This pin makes the required menu artwork
reproducible without downloading assets at build/run time. Per-system playlist
icons are not shipped yet; standard default/content icons are included.
The XMB ribbon retains its two-float shader layout but uploads a zero-padded
16-byte uniform block (named patch 0060), matching libps5vk's current whole-record
restriction. This is frontend compatibility padding, not general support for
arbitrary UBO ranges in the driver. Named patch 0061 expands each triangle strip
into list vertices with alternating winding and submits the expanded count for
both effect and icon draws. `tests/test_xmb_assets.py` executes the C index mapping
for strips through the full 8,064-vertex ribbon and checks bounds and winding.
Named patch 0062 also binds the existing white texture and nearest sampler for
untextured menu effects. The ribbon shader does not sample them, but libps5vk
currently validates all stage-visible descriptor layout entries rather than just
statically used shader bindings. This uses the same descriptor-fill compatibility
path as textured draws (including patch 0026's second sampler slot).
Named patch 0065 keeps texture coordinates consistent with the physical image
width required by the driver's row alignment. RGBA8 rows need 64-texel alignment;
R8 font rows need 256-texel alignment. Display UVs are scaled to the logical image
region and font atlas offsets are normalized by the physical width. Padding only
the image width changes what a normalized coordinate samples.
Named patch 0066 keeps static/menu textures at one mip level. XMB's full mipmapped
icons rendered as repeated/cropped fragments in the tested libps5vk path; the
single-level images render correctly. This is a compatibility limit, not proof
that the driver's general mip-chain storage/sampling is correct. RGUI and the
CPU video driver remain compiled and selectable. The retired screenshot recipe
is `parked/xmb-readback/README.md`; screenshots are not taken by normal builds.
## Native PS5 audio
`src/audio_ps5.cpp` adapts ProsperoLight's `sceAudioOutInit/Open/Output/Close`
sequence to RetroArch's `audio_driver_t`. Patch 0067 registers `audio_ps5` and
selects `ps5` by default; the staged config agrees. The backend opens the system
user's main output at 48,000 Hz, signed 16-bit interleaved stereo, 256 frames per
native output call. `new_rate` tells RetroArch to resample other source rates.
Only the default device is supported. No SDL or Opus dependency is introduced.
A dedicated worker feeds AudioOut, whose synchronous output call paces playback.
The producer and consumer share a bounded ring guarded by a mutex/condition;
no lock is held while calling AudioOut. Requested latency sizes the ring in
256-frame multiples, with a 512-frame minimum and 8,192-frame maximum. A zero
latency request uses 64 ms (3,072 frames). An additional native block can be in
flight. `write`, `write_avail` and `buffer_size` use bytes, matching RetroArch's
call sites. Blocking writes wait for space; nonblocking writes return the bytes
accepted, including zero when full. Empty or partial blocks are zero-filled.
Pause drops queued samples, waits for the in-flight block, then drains the native
port. Resume uses the same worker. Free wakes and joins the worker before draining
and closing the port. Output errors make the driver inactive and wake blocked
writers. Startup, failures and close summaries are logged; successful blocks are
not individually logged. Silence includes normal idle/menu output and padding,
so it is not by itself evidence of a streaming underrun.
The opt-in backend test is documented in `docs/TESTING.md`. Core integration,
long-duration A/V synchronization and streaming underrun behavior need their own
content-based acceptance runs; native test tones do not prove those properties.
## Native platform frontend and paths
`frontend_ctx_ps5` supplies platform initialization, a startup environment callback
and a drive-list callback (patch 0068). Its non-null environment callback keeps the
initial command line intact. Without it, task_content substitutes
`menu_content_environment_get`, reconstructs argv from empty startup state and
loses `-c`. Subsequent menu-initiated content loads retain upstream's menu callback.
The frontend seeds `g_defaults` with paths under `/app0`, creates the title's own
config/core/content/system/save/playlist directories, and copies the packaged seed
to `/app0/config/retroarch.cfg` only when that file is absent. A temporary file and
rename avoid installing a partial copy. Existing configs are left intact, including
when a newer packaged seed is uploaded. `docs/DEPLOYMENT.md` maps these internal
paths to FTP. Platform defaults apply when an existing config leaves paths unset.
Patch 0069 returns `/app0/eboot.bin` for application-path discovery. The generic
Unix procfs/getpid/readlink path is inappropriate for a PS5 title; configuration
saving invokes it while abbreviating paths. Patch 0070 routes RetroArch's VFS
`opendir/readdir/closedir` calls through `src/ps5_directory.cpp`. The adapter uses
public SDK `open(O_DIRECTORY)`, `getdents`, and `close`, validates variable-length
FreeBSD records, skips deleted entries and returns directory types/names. Reads
use a 64 KiB buffer: 4 KiB failed with EINVAL on the tested `/app0` mount even
though it worked on `/`; 64 KiB succeeded. The minimum accepted size was not
determined. It does
not change file reads/writes or bypass the title's filesystem permissions.
The title compiles against the configured libretro-common include tree. Its
headers resolve relative `config.h` includes there; using the pristine vendor
include tree fails as soon as the native frontend includes the menu interfaces.
No new dependency, SDK version or graphics-driver change is involved.
Managed directories (`config`, `cores`, `content`, `system`, `savefiles`,
`savestates`, `playlists`) use mode `0777`. Startup applies `chmod` after `mkdir`,
including when the directory already exists, so an inherited umask cannot remove
FTP write access and older `0755` directories are repaired. Mode `0775` was
applied but still denied real FTP uploads on this console, despite matching
groups in directory listings; the owner authorized `0777`. This policy
changes only those directory modes; it does not recursively rewrite user files.
### Native joypad and binding interface
`input_ps5` pairs with `ps5_joypad`, registered before the null joypad. The joypad
owns the single initial-user pad handle and exposes 16 raw buttons plus six axes
(left X/Y, right X/Y, L2/R2). The built-in `PS5 Controller` profile maps them to
RetroPad defaults through RetroArch's normal autoconfiguration. Existing saved
configs with an empty joypad driver select it automatically; new seeds name
`ps5`. Explicit user bindings override the profile.
The input interface adds no raw gamepad state: OR-ing the old direct mapping into
RetroArch's mapped state would make reassigned buttons remain active. Menu analog
navigation and binding capture use the joypad callbacks. Sticks span -32767 to
32767 centered on byte 128; triggers span 0 to 32767. RetroArch owns deadzone and
axis-threshold handling. A zero-sample read preserves the previous sample (the
binding screen may poll twice); errors/disconnects and shell interception suppress
input. No per-frame or per-button logging is added. Rumble and multiple controllers
remain unsupported in this backend.
## FCEUmm core build
`make fceumm` (or `tools/build-fceumm.sh`) cross-builds the pinned upstream
FCEUmm revision and its pinned core-info metadata. The script verifies both
input SHA-256 digests, extracts fresh sources into `build/cores/fceumm`, and
uses this project's `.deps/native/ps5-payload-sdk` wrappers for CC/CXX/AR/LD.
It does not modify the fetched upstream source. `platform=unix` selects upstream
source/features, while explicit compiler and linker make variables prevent a
host build. The linker is the compiler driver because upstream passes `-Wl`
options; `-nostdlib -nodefaultlibs` excludes payload CRT/static libc defaults.
The `--build-id=sha1` override replaces Clang's random PS5 UUID with a
content-derived ELF ID so repeated builds have stable hashes.
`-T tooling/native/ps5-core.ld` separates executable, read-only and writable
segments on 16 KiB boundaries; the loader never maps writable executable pages.
The core checker rejects a writable executable load segment.
Math and libc imports come from the native runtime stubs, with `libkernel_web`
rather than the payload `libkernel_sys`. No SDL or decoder is added.
Outputs are `build/cores/stage/cores/fceumm_libretro.so` and
`build/cores/stage/info/fceumm_libretro.info`. `tools/build-title.sh` builds and
copies both after assembling the title, before recording its manifest. Native
core metadata defaults to `/app0/info`. The same `.info` file is also staged
beside the core because older saved configs set `libretro_info_path = ""`;
upstream then searches the core directory. Existing settings are preserved.
`build/cores/fceumm/build.json` records
source/metadata pins, hashes, ELF imports, exported callbacks and compiler-wrapper
identity. First use requires network access; verified cached inputs allow offline
rebuilds. `JOBS` controls build parallelism.
The reference was john-tornblom's websrv
[core build script](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/RetroArch/build-snes9x2010.sh).
Only its fetch/build/stage pattern applies here: this application uses the native
title pipeline and its own SDK. A shared ELF passing the ABI gate is not proof
that the native title's dynamic loader can load it. FCEUmm renders NES frames in
software; RetroArch presents those frames through the existing Vulkan driver.
After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
## Native in-process core loader
`src/core_loader_ps5.cpp` supplies the dynamic-library operations used by the
frontend. It loads this pipeline's ELF64 x86-64 FreeBSD-ABI shared cores into
anonymous memory, resolves their imports against explicit title bindings,
applies checked RELA relocations, then protects each segment as RX, R or RW.
File reads use bounded POSIX reads into a separate mapped buffer. Failed opens
release allocations and report the failing operation; successful handles are
reference counted and unmapped on the final close.
`tools/core-imports.py` derives the union of required native bindings from the
explicitly shipped FCEUmm, mGBA, Snes9x, FBNeo and Genesis Plus GX ELFs; bindings and all cores participate in the title build
identity. Directory imports use this port's directory adapters, including `rewinddir`.
The `localtime_r` binding uses RetroArch's existing locked `rtime_localtime`
helper, initialized before gameplay. The
runtime dependencies are limited to the public kernel_web, libc and Posix stubs.
This route does not depend on websrv's payload loader hooks or publish native
module exports through the title converter.
The initial supported contract is deliberately narrower than a general dynamic
linker: no TLS, ELF interpreter, legacy DT_INIT/DT_FINI, C++ unwind
registration, REL/RELR, or additional dependent shared libraries. Bounded
DT_INIT_ARRAY callbacks are supported: every relocated pointer must target this
module's executable segment, and all callbacks are checked before any run.
Initializers run once per new mapping after protection, before exposing the
handle; a reference-counted reopen does not rerun them. Unsupported imports and
relocations fail explicitly. Adding another core requires reviewing this
contract and extending both host tests and target diagnostics as needed.
Core selection checks load success before content startup tears down drivers.
The main loop also refuses to poll drivers after frontend initialization fails.
This protects against load failures; it cannot isolate a fault inside arbitrary
core code running in the same process.
Software core frames in libretro XRGB8888 are converted by
`src/core_frame_ps5.cpp` to RGBA8 bytes with opaque alpha, respecting both row
pitches and in-place framebuffers. The Vulkan frontend requests RGBA8 for both
staging and sampled core textures. This extends the menu's matching-format
upload rule to actual content, avoiding the RGB565-only compute branch. Frame
scaling and presentation remain on the Vulkan GPU path; `video_ps5` stays
registered as the selectable fallback. RGB565 core frames retain their existing
path and require separate content acceptance.
## mGBA core build
`make mgba` runs `tools/build-mgba.sh` with the same SDK and ELF linker layout as
FCEUmm. The pinned source is the [official libretro mGBA tree](https://github.com/libretro/mgba/tree/7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6).
Source and `.info` inputs have verified SHA-256 digests; a fresh extraction goes
to `build/cores/mgba/`. CMake is required by this source revision. The wrapper in
`tooling/mgba/` selects only the shared libretro target, both GB and GBA engines,
software XRGB8888 output, and native kernel_web/libc/Posix imports. External
frontend/media dependencies are disabled: RetroArch supplies presentation,
audio, archive extraction and content services. No SDL, Qt or OpenGL backend is
added. This core uses Vulkan for frontend presentation, not hardware emulation.
The toolchain restricts header/library/package searches to the SDK. Function
probes link against the actual native stubs without a CRT and are never executed;
static-library probes falsely reported unavailable locale functions. The SDK
provides locale types/headers but lacks several locale runtime functions, so the
small `patches/mgba/native-locale-type.patch` uses a private formatting type for
upstream's string-locale fallback; `HAVE_XLOCALE` is removed from this target.
The patch is applied with zero fuzz to the fresh source. `HAVE_LOCALTIME_R` names
the frontend adapter, avoiding upstream's unimplemented fallback. `-z undefs`
permits that frontend-provided import; the generated binding table must still
resolve every core import when the title links.
`--build-id=sha1` keeps output repeatable. `GIT_CEILING_DIRECTORIES` prevents
upstream's version generator from discovering RetroArch's parent Git checkout;
the archive reports its own 0.11.0 version, with the exact source revision in
`build/cores/mgba/build.json`. That report also contains the SDK wrapper, source,
metadata and port-input hashes. No source checkout or environment is modified.
Outputs are `build/cores/stage/cores/mgba_libretro.so` and
`build/cores/stage/info/mgba_libretro.info`. The title build stages all shipped cores,
with metadata in both `info/` and `cores/` for existing saved configurations.
Changing the shipped core list requires rebuilding the frontend's native import
bindings. Console loading and manual gameplay remain separate target checks.
`MGBA_PS5` selects `/app0/config/mgba` for mGBA's optional standalone config
and disables `portable.ini` working-directory discovery. The zero-fuzz
`native-config-path.patch` avoids native `getcwd`/Unix home discovery; core
options continue to come from RetroArch's libretro environment.
## Large application allocations
`src/memory_ps5.cpp` maps allocations of at least 1 MiB with the native kernel
and keeps smaller requests on libc. The title linker wraps `malloc`, `calloc`,
`realloc` and `free` together, including the generated core binding references.
This flag is required because a 16 MiB 7z extraction allocation returned null
through the native libc import. The donor also uses mappings for large buffers;
this implementation tracks mappings in a mutex-protected list and never reads
headers before unknown pointers. Buffers allocated inside native libraries
remain libc-owned and are forwarded to native free/realloc. Resize of a mapped
buffer preserves its contents and leaves the old allocation intact on failure.
Resize of a libc-owned buffer remains a libc operation, including growth beyond
the threshold. Do not pass a mapped pointer to an API that privately frees or
reallocates it inside an unwrapped native library.
Patch 0076 reports 7z allocation/header/extraction failures without filenames
and checks allocation before copying extracted content. It logs errors only.
The mGBA libretro callback converts native XBGR to its declared XRGB8888 format
using `ps5-video.h` and a separate bounded video buffer. It leaves the renderer's
pixels intact for duplicate/cached frames. Upstream's 16-bit-only colour
correction/interframe-blending routines are not enabled by this 32-bit build.
Patch 0077 sets RGBA menu image decoding on every Vulkan context initialization
and requests RGBA textures for those decoded pixels. This is separate from core
XRGB frames. The Vulkan software-framebuffer callback returns false: callers
must retain their own original pixels, since exposing RGBA staging memory as
writable XRGB would let upload conversion mutate cached frames while paused.
FCEUmm uses its own buffer when that optional callback is unavailable.
On normal frontend return, `catchReturnFromMain` requests
`sceSystemServiceLoadExec("exit", nullptr)` and waits for asynchronous shell
termination on success. RetroArch has already cleaned up drivers and saved its
configuration. A failed request is logged and falls back to the existing CRT
termination path; it must not be described as a successful clean exit.
### Padded core presentation
Patch 0078 extends the existing 256-byte texture-row workaround to software
core presentation. The filter chain carries logical width separately from the
sampled image width and crops both triangle UVs to that initialized region.
Each pass owns one small vertex buffer per retired sync slot, so CPU updates do
not overwrite another in-flight frame. Swapchain reconstruction releases these
buffers. Hardware-provided images and intermediate pass outputs keep their own
extents; sampled format substitution is applied before computing padded width.
The host regression covers FCEUmm, the blank menu frame, GBA and GB widths across
repeated transitions. Console flicker acceptance is recorded in `evidence/mgba-native/`. This is
not validation of arbitrary Slang presets, history sampling or linear-filter
edge behaviour; those remain separate milestones.
## Snes9x core build and colour contract
`make snes9x` runs `tools/build-snes9x.sh` against pinned official libretro source
`fae2fea08f74180759ef540ee94259213f503480` (Snes9x 1.63). Its own libretro Makefile
selects the sources; CC/CXX/AR/LD are explicitly the project's SDK wrappers.
LTO remains off as upstream defaults, and upstream's no-strict-aliasing,
no-exceptions/no-RTTI flags remain. No alternate SDK, CRT, SDL or host library
is linked. The existing native linker script separates RX/R/RW pages. Native
imports and C++ SDK runtime references are bound by the frontend's explicit
union table; `-z undefs` permits these at core link, while the title link must
resolve every entry. Metadata uses the same official core-info revision as the
other cores; its upstream display_version says 1.61, while the core reports 1.63
plus its source revision. The metadata otherwise stays upstream unchanged.
The renderer retains RGB565 including its NTSC and hires paths. The callback
patch converts to a separate bounded XRGB8888 buffer, reports XRGB8888 at both
normal and subsystem load sites, and supplies the converted 32-bit pitch.
The existing frontend XRGB-to-RGBA conversion then feeds matching staging and
sampled textures. Cached native pixels remain unchanged. Tests cover every
RGB565 value end to end, non-tight pitch and 256/512/602/1024-pixel widths through
478 rows. These tests do not by themselves establish console acceptance of
NTSC filtering, HD Mode 7 or interlaced games.
Snes9x has C++ globals. `tooling/native/core_cxx_runtime.cpp` is linked locally
inside this core, hidden by upstream's version script. Its own __cxa_atexit list
runs in reverse registration order via a fini-array callback, so unloaded code
is never retained in the process-wide exit registry. The loader accepts bounded
DT_FINI_ARRAY/DT_FINI_ARRAYSZ, validates all targets before any initializer, and
runs finalizers in reverse array order on the last close before unmapping.
Reload gets a new registry; a rejected load executes no callbacks. This does not
add TLS, legacy init/fini functions or exception-unwind registration support.
As with initializers, lifecycle callbacks must not reenter this bounded loader.
## FBNeo native core contract
`make fbneo` uses pinned official libretro/FBNeo source
`6bb3167a044e19e7106a5110d5531aa9c6afa96f` and the project's native SDK wrappers.
The full upstream source selection remains, including ZIP/7z and CHD support;
no ROM, BIOS, samples or user-provided metadata is shipped. Official core-info
revision `5a74858ab2f7a50cebb5a6330895bc38899531c0` supplies the `.info` file.
Archive digests are verified before fresh extraction. The local make wrapper
removes obsolete GCC `-fforce-addr`, selects C++11 for SDK libc++, and disables
exceptions/RTTI because the native module loader has no unwind registration.
The shared core-local destructor registry handles C++ globals before unmapping.
Native imports are included in the title's explicit union table, so the core
and updated frontend must be deployed together.
Native 32-bit FBNeo output is already XRGB8888 (0x00RRGGBB). Drivers flagged
16-bit-only retain RGB565 internally; the video callback converts those frames
into a separate XRGB8888 buffer with a 32-bit pitch. Source pixels are never
mutated, and NULL duplicate-frame callbacks remain NULL. The existing frontend
converts XRGB into RGBA for the Vulkan texture upload. This is CPU emulation and
software rendering with Vulkan presentation, not Vulkan hardware emulation.
The MPEG layer 2/AMM decoder uses an exception solely to escape an exhausted
bit reader. The native patch substitutes an instance-local setjmp/longjmp
escape and the same false return. The traversed decoder frames have only
trivial local variables; no C++ destructor is bypassed. This is not general
exception support. Differential host tests compare the original and adapted
real decoder on complete and truncated synthetic layer 2 and AMM input. Core metadata
queries also use a bounded static version string, avoiding upstream's
unowned duplicate on each query.
At runtime FBNeo sets need_fullpath=true and block_extract=true: RetroArch
passes the whole archive to the core. Do not unpack arcade ZIP/7z collections
as single-content archives. BIOS lookup includes the frontend's system/fbneo/
and system/ directories and the content directory. User BIOS/ROM sets must
match the pinned core's requirements; a successful build is not a compatibility
claim for every supported board, disc format or subsystem.
FBNeo's writable driver catalogue uses two allocations: a bulk name store and
its pointer tables. Thousands of individual sub-1-MiB allocations otherwise
exhaust SceLibcInternal's small heap during BurnLibInit. The name store crosses
the frontend allocator's existing 1 MiB mapping threshold; that allocator and
all other cores stay unchanged. Catalogue construction validates lengths and
allocations before changing driver pointers, and exit restores the original
pointers before freeing storage. Repeated exit/reinitialization is safe.
Initialization failure is logged and causes regular/subsystem content loads to
return false. No partially initialized catalogue is used for game execution.
## Genesis Plus GX native core contract
`make genesis-plus-gx` uses official libretro/Genesis-Plus-GX source
`c2838c7dc4236fc2fe94e5dbd08b41486067918e` (archive SHA-256
`7ba2eab9d6dae71bb42e8208573300ad3475a4263e860178c4d19c36d85fc92b`).
Official core-info comes from `5a74858ab2f7a50cebb5a6330895bc38899531c0`,
SHA-256 `9793bff8d9e298a7ee0c94c0511dab242200ca60fbe87e3720eb9231a3e0166a`.
The supplied .info is reference material; only the hash-checked official file is
staged in info/ and cores/. The fresh source build uses this project's explicit
SDK compiler/archiver wrappers and native linker script/import union, without a
payload CRT. `HAVE_CDROM=0` disables Linux-host autodetection of physical CD-ROM
access, retaining CHD and disc-image codecs. `ZSTD_TRACE=0` disables optional weak
instrumentation hooks for which the title has no external provider. It does not
disable core/frontend error logging. Source, metadata, SDK wrapper and port-input
hashes are recorded in build/cores/genesis_plus_gx/build.json.
The software renderer, NTSC filters, cursors and Game Gear LCD persistence retain
upstream RGB565 storage. A separate bounded XRGB8888 callback buffer preserves the
cached source and the existing frontend's RGBA Vulkan upload contract. Conversion
uses the actual 720-pixel input stride and byte viewport offset; output is packed
at width*4 bytes per row. Rejected XRGB8888 negotiation rejects content through
upstream cleanup. Invalid views are logged and never read out of bounds. Skipped
frames still submit NULL. The patch normalizes libretro.c line endings only in
the disposable extraction and corrects the unused-return update_geometry helper
to void. Frontend video code and PS5_Vulkan require no change.
Upstream covers Mega Drive/Genesis, Master System, Game Gear, SG-1000 and Sega CD;
this is a software core using Vulkan presentation, not a Vulkan hardware renderer.
Its runtime uses need_fullpath=true and block_extract=false, allowing frontend
archive extraction. Place region-specific Sega CD BIOS files in the configured
system directory root, following the official .info names. No BIOS or ROM is
bundled. Console acceptance is limited to the systems and transitions actually
recorded in evidence; available build features do not establish full coverage.
-79
View File
@@ -1,79 +0,0 @@
# Releasing
What a published release must carry, and the order that makes the tag, the ZIP and
the source agree. The history behind each rule is docs/releases/SOURCE_CORRESPONDENCE.md.
## What a release carries
1. **The title ZIP**, `PS5_RetroArch-<tag>.zip`, holding `PPSA99169/` with its
`licenses/` folder: the licence texts each part requires, `components.json`
(every part's licence and source revision, and the sha256 of every executable
file) and `README.txt`. `tools/build-title.sh` writes it
(`tools/stage-notices.py`, from `tooling/notices/components.json`).
2. **The source archives** from `tools/source-bundle.py`, with their `SHA256SUMS` and
`SOURCES.txt`, attached to the same release. The GPLv3 lets the source sit on
another server with clear directions (section 6(d)), and GPLv2 counts access from
the same place as the binary (section 3); attaching the archives satisfies both,
and it covers the cores this project fetches from repositories it does not
control. Genesis Plus GX's licence asks for the complete source of a modified
redistribution, which the archives are.
3. **The notes**, with a "Source" section naming the commit the ZIP was built from
(it is the tag, see below) and the non-commercial parts (Snes9x, FBNeo, Genesis
Plus GX): they may not be sold. FBNeo's licence also forbids asking for donations
for a project that uses its code, so a release page carries no donation link.
## The order
1. **Commit and push everything that is linked in.** PS5_RetroArch itself, and every
revision it pins or takes from a sibling: PS5_PayloadSDK (`sdk_revision`), each
core fork, PS5_Vulkan (its `HEAD`) and the PS5_Mesa revision its RADV archive
records. Check that each is on GitHub before building:
```bash
gh api repos/mihawk-99/PS5_Mesa/commits/<revision> --jq .sha
```
2. **Build from the clean tree**, naming the release:
```bash
PS5_RELEASE_TAG=v0.6.0-alpha.6 bash tools/build-title.sh
bash tools/verify.sh
python3 tools/check-notices.py --release
```
`--release` fails when any part was built from uncommitted source or has no
published address.
3. **Make the source archives and the ZIP**:
```bash
python3 tools/source-bundle.py dist/PPSA99169 dist/source-v0.6.0-alpha.6
(cd dist && zip -r -X PS5_RetroArch-v0.6.0-alpha.6.zip PPSA99169)
sha256sum dist/PS5_RetroArch-v0.6.0-alpha.6.zip
```
An asset may be at most 2 GiB. The largest archives are PPSSPP (about 560 MB,
its submodules included), MAME and the parts of LLVM RPCS3 links.
4. **Tag the built commit and push the tag before creating the release**, so GitHub
does not make a tag from whatever `main` holds:
```bash
git tag -a v0.6.0-alpha.6 -m "PS5 RetroArch v0.6.0-alpha.6" <built commit>
git push PS5_RetroArch v0.6.0-alpha.6 # this repository's remote for github.com/mihawk-99/PS5_RetroArch
gh release create v0.6.0-alpha.6 --verify-tag --prerelease \
dist/PS5_RetroArch-v0.6.0-alpha.6.zip dist/source-v0.6.0-alpha.6/*
```
5. **Read it back.** Download the ZIP from the release, compare its sha256 with step
3, unpack it and run `python3 tools/check-notices.py <unpacked>/PPSA99169`.
## Parts that need a decision before they ship
- **RPCS3** is GPL-2.0-only and is combined with GPL-3.0-or-later code (this port's
core runtime object is linked into it, and it runs against the title's GPL-3.0
platform code). Do not ship it in a release until that is resolved; the audit's
counsel packet sets out the options.
- **Artwork whose origin is not recorded** (`sce_sys/pic0.dds`, `pic1.dds`) should be
replaced with artwork whose licence is known, or its origin recorded, before the
next release.
-679
View File
@@ -1,679 +0,0 @@
# RPCS3 as a libretro core: the port
This document is the audit behind porting RPCS3 (PlayStation 3) to the PS5
title as `rpcs3_libretro.so`, and the plan it leads to. Progress and run
results are recorded in `docs/ACTIVE.md` and `docs/PHASE_LOG.md`, not here.
## The objective
On my console, `rpcs3_libretro.so` boots God of War HD (the PSN release
NPUA80490) through RetroArch's Load Content or `/app0/args.txt`. The game
reaches gameplay, rendered by RPCS3's Vulkan renderer on RADV at a 300%
resolution scale (3840×2160 internal for its 720p output). The evidence is:
- a RetroArch screenshot of gameplay whose picture I accept;
- ten minutes of play with no crash;
- the Close Content + reload battery passing;
- each 10 s audio window's speed, recorded. Full speed is not part of the
objective, but its measurement is.
What the console holds (read in place, never copied anywhere):
| Item | Where on the console |
| --- | --- |
| PS3 firmware | `/app0/system/RPCS3/PS3UPDAT.PUP` (206 MB) |
| The game | `/app0/content/PS3/God of War.pkg` (6.3 GB), a PSN package |
| Its licence | `/app0/content/PS3/UP9000-NPUA80490_00-GODOFWARHDUS0000.rap` |
Because the game is a package and not a disc folder, the first content path is
"install the package and its RAP, then boot the installed game". The
installed copy needs another 6.3 GB on the console's storage.
## The fork and the pin
`../PS5_RPCS3` is my fork of RPCS3/rpcs3 (github.com/mihawk-99/PS5_RPCS3,
public), with an `upstream` remote. Its `main` starts at upstream
**1707d7fc883e** (2026-09-28, "remove unnecessary #ifdef"): the newest master
commit whose CI passed all thirteen checks, including RPCS3's FreeBSD build.
The PS5's system software is FreeBSD-based, so that is the configuration
closest to this port. Every port change is a commit on the fork's `main`, and
`tools/build-rpcs3.sh` pins the fork by revision. Submodules RPCS3 names by
relative URL (`../../llvm/llvm-project.git` and the rest) resolve from the fork
as from upstream. A submodule that needs changes gets its own fork,
`../PS5_<Name>`, referenced by relative URL, as PS5_Azahar does with
PS5_Dynarmic.
## Licence
RPCS3 states its licence as GPL-2.0-only (its README; `LICENSE` is the GPLv2).
The rest of the title is GPL-3.0-or-later (RetroArch, this port, the payload SDK
fork and its platform layer), and GPL-2.0-only code and GPL-3.0 code cannot be
combined into one work and distributed. Whether the core and the title are one
work is not settled by the core being a separate `.so`; these are the facts that
bear on it (2026-09-29):
- `rpcs3_libretro.so` statically links `tooling/native/core_cxx_runtime.cpp`
(this repository, GPL-3.0-or-later by the repository's licence) through
`PS5_CORE_LINK_INPUTS`, beside LLVM (Apache-2.0 WITH LLVM-exception, whose
exception addresses GPLv2 combinations), FFmpeg and GNU libiconv
(LGPL-2.1-or-later).
- At load time the title's native loader binds the core's libc and file-system
imports to the title's own wrappers and to the platform layer
(`tools/core-imports.py`, `build/core_imports.inc`), and the core runs in the
title's process, on the title's heap, through RetroArch's libretro and Vulkan
context interfaces.
So it is not in a release. The ways out are a licence decision for the owner of
the GPL-3.0 parts that touch the core (dual-licensing `core_cxx_runtime.cpp`
and the platform layer's bound functions under a GPLv2-compatible licence, which
is possible only for code whose authors agree), a change of interface, or
counsel's view that the combination is not one work; the licence audit of
2026-09-29 sets them out for that decision.
The title bundles no PS3 firmware, keys or games; RPCS3 installs the firmware
the user supplies and decrypts packages with the user's own licence files
(RAP); that audit covers those paths too.
## Phase 1: the audit
### 1. Dependencies
RPCS3 builds its emulator as the static library `rpcs3_emu`, and its Qt
application links that. Upstream already builds `rpcs3_emu` without Qt for its
Android port (`ANDROID` in `CMakeLists.txt`): Qt, the Qt UI and the OpenGL
renderer are left out, OpenAL is compiled out (`WITHOUT_OPENAL`) and the
bundled FFmpeg is not used. The libretro
build follows that path, with a `LIBRETRO` switch of its own where Android's
choices differ.
`3rdparty/` at the pin (submodule revisions in brackets):
| Item | What RPCS3 uses it for | Decision |
| --- | --- | --- |
| llvm (llvm-project `ca7933e4`, LLVM 22.1) | PPU and SPU recompilers | **Build**: a pinned static LLVM for `x86_64-sie-ps5`, X86 target only (below) |
| asmjit (`416f7356`) | SPU ASMJIT recompiler, trampolines, JIT helpers | **Build**, its code memory through the platform layer (gap a) |
| ffmpeg (RPCS3/ffmpeg-core `649d9d93`, prebuilt) | video and audio decoding (cellVdec, cellAdec, ATRAC3+, cellMusic) | **Build from source**: a pinned FFmpeg release, static, software decoders (below); the prebuilt archive has no PS5 build |
| glslang (`f0bd0257`) | GLSL to SPIR-V for the Vulkan renderer's shaders | **Build** |
| GPUOpen/VulkanMemoryAllocator (`1d8f600f`) | Vulkan memory | **Build** (header library) |
| zlib (`da607da7`), zstd (`f8745da6`) | archives, save states, caches | **Build** |
| libpng (`3061454d`), stblib (`013ac3be`), bcdec | images, texture decoding | **Build** (bcdec and stb are headers) |
| yaml-cpp (`51a5d623`), pugixml (`c8033ce9`) | configuration, patches, XML | **Build** |
| SoundTouch (`a0fba77b`) | audio time-stretch in the resampler | **Build** |
| fusion (`93254240`) | motion-sensor fusion in `PadHandler.cpp` | **Build** (small C library) |
| unordered_dense | hash maps | **Build** (header) |
| protobuf (`edaa823d`) | RPCN (NP) messages | **Stub**: RPCN is compiled into `rpcs3_emu` unconditionally, so a `LIBRETRO` option leaves the NP network clients out with an offline implementation. Networking is dropped |
| wolfssl (`3c5eead4`), curl (`01346829`) | RPCN and clans over TLS | **Drop**, with the network clients (above) |
| miniupnp (`d66872e3`) | UPnP port mapping | **Drop**, with `upnp_handler` stubbed |
| libusb (`87a55632`) | USB passthrough (`sys_usbd`, `usb_device`) | **Stub**: no PS5 backend; `sys_usbd` reports no devices |
| hidapi | HID pads, only in `rpcs3/Input` | **Drop** (the libretro pad handler replaces `rpcs3/Input`) |
| rtmidi (`1e5b4992`) | MIDI instruments (Rock Band) | **Build** with RtMidi's dummy API, or stub the MIDI devices |
| OpenAL | microphone capture in cellMic | **Drop**: `WITHOUT_OPENAL`, as on Android |
| cubeb, FAudio, XAudio2 | audio output backends | **Drop**: a libretro audio backend replaces them |
| libsdl-org/SDL | input, camera, FAudio's dependency | **Drop** |
| opencv | PS Move tracking in `rpcs3/Input` | **Drop** |
| discord-rpc, feralinteractive (GameMode), pine | Discord, GameMode, PINE IPC | **Drop** |
| 7zip | not used by `rpcs3_emu` at the pin | **Drop** |
| MoltenVK, GL | macOS Vulkan, OpenGL headers | **Drop** (no OpenGL renderer, as on Android) |
| Qt 6 | the UI | **Drop** |
CMake options (top-level `CMakeLists.txt`):
| Option | Default | Port |
| --- | --- | --- |
| `USE_NATIVE_INSTRUCTIONS` | ON | OFF; `-march=znver2` instead, the PS5's Zen 2 (AVX2, FMA, BMI2), set in the toolchain file |
| `WITH_LLVM` | ON | ON |
| `BUILD_LLVM` | OFF | OFF: LLVM is built separately and found through `LLVM_DIR` |
| `STATIC_LINK_LLVM` | OFF | ON |
| `USE_FAUDIO`, `USE_SDL`, `USE_LIBEVDEV`, `USE_DISCORD_RPC`, `USE_GAMEMODE` | ON/OFF | OFF |
| `USE_VULKAN` | ON | ON |
| `USE_PRECOMPILED_HEADERS` | OFF | OFF (ccache) |
| `USE_SYSTEM_*` | mixed | OFF throughout: every library is built for the PS5 or dropped |
| `HAS_MEMORY_BREAKPOINTS`, `BUILD_RPCS3_TESTS`, `RUN_RPCS3_TESTS` | OFF | OFF |
| `USE_LTO` | ON | OFF: a 1 GiB-class shared object links in time without it; revisit when it runs |
**LLVM.** RPCS3 requires LLVM 18 or newer and builds 22.1 from its submodule.
I build that same revision (`ca7933e4`) once, as a pinned static install
(`tools/build-llvm.sh` into `.deps/native/llvm-ps5`), with `prospero-clang`,
ccache, `LLVM_TARGETS_TO_BUILD=X86`, and no tools, tests, docs or examples, and
point RPCS3 at it with `LLVM_DIR`. Keeping it out of RPCS3's build tree means a
change to RPCS3 never rebuilds LLVM. LLVM's own `Support` layer assumes Unix
facilities (`mmap`, `sysconf`, `dladdr`, signals) that the platform layer and
the core loader's import table provide or refuse; the link shows which. The
archive size, and what it adds to the core, are measured at step 3; LLVM with
only the X86 target is on the order of a few hundred MB of archives, of which
the core links the code generator, MCJIT and RuntimeDyld.
**FFmpeg.** RPCS3's prebuilt `ffmpeg-core` has no build for this console, so I
build FFmpeg from a pinned source release: static, no programs, no network, no
hardware acceleration. The decoders are the ones the PS3's media libraries
need (H.264, MPEG-2, MPEG-4 Part 2, ATRAC3, ATRAC3+, AAC, MP3, AC-3, LPCM), with
libavformat's MP4, MPEG-PS and raw demuxers, libswscale and libswresample. No
encoders: video recording is dropped. Software decoding comes first. The
console's hardware video decoder, which ProsperoLight uses, is a later option
for H.264.
### 2. The frontend
RPCS3 drives `Emu` (the `Emulator` in `rpcs3/Emu/System.h`) from a frontend
that fills `g_emu_callbacks` (`rpcs3/Emu/emu_callbacks.h`: 49 `std::function`
hooks) and calls `Emu.Init()`. `headless_application` shows the minimum: it is
a `QCoreApplication` whose callbacks create null renderers and handlers, and
it refuses Vulkan. The libretro layer, `rpcs3/libretro/` in the fork, replaces
it without Qt:
- **Entry points.** `retro_init` sets RPCS3's directories (below) and fills the
callbacks. `retro_load_game` installs the firmware if it is missing, installs
a package if the content is one, then boots. `retro_run` runs the queued
main-thread calls (`call_from_main_thread`, which RPCS3 uses for UI-side work
on the frontend's thread), presents the newest frame and pushes audio.
`retro_unload_game` stops `Emu` and waits for its threads.
- **Vulkan.** RPCS3 creates its own instance (API 1.2) and device
(`vkutils/instance.cpp`, `vkutils/device.cpp`). Through RetroArch's
context-negotiation interface (v2) the core creates the instance at the
version RPCS3 asks for (`create_instance`), and the device with RPCS3's
feature and extension choices (`create_device2`). RetroArch uses that
instance and device. RetroArch's own instance is 1.1, which is why the core
creates it. RPCS3's existing `native_swapchain_base` owns its presentable
images. A `swapchain_libretro` on top of it presents by handing the finished
image to RetroArch with `set_image` and `set_signal_semaphore`, in the layout
RetroArch expects. Every `vkQueueSubmit` and present that RPCS3's RSX thread
makes on the queue it shares with RetroArch goes between the interface's
`lock_queue` and `unlock_queue`.
- **Audio.** A `LibretroBackend : AudioBackend` takes cellAudio's output
through the backend's write callback, resamples it to RetroArch's rate
(48 kHz; RPCS3's audio is 48 kHz already, so resampling is a copy until a
game asks otherwise), converts it to 16-bit stereo and queues it. `retro_run`
hands it over with the batch callback.
- **Input.** A libretro pad handler (a `PadHandlerBase`) maps the RetroPad to
a DualShock 3: buttons, both sticks and the analogue triggers. Pressure
sensitivity comes from the digital state. Keyboard, mouse, camera and music
handlers are the null ones.
- **Firmware.** The Qt application installs `PS3UPDAT.PUP` in
`main_window::HandlePupInstallation` (about 320 lines). The libretro layer
does the same work without Qt: `pup_object` (`Loader/PUP`), the update
packages' TAR extraction and the SPRX decryption. It runs on first boot when
`utils::get_firmware_version()` is empty and reports progress through the
RetroArch message interface.
- **Content.** The formats, in order:
1. A PSN package plus its RAP. This is the game at hand. The layer installs
it with `package_reader` (`Crypto/unpkg`), as
`main_window::HandlePackageInstallation` does (about 380 lines), and
copies the RAP to `dev_hdd0/home/<user>/exdata/`
(`main_window::InstallFileInExData`), then boots the installed
`dev_hdd0/game/<title>/USRDIR/EBOOT.BIN`. An already installed package is
not installed again.
2. A disc game's folder (`PS3_GAME/USRDIR/EBOOT.BIN`).
3. An ISO, which the pinned revision supports (`Loader/ISO.cpp`).
- **Where RPCS3 keeps its files.** RPCS3's emulator directory (`dev_flash`,
`dev_hdd0`, config, caches) is `/app0/system/RPCS3/`, next to the firmware.
Folders are 0777 and files at least 0666, so everything stays reachable over
FTP.
- **Core options and PS5 defaults.** Resolution scale 300%. Shader mode
asynchronous with the shader interpreter (`async_with_interpreter`, already
upstream's default), so no game waits for a shader compile or relies on a
warm cache. PPU and SPU decoders LLVM (upstream's defaults). Options are the
libretro v2 kind, written to RPCS3's configuration when content loads
(Phase 2 below).
- **Core info.** libretro-core-info has no RPCS3 file, so
`tooling/rpcs3/rpcs3_libretro.info` is in this repository (name, extensions
`pkg|iso|bin|elf|self|sfo`, firmware entry, GPLv2, `needs_fullpath`).
- **Reference ports.** Dolphin's `Source/Core/DolphinLibretro/` (the Vulkan
negotiation, its instance and device hooks and main-thread handling),
PPSSPP's libretro port and Azahar's.
### 3. Platform gaps
Each gap is either already measured on the console (`docs/PROBE.md`, the
platform layer's probes) or gets a probe step in the platform layer
(`src/probe.c`, host tests in `tests/test_platform.c`), run inside the title
with `/app0/platform-probe.txt`.
**(a) Code memory for LLVM and asmjit.** RPCS3 reserves code areas with
`utils::memory_reserve(size, can_be_jit=true)` and commits them
read-write-execute (`protection::wx`). Measured: direct memory mapped
read-write and then given execute runs, read-write-execute included, and a
protection change costs about 26 µs. `ps5_exec_allocate` has had no limit on
live blocks since SDK d3e8625: 2,000 4 KiB blocks and 300 64 KiB regions ran on
the console. LLVM uses the Small code model with PIC, so its code reaches
RPCS3's helpers through RuntimeDyld stubs and needs no placement near the
core. asmjit commits its 2 GiB area up front only under `CAN_OVERCOMMIT`
(Linux), which the PS5 branch leaves undefined, so it grows on demand.
*To measure:* a probe step that reserves a 2 GiB range, commits 64 KiB pieces
read-write-execute on demand, and runs code in each, with the time a commit
takes.
**(b) Address-space reservations.** RPCS3 reserves its guest memory in 4 GiB
steps from `0x3_0000_0000` upward (`Emu/Memory/vm.cpp`):
| Area | Size |
| --- | --- |
| `g_base_addr` + `g_sudo_addr` | 8 GiB |
| `g_exec_addr` | 12 GiB |
| `g_hook_addr` | 32 GiB |
| `g_stat_addr` | 4 GiB (`g_free_addr`, for SPU, starts right after it) |
That is 56 GiB, plus JIT areas (2 GiB for asmjit, 768 MiB for each LLVM
memory manager) and RSX's mappings. Measured (`docs/PROBE.md`): the GPU window
is `0x2_0000_0000`–`0x2_FFFF_FFFF`; from `0x4_0000_0000` at most 15 GiB; from
`0x10_0000_0000` to `0x80_0000_0000` 16 GiB is reserved at the hint itself;
nothing is granted at or above 1 TiB; and a hint whose free space is too small
is refused, not moved. The PS5 branch of `memory_reserve` therefore starts at
`0x10_0000_0000`, through `ps5_vrange_reserve`, which never draws on the
title's 448 MiB flexible pool, where an anonymous `mmap` reservation would.
*To measure:* one probe step reserving RPCS3's layout, 56 GiB in the sizes
above from `0x10_0000_0000`, to confirm 16 GiB is not a per-reservation cap
for 32 GiB.
**(c) Guest memory as views.** RPCS3's `utils::shm` creates a memory object
(`memfd_create`, or `shm_open(SHM_ANON)` on FreeBSD) and maps it at several
addresses: `g_base_addr` and `g_sudo_addr` see the same memory at different
protections. The PS5 branch uses `ps5platform/shm.h`: `ps5_shm_create` (one
direct-memory object), `ps5_shm_map` at fixed addresses inside a
`ps5_vrange_reserve` range, and `ps5_memfd_create` where RPCS3 wants a
descriptor. The platform tests already cover several views of one object.
*To measure:* 4 GiB of direct memory as two views, written through one and
read through the other, and the time a 64 KiB view takes to map.
**(d) 16 KiB host pages.** RPCS3 already handles hosts whose pages exceed
4 KiB (Apple Silicon): `vm.cpp` stops protecting 4 KiB guest blocks one by one
when `utils::get_page_size() > 4096` and maps them read-write, and every
`mprotect` is rounded to the host page. The RSX texture cache still protects
pages to catch CPU writes, now 16 KiB at a time, so a write near a cached
texture invalidates a wider range. *To measure:* what `sysconf(_SC_PAGESIZE)`
returns in the title (it must be 16384; the platform layer answers it
otherwise), and the texture cache's protection changes per frame in the game,
from RPCS3's own counters, at 26 µs each.
**(e) `thread_local` on emulated TLS.** The core loader has no PT_TLS support,
so a core's `thread_local` goes through `__emutls_get_address`. RPCS3 has 93
`thread_local` uses. The hot ones are `cpu_thread::g_tls_this_thread`,
`vm::g_tls_locked` and atomic.cpp's wait callbacks, all read on every guest
reservation and wait. *To measure:* the cost of an emulated-TLS read on the
console (a probe step timing `__emutls_get_address` over a million reads),
then RPCS3's rate of those reads in the game. If it matters, the fork keeps
those few variables in a per-thread structure the thread already carries.
**(f) Atomic waits.** On FreeBSD, RPCS3's `atomic.cpp` uses its `USE_STD`
path (`std::condition_variable`), and `Utilities/sync.h` emulates `futex()`
off Linux, so it needs only pthread mutexes and condition variables, which the
console exports. `_umtx_op` is exported too, if a faster path is wanted later.
Nothing to add.
**(g) Faults and `ucontext`.** RPCS3's access-violation handler
(`Utilities/Thread.cpp`) reads `uc_mcontext.mc_rip`, `mc_err` and the general
registers on FreeBSD. The SDK's `sys/_ucontext.h` corrects the console's
machine-context layout, and the platform probe already backpatches 200 fault
sites from a handler. RPCS3 also installs `SIGSEGV`, `SIGBUS`, `SIGILL` and
`SIGPIPE` handlers process-wide. In the title those would sit beside the
title's own crash reporter. *To measure:* a guest page fault handled and
resumed inside the core, with the title's reporter still catching a genuine
crash.
**(h) Threads.** RPCS3's `named_thread` calls `pthread_create`, which the
title binds to its factory (`src/core_threads_ps5.cpp`, 2 MiB minimum stack).
RPCS3 creates a PPU thread for each guest thread, six SPU threads, the RSX
thread and a pool of LLVM compile workers sized from
`sysconf(_SC_NPROCESSORS_ONLN)`. Measured by the thread probe (SDK 9a57ba7):
thirteen CPUs (0–12) are available to the title, and threads run at policy 1,
priority 700, in a range of 256 to 767. A priority set on a running thread
reads back. `pthread_getaffinity_np` with FreeBSD's `cpuset_t` returns ERANGE,
and RPCS3's `set_thread_affinity_mask` uses exactly that form. The platform
layer gets `ps5_pthread_setaffinity_np` and `ps5_pthread_getaffinity_np` (the
exported `scePthreadSetaffinity`/`scePthreadGetaffinity` with a 64-bit mask,
or the size the kernel accepts), bound through `tools/core-imports.py`.
`sysconf(_SC_NPROCESSORS_ONLN)` must answer 13. *To measure:* the affinity
calls round-tripping on the console, and what sysconf answers.
### 4. Risks
| Risk | How it is measured |
| --- | --- |
| LLVM does not build for the PS5 target, or its `Support` layer needs what the console lacks | the LLVM build (step 1) and the core's link and import report (step 3) |
| The core is too large for the loader (1 GiB image cap) | `check-core.py` and the loader's `mapped_bytes` (step 4) |
| Guest reservations do not fit below 1 TiB with the GPU window avoided | the reservation probe (gap b), then `Emu.Init` on the console |
| Emulated TLS costs too much in the hottest paths | the TLS probe (gap e), then the sampler in the game |
| RetroArch's Vulkan interface cannot give RPCS3 the device it needs (1.2 core, extensions) | device creation through `create_device2` (step 4), and RPCS3's own capability log |
| Queue sharing with RetroArch stalls the RSX thread | the sampler on the RSX thread; frame timing |
| Firmware install needs host facilities the title lacks | step 5's install run, file by file |
| Shader compiles cost speed | windows during the game's first minutes with an empty cache |
| The package install fills the console's storage | the install run's free-space check before it starts |
| 16 KiB pages widen the texture cache's invalidations | RPCS3's texture-cache counters in the game |
| The CPU cannot hold full speed (not part of the objective) | the 10 s windows, reported as they are |
### 5. The ladder
Each step ends with its evidence in `docs/ACTIVE.md` and a dated entry in
`docs/PHASE_LOG.md`.
1. **Fork and build scripts.** The fork at the pin; `tools/build-llvm.sh` (the
pinned static LLVM); `tools/build-ffmpeg.sh` (the pinned FFmpeg);
`tools/build-rpcs3.sh` on `tools/core-fork.sh`; `tooling/rpcs3/ps5-toolchain.cmake`;
`rpcs3` in `core_names`. *Evidence:* LLVM and FFmpeg archives with their sizes,
and `rpcs3_emu` compiling for the PS5 target.
2. **Host build (optional).** The same libretro core for the host, debugged in
a desktop RetroArch if this host's Vulkan driver allows. *Evidence:* the
host core reaching the firmware check.
3. **Link and ABI.** The PS5 core links and passes `tools/check-core.py`;
missing libc and kernel functions go into the platform layer with aliases
in `tools/core-imports.py`. *Evidence:* `check-core.py` PASS and the import
list.
4. **Load.** The core loads on the console and `retro_init` returns; the probe
steps for gaps (a)–(h) run inside the title. *Evidence:* the loader's
`ready` line, the probe's checks.
5. **Firmware, then a homebrew program.** `PS3UPDAT.PUP` installs from
`/app0/system/RPCS3/`, then a small PS3 homebrew or test program boots
before any commercial game. *Evidence:* the firmware version RPCS3 reports,
and the program's output.
6. **The game.** God of War HD installed from its package and RAP, booted at
100%, then at 300%: screenshots, the speed of each window, the Close
Content + reload battery and a ten-minute run. *Evidence:* the screenshot I
accept, the windows, the battery lines and the ten-minute trace.
## Phase 2: what the port turned out to need
Where the build and the first code differ from the audit above.
**Dependencies.**
- *LLVM* is my fork `../PS5_LLVM` (github.com/mihawk-99/PS5_LLVM), pinned in
`tools/build-llvm.sh`. The PS4/PS5 ABI ignores `alignas` on an empty base
class, and LLVM relies on it twice. `SmallVector<T, 0>`'s layout assertions
failed at build time; the fork aligns `SmallVector` itself under `__SCE__`.
`TrailingObjects` failed silently: its empty aligner base is what raises a
class to its trailing objects' alignment, so on the PS5 a header of an `int`
and some `bool`s stayed 4-aligned and 12 bytes, and two trailing pointers
were given 28 bytes while `getTrailingObjects` read them from 16 to 32. The
next allocation's first field overwrote the last pointer's upper half
(`MachineInstr::ExtraInfo`'s memory operands: God of War HD's SPU compiles
crashed in `MachineInstr::hasOrderedMemoryRef` on `0x2_xxxx_xxxx`, heap
pointers with their upper 32 bits replaced by the count 2). The fork gives
the aligner a zero-length member there, a non-empty base without storage;
the PS5 target then lays such a class out as the host does (8-aligned, 16
bytes, 32 allocated, trailing objects at 16). The other empty `alignas`
classes in LLVM and RPCS3 are standalone objects, whose alignment the ABI
keeps. The build uses host tablegen
tools (`LLVM_NATIVE_TOOL_DIR`) and builds only the libraries
(`LLVM_INCLUDE_TOOLS=OFF`), since no LLVM program links for the console.
- *FFmpeg* 8.1.1 comes from its signed release tarball (`tools/build-ffmpeg.sh`),
with RPCS3's own component list and NASM for its x86 assembly.
- *GNU libiconv* 1.18, static, comes from its signed release tarball
(`tools/build-libiconv.sh`). `cellL10n` converts the PS3's character sets
with iconv, and the console's libc has none.
- *OpenAL Soft* is built rather than compiled out: `cellMic` includes its
headers outside `WITHOUT_OPENAL` off Android. Every backend is off, so it
opens no device. Its sources are C++20 modules; CMake's dependency scan runs
through `tooling/rpcs3/clang-scan-deps`, which gives the scanner the SDK's
target and headers.
- *zlib* is the frontend's pinned build. libpng's generated configuration does
not see the submodule's headers.
- *pkg-config* answers nothing in the toolchain file
(`tooling/rpcs3/ps5-toolchain.cmake`). The host's packages would add the
host's `/usr/include`.
- The network clients (RPCN, clans, UPnP) still build, with wolfSSL and curl.
They are not stubbed yet; the title has no network use for them.
- The target defines `__FreeBSD__` as 9, so RPCS3's FreeBSD branches take their
pre-13 paths (`shm_open(SHM_ANON)` for `memfd_create`). The PS5 branch below
replaces them where memory is concerned.
**Vulkan** (`rpcs3/Emu/RSX/VK/vkutils/swapchain_libretro.*` in the fork).
RetroArch creates the instance with the core version RPCS3 asks for (1.2), and
the device through `create_device2`. The core runs RPCS3's own
`render_device::create` there, so the device has RPCS3's features and
extensions plus RetroArch's; when the renderer starts, `render_device::create`
runs again and adopts that device instead of creating one. Features RPCS3
enables only for some settings are the same whatever the game's configuration,
so the device a boot needs is the one RetroArch made. Every submission and
device-wide wait takes RetroArch's queue lock inside RPCS3's own submit lock.
The swapchain is the core's own images, sampled by RetroArch:
- acquiring an image signals RPCS3's semaphore with an empty submission;
- presenting waits on RPCS3's semaphore and signals a fence;
- `retro_run` hands RetroArch the newest finished image with `set_image`, after
its fence;
- an image RetroArch showed is reused only after RetroArch's frame index that
last read it comes around again, which is when RetroArch has waited for that
frame.
The images are not RPCS3's allocations, so they outlive the renderer while
RetroArch may still show one; they go when a newer frame replaces them or the
context is destroyed. The game boots in the first `retro_run` after RetroArch's
context is up, because RPCS3 builds its renderer while booting.
**Input** (`rpcs3/libretro/libretro_pad_handler.*`). RetroPads 1 to 4 are the
PS3's controllers, through a pad handler on RPCS3's generic machinery (SDL's
value conventions). The core compiles `rpcs3/Input/pad_thread.cpp` and what it
needs, without the HID handlers (no hidapi on the console), and writes the
input configuration that names the RetroPads before the pad thread loads it.
**Memory** (the `__PROSPERO__` branch of `rpcs3/util/vm_native.cpp`). RPCS3's
reservations, commits and shared memory go through the platform layer:
- reservations are `ps5_vrange_reserve` and `ps5_vrange_reserve_at`;
- commits and decommits are `ps5_vrange_commit` and `ps5_vrange_decommit`,
direct memory in 64 KiB units (SDK 7f3d3ff);
- `utils::shm` is a `ps5_shm` direct-memory object with its views.
Guest memory starts at 64 GiB (`0x10_0000_0000`). The 32 GiB hook area is only
reserved: nothing reads it, and a 32 GiB copy-on-write view of zeros has no
direct-memory form. The page size is the kernel's 16 KiB. The core's
reservations are released, with the memory committed in them, when the core is
unloaded, so the next load finds its addresses free.
**Faults.** In the libretro core, a fault RPCS3 does not handle goes back to the
handler it replaced (the title's crash reporter), and the title's handlers
return when the core is unloaded.
**Platform gaps closed in the SDK (8160289).** Each is aliased in
`tools/core-imports.py`.
- No export at all: `accept4`, `getpagesizes`, `in6addr_any`.
- Exported, but resolved to nothing in a title: `gai_strerror`, `statfs`,
`fstatfs`, `umask`, `fork`, `setsid`, `wait4`.
- Formerly taken from the SDK's static payload libc, whose system calls a title
may not make: `syscall` (only `SYS_write`), `getpwnam_r`, `posix_madvise`,
`strsignal`.
- Answered differently on the console: `pthread_getaffinity_np` and
`pthread_setaffinity_np` (the 64-bit mask), `sysconf` (13 CPUs, not 16),
`pthread_exit` (a thread's `thread_local` destructors first).
- Refused: `realpath` answers EPERM, even for `/app0`, so libc++'s
`std::filesystem::canonical` and `weakly_canonical` came back empty and the
package installer could not normalise its path. `ps5_realpath` makes the path
absolute, resolves `.` and `..` by name and checks each component with
`stat`; it keeps a symbolic link's name. The title links it too
(`--wrap=realpath`).
- Memory: committed ranges (`ps5_vrange_commit`), exact reservations, zeroing
of reused direct memory, and placement up to 1 TiB.
The title's core loader answers a core's `dlopen(NULL)` with the process handle
(`src/core_loader_ps5.cpp`), which LLVM's JIT needs.
**What the console run needed in the fork** is recorded in docs/PHASE_LOG.md
(2026-09-28): RSX's start order, VMA's entry points under volk, the kqueue
trigger RSX audio's timer uses, releasing LLVM's memory-manager ranges, the
log listeners' lifetime, and quitting through `Emulator::Quit`.
**Content loading and the loading screen** (`rpcs3/libretro/libretro_loader.*`
in the fork). `retro_load_game` returns at once: the firmware and a PSN package
install on a worker thread while `retro_run` shows a loading screen, and the
game boots when they are done. The screen is drawn on the CPU (the stb
libraries RPCS3 already has) and handed to RetroArch as a Vulkan image, one
per RetroArch frame index, with the copy submitted through
`set_command_buffers`:
- a violet night behind the game's own art (its PIC1, blurred and tinted),
its ICON0 on a glass card, the title in Inter, and a violet-to-orchid bar
with a sweeping highlight;
- "Installing firmware" with its packages, "Installing" with the bytes, the
speed, the time left and the percentage, then "Starting" with RPCS3's
compile progress, until RPCS3's first frame replaces it;
- what failed, in rose, when something does.
Inter 4.1 (SIL Open Font License, its licence beside it) is fetched by
`tools/build-rpcs3.sh`, pinned by digest, and staged in `system/RPCS3/fonts`;
the firmware's Rodin is the fallback. A finished package install is recorded
in `system/RPCS3/libretro/installed/<title ID>`, since an interrupted one leaves
an `EBOOT.BIN` behind, and the record goes when an install starts. Closing
content during an install stops it, inside a large file too (the fork's
`unpkg.cpp`); a partial firmware is installed again next time.
**RPCS3's folder, as staged.** Besides the loading screen's fonts, the title
stages RPCS3's own overlay images (`bin/Icons/ui` into `system/RPCS3/Icons/ui`),
which its native dialogs draw. The configuration the core writes at load also
turns RPCS3's GDB server off: no debugger connects to a core, and it would only
open a socket on the console. Open question: the server's address did not
match RPCS3's IPv4 `std::regex` on the console (it fell through to the Unix
socket branch), which may mean `std::regex` misbehaves there; RPCS3 also parses
patches and its game database with it, so it wants a probe.
**What loads.** A PSN package (`.pkg`, installed once, then booted at
once), a game's `EBOOT.BIN`, an ISO, or a folder (RetroArch's "Use this
directory"): the folder's `USRDIR/EBOOT.BIN` (an installed game, such as
`system/RPCS3/dev_hdd0/game/NPUA80490`) or `PS3_GAME/USRDIR/EBOOT.BIN` (a
disc game) boots. RPCS3 itself treats a bare folder as "build the PPU cache of
everything in it", which compiled and stopped. Loading a folder also first
crashed the title: canonicalising a relative path asks libc++ for the working
directory, and the title's own `getcwd` resolves to nothing on the console
(libc's calls `__getcwd`, which only libkernel_sys has). The title links it to
the platform's `ps5_getcwd` (`--wrap=getcwd`), as the cores already were, and
`tooling/native/app-symbols.map` keeps every `__wrap_` symbol local, since lld
exported `__wrap_getcwd` to interpose on libc's stubs and a title publishes
no exports.
**Core options** (`rpcs3/libretro/libretro_options.*` in the fork). Every RPCS3
setting with a set of values is a libretro v2 core option, one category per
section of RPCS3's configuration: CPU (Core), Virtual File System, GPU
(Video, with its Vulkan, performance overlay and shader loading dialog
subsections), Audio, Input, System, Network, Save States and Miscellaneous.
They are built from the configuration tree itself, so their names, values
(an enum's names; a number's range as a list: all of a small range, else the
1-2-5 series, even steps, the bounds and the default), defaults and whether
RPCS3 takes a change while a game runs are RPCS3's own. A change RPCS3 takes
live applies at once; the rest are written to `config.yml` when content loads,
as RPCS3's settings dialog writes them, so a game's custom configuration still
overrides them. Left out: what the core fixes itself (the Vulkan renderer,
RetroArch's audio backend, the RetroPads in place of keyboards, mice, cameras
and Move), the Qt application's window behaviour (auto-start and auto-exit,
fullscreen, focus pause, native UI, GameMode), settings for other hosts (Metal),
VSync (RetroArch paces frames) and RPCS3's savestate folder limits. Paths and
free-text settings have no set of values to offer. The resolution scale keeps
100% to 300% (the frame is 720p at the scale, 2160p at most), 300% by default.
**Game patches** (`rpcs3/libretro/libretro_patches.*`). RPCS3 has no
frame-rate unlock of its own: a game runs faster than it was made to only
through the patches in RPCS3's game patch database. The fork keeps that
database as RPCS3's patch manager downloads it (`bin/patches/patch.yml`, its
source, date and hash in `bin/patches/SOURCE`; 815 executables, 502 of them
with a frame-rate patch on 2026-09-29), and the title stages it where RPCS3
reads it (`system/RPCS3/patches/`). The "Frame-rate patches" core option, on by
default, turns each game's frame-rate patch on when content loads: "60 FPS"
where the database has one, else "Unlock FPS", which RPCS3's frame limit holds
at the display's rate; one per executable, serial and version, and another
rate (30, 50, 120), a fallback variant or a "No User Input" one stays off.
RPCS3's `patch_config.yml` is written only when that changes, and keeps the
state of every other patch. A patch for a game the database lacks goes in
`patches/<serial>_patch.yml`, which RPCS3 reads for that game.
**Save states** (`rpcs3/libretro/libretro_state.*`). RetroArch's save states
are RPCS3 savestates. RPCS3 writes one to a file system in memory, a virtual
device like its ISO one (`Emulator::savestate_path_override` points the
savestate there), and the game boots again from it, as RPCS3's own "save state
and continue" does; loading a state stops the game and boots the state in its
place. Nothing goes through the console's storage but RetroArch's own state
file. RetroArch asks for a state's size right before it serializes one, and
before it loads one (to back up what runs for "Undo Load State"), so the core
makes the state at the size query and keeps it until the next frame: one
request costs one savestate. RPCS3 refuses a savestate while an SPU cannot be
stopped (its "Compatible Savestate Mode" option), a video decoder context
exists (LLE libvdec avoids it) or the game is saving; the game then runs on
and RetroArch reports the failure. The core declares its states variable in
size and platform-dependent, and its `.info` offers basic save states only (no
rewind or run-ahead, which RPCS3 savestates cannot serve). The pad script's
`SAVE_STATE` and `LOAD_STATE` actions save and load the current slot, so a
test run can start from a known scene (and `SCREENSHOT` keeps the frame shown
then as `/app0/pad-shot-<n>.png`, so one run shows each screen it walks
through); RetroArch names the state after the
content, so a game loaded as its folder (`dev_hdd0/game/GTA4`) has
`GTA4.state`, where an `EBOOT.BIN` gives every game `EBOOT.state`.
**A title's write budget.** The console, not its NVMe drive, sets how fast a
title writes: about 1.3 GiB at 242 MiB/s, then about 2 MiB/s, whether through
`write()`, `O_DIRECT` or a shared mapping and with however many threads, and it
refills while the title does not write. Its FTP server, another process, is
held back less: about 7 MiB/s a connection and 22 MiB/s in all (the SDK's
docs/PROBE.md, "Sustained writes" and "Writing through another process"). So
RPCS3's files in the title's folder go through that server once the title's
burst is spent (the fork's `Utilities/File.cpp`, on the SDK's
`include/ps5platform/offload.h`): a file opened for writing in `/app0` times
its writes, and once its first MiB is written and a MiB of the title's writes
ran slower than 50 MiB/s, a write at the file's end starts a stream that
carries the file's following writes through a loopback connection to the
server; anything else done with the file (reading, seeking, its size,
closing) first waits for the server to confirm the stream's bytes. The stream
keeps what the server has not confirmed (16 MiB at most) and writes it itself
if the server fails, so nothing is lost; with no server it never starts. The
server is found by its greeting and the folder it sees `/app0` as by a marker
file, and the answer is kept in `/app0/.ps5-offload`. GTA IV reinstalling
1414 MiB of its install with the budget spent took 4 minutes, about 6 MiB/s,
where its first install ran at 0.7 MiB/s once the budget was spent, and the
nine files it wrote were byte for byte the ones it had written before
(2026-09-29). Several files streamed at once would reach the server's 22 MiB/s;
RPCS3 writes one at a time. Installing a package's large files without copying
them, read and decrypted from the package the way RPCS3 reads an ISO through a
virtual device, would remove the wait and the second copy.
## GTA IV (BLES00229 1.00): the test game, toward 4K at 60 fps
GTA IV is the RPCS3 core's test game from 2026-09-29; the target is 4K at a
stable 60 fps, measured from a save state in its first playable scene (Roman's
taxi at the docks, the first mission). What it took, all in the fork:
- **A FIFO in local memory** (`RSX: a FIFO whose command buffer is in local
memory`). 1.00's libgcm allocates its context with system mode 0x210 and
writes its first commands at local-memory offset 0x1000 with no IO memory
mapped; RPCS3 read GET and PUT as IO offsets and stopped with "Dead FIFO
commands queue state" (RPCS3 issue #14194, where 1.00 never boots). Mode 0x200
now makes GET, PUT and the jump and call targets local-memory offsets.
- **The ZCULL late fault** (`ZCULL: a late fault on a report page unlocks it`),
RPCS3 issue #19610: on 16K-page hosts the RSX thread's semaphore labels share
a page with the reports, and a declined late fault spun forever.
- **Its folder** (`Disc games in dev_hdd0/game mount their folder whatever its
name`, and `A disc game in a dev_hdd0 folder resumes from a savestate in that
folder`): GTA IV is kept as `dev_hdd0/game/GTA4`, which the boot took as a
9-character title ID, and a savestate's boot path carries a doubled separator.
- **Save states** (`vm: memory restored from a savestate takes its block's page
size`): loading a state over a game resumed from one stopped on "Memory
inconsistency found" at the stack block. A save costs about 20 s of the game,
and so does a load: RPCS3 resumes by booting the state.
- **Its frame rate** (`libretro: GTA IV 1.00's frame-rate patch`): RPCS3's
patch database unlocks 1.08 only. 1.00's render thread holds frames to the
frame lock times 1/60 s, polling the timebase with 30 us sleeps (1.16 million
of them in 15 s of gameplay), and skips that when the frame lock is 0; the
fork's `bin/patches/BLES00229_patch.yml` sets it (0x0113b9f4) to 0, found in
the game's code (a dump of the running executable, the `ppu-dump.txt` test
hook). A higher vblank rate (120) left it at 30: it counts time, not vblanks.
The game installs 3.3 GB of its disc to `dev_hdd0/game/BLES00229` on its first
start; see "A title's write budget" above.
Measured from the baseline state (70 s runs, `--appendconfig` test config,
2026-09-29):
| Build | Resolution | Frames a second (10 s windows) |
|---|---|---|
| no frame-rate patch | 300% (2160p) | 27.9-30.0: the game's own 30 |
| no patch, Vblank Rate 120 | 300% | 27.8-30.0 |
| frame-rate patch | 300% | 50.4-51.5, loading screens 59.8 |
| frame-rate patch | 100% (720p) | 53.4-54.8 |
The game keeps its speed (the audio windows are full), and the resolution
costs little: the limit is the emulation, not the PS5's GPU. RPCS3's
performance overlay puts the guest RSX at 98% busy, while its host CPU
figures read nearly nothing on the console (5.5% PPU, 1.8% SPU: the thread
times do not work there). The title's sampler, every thread sampled (its walk
now bounded by each thread's stack), found one thread busy 85-89% of the time
in recompiled code, most likely an SPU running GTA IV's SPURS work, and five
more at 30-55% with `vm::writer_lock` their most frequent address, while the
RSX thread ran about half the time. With RPCS3's accurate SPU reservations
(the default, which stays), every SPU atomic write takes that lock, and a
PS5's Zen 2 has no transactional memory to take it faster. That lock and the
busiest SPU are where 60 fps is to be found next.
-339
View File
@@ -1,339 +0,0 @@
# Testing and evidence
How this project decides that something is true, and what it keeps as proof.
`AGENTS.md` lists the gate commands; this file says what they mean, how to add to
them, and what counts as evidence.
## The four layers
| Layer | Runs | Answers | Must be |
| --- | --- | --- | --- |
| Format and static | every save | is it shaped right? | fast, no network |
| Unit | every commit | is the logic right in isolation? | deterministic, milliseconds |
| Integration | every commit | do the parts work together? | deterministic, seconds, no console |
| Target | every step | does it work where it ships? | scripted, repeatable, captured |
The first three run on the development machine and are the agent's own feedback
loop. The fourth runs on the console and is the only layer that can close a
step. A step is never closed by a mock of the console.
## What each gate runs
| Gate | Command | What it proves |
| --- | --- | --- |
| format | `tools/verify.sh format` | `tools/lint-shell.sh` parses every script in `tools/` (`bash -n`) and every python tool (`ast.parse`), rejects CRLF and byte-order marks, and refuses a committed console address or credential; `tools/lint-format.sh` then runs `clang-format --dry-run --Werror` over `src/`, `tests/` and `tooling/native/` against the repository's `.clang-format`. Formatters are not installed for shell or python on this machine, so this gate is syntax and policy rather than style. |
| unit | `tools/verify.sh unit` | `make test-unit` runs `tests/test_frontend.py`. Four kinds of check, none of which needs a console: the built `gfx_video_driver.c.o` really lists `video_ps5` before `video_null` in `video_drivers[]` (read from its relocations); the title and the frontend agree on `video_driver_t`, by compiling RetroArch's header with the frontend's own defines from `tools/retroarch-flags.sh` and comparing `sizeof` and the `poke_interface` offset against the object the title links - the fault that kept the menu off the screen, see `docs/FINDINGS.md`; the frame-layout function from `src/display.cpp` is compiled from the source by the host compiler, compared against `../PS5_Vulkan`'s own addressing over all 2,073,600 pixels, and pinned against a table of values so a change to it cannot be silent; and the built `dist/<TITLE_ID>/` is a fake self wrapping a 64-bit x86-64 ELF whose `param.json` names its own folder. |
| build | `tools/verify.sh build` | `bash tools/build-title.sh` fetches verified core inputs on first use, compiles RetroArch's sources with the cross toolchain, archives them, compiles `src/` with the same `-D` flags the archive was compiled with, links it with the pipeline's CRT, signs the result and records the folder's manifest. It is not bare `make app`: the things the link needs, and the defines the two sides must share, are set by that script and by nothing else. |
| integration | `tools/verify.sh integration` | `make test-integration` (the same tests as `unit`, from the other side of the build) followed by `tools/check-manifest.sh` on the built tree: every recorded file present and unchanged, nothing extra, `eboot.bin` a fake self, and `param.json` naming the folder it sits in. |
| evidence | `tools/verify.sh evidence` | `tools/evidence.py compare evidence/` replays every committed capture against its expectation and exits non-zero on a difference. It never contacts the console. |
`tools/verify.sh` runs them in that order and stops at the first failure. The
order is deliberate: a formatting failure is cheaper to fix than a staged
artifact that cannot load.
## Evidence
Evidence is a committed artifact plus the command that reproduces it. The shape
this project uses: **capture on the console, replay on the development machine,
compare the two.**
- **Capture.** A console run is captured by the target-layer tooling — the run's
own stdout through the SDK's stdio, plus the console's kernel log for the
window of the run. Raw captures are large and console-specific, so they stay in
the git-ignored `klog/`; the distilled artifact is committed under
`evidence/<step>/`.
- **Distil.** `tools/evidence.py distil` turns a raw capture into a small,
machine-readable record: the ordered identity and event lines, a frame digest,
a present count, and the driver and core names, with timestamps and process ids
replaced by placeholders. A distilled record must let a stranger tell pass from
fail by reading it.
- **Replay.** `tools/evidence.py compare` re-runs the deterministic part on this
host — the same argument builders, the same config round-trip, the same digest
computation — and compares its output with the committed record.
- **Compare.** A comparison is exact unless the record declares a tolerance, and
a tolerance states its unit, its value and what makes it necessary, in
`docs/FINDINGS.md`. A frame digest is compared exactly; a timing line is
compared only when the record says which fields are bounded.
An artifact is only evidence if a stranger can tell pass from fail by reading it.
A screenshot, a green check with no input recorded, or a log line that says "ok"
is not evidence.
## Adding a test
- A bug fix lands with the test that fails without the fix. Write the test
first, watch it fail, then fix — and say in the commit that it was seen
failing.
- A new test goes next to the code it covers, follows the naming of its
neighbours, and is added to the gate that runs it.
- A test that needs the console is split: the deterministic part runs in the
integration gate, the console part becomes a captured artifact.
- Never weaken, skip or special-case a test to make a gate pass. If the test is
wrong, fix the test in its own commit with the reason.
## Determinism
Unit and integration tests are deterministic: no wall-clock reads, no sleeps to
synchronise, no unseeded randomness, no network, no console, and no dependence on
the order tests run in. Time, randomness and I/O enter through an injectable seam
so the test can pin them. A test that cannot be made deterministic is a
target-layer test, not a unit test.
A capture is the one artifact that comes from an uncontrolled environment, so its
distillation removes everything that is not a decision: timestamps, pids, buffer
addresses, and the console's own boot noise.
## Tolerance and flakiness
- A comparison with a tolerance states the tolerance, its unit, and what makes
it necessary.
- A flaky test is a bug in the test or in the code, and it is named in
`docs/ACTIVE.md` under Open findings until it is fixed. It is never dealt with
by retrying until green.
- A known-benign console message (a service warning, a busy-device notice) is
recorded once in `docs/TROUBLESHOOTING.md` with its exact text, and every later
run may say "known benign" only if the text matches.
## Native audio backend
The unit gate compiles the real `src/audio_ps5.cpp` against an explicitly clocked
AudioOut mock (`tests/audio_ps5_test.cpp`). It checks native arguments, 48 kHz rate
negotiation, byte counts, FIFO order across wrap, full/partial nonblocking writes,
blocking backpressure, zero-filled tails, pause/resume and output-error wakeup.
Condition barriers coordinate the test; timeouts only bound a deadlock failure.
`tools/run-title.sh --no-build --audio-test --watch 45` arms a consumed
`/app0/audio-test.txt` file. Before the frontend starts, the same `audio_ps5`
callbacks play four one-second PCM tones: left 440 Hz, right 660 Hz, repeated, at
12.5% peak with 10 ms boundary ramps. Announce the tones before launch and obtain
an audible/channel-order confirmation from the console owner. The test uses
1/255/257/1000-frame writes, drains and pauses/resumes between phases, and then
checks that an oversized nonblocking write accepts exactly one queue capacity.
It closes its port before RetroArch opens its own normal audio driver.
The runner retrieves `audio-test.json` into the ignored capture directory and
rejects failure, stale build identity, wrong format, lost frames, native errors
or queue-count mismatches. Expected: 48 kHz, 256-frame grains, four bytes/frame,
1,536-frame queue, 193,536 accepted and played frames, zero errors, 6,144 bytes
accepted from the oversized nonblocking write. The report cannot establish
speaker audibility; that confirmation is recorded separately in the evidence.
Normal launches remove leftover control files and never generate test tones.
## Platform paths and directory browsing
`tests/test_platform_paths.py` runs the actual native platform setup against an
isolated host filesystem, checking first-install config seeding, preservation
across a changed seed, startup argument preservation and accessible browser roots.
The real SDK directory adapter receives synthetic FreeBSD records to test multiple
batches, directory/file names and types, deleted records, EOF, denied opens,
valid fd zero, truncated/oversized records and missing string terminators. The
mounted-directory fixture rejects small reads; the previous 4 KiB variant fails
and the 64 KiB adapter passes.
Application-path tests compile the patched upstream function with procfs-related
calls replaced by aborts, checking the known executable path and small buffers.
On the console, retain the current build identity, platform directory summaries,
configuration load/save logs, GPU API results, and the owner's browser observation.
Read back the live config over FTP and verify its selected paths/settings. Launch
again without overwriting it to prove parsing/persistence across runs. No core or
ROM execution is implied by seeing directory entries. Raw filenames, settings,
crash dumps and console information stay in ignored captures; commit sanitized
counts, known port paths, crash symbols and acceptance results only.
The platform fixture also creates existing `0755` cores and `0775` content
directories and uses umask `0077`; every managed directory must become `0777` after initialization.
This test fails against the former mkdir-only implementation. After console
startup, `python3 tools/check-ftp-write.py` requires both mode `0777` and successful
FTP upload/readback/cleanup in all seven managed directories. Mode listings alone
are insufficient evidence that the FTP process can write.
The native input fixture compiles `src/input_ps5.cpp` against mocked pad services
and the actual upstream `input_state_wrap` and analog helpers. It checks raw button
capture, all stick directions, trigger scaling, invalid ports/axes/buttons,
user binding priority without the old direct mapping, menu deadzone behavior,
zero-sample repeat polling, interception/disconnect and driver teardown/reinit.
ELF relocations verify joypad registration and the built-in profile in the shipped
frontend. On console, confirm left-stick menu navigation, button/axis capture in
Settings > Input > RetroPad Binds > Port 1 Controls, and saved bindings after a
restart. Preserve the owner's live configuration when deploying.
## Libretro core artifacts
`tools/check-core.py <core.so> --report <report.json>` requires an ELF64 x86-64
FreeBSD/PS5 shared object, 16 KiB-compatible load segments, native runtime imports
including `libkernel_web.sprx`, and all 25 libretro callbacks in the dynamic symbol
table. It rejects Linux ELF ABI, payload kernel imports and absent callbacks.
The report deliberately keeps `console_loading_verified` false: ELF structure
cannot establish runtime loading. The title manifest check also checks the
staged FCEUmm artifact and requires identical `.info` metadata in `info/` and
`cores/`, covering both explicit and legacy empty core-info paths.
`tests/test_core_abi.py` builds real host fixture ELFs, marking only test fixtures
with the PS5 OSABI. Positive and negative cases exercise the parser through
readelf: missing callback, wrong runtime library, wrong ABI, 4 KiB segments and
truncated header. These fixtures are never staged or run on the console.
For the target test, upload a verified build, open RetroArch normally, and let
the owner select FCEUmm under Load Core and then a game under Load Content.
Collect `retroarch.log`, trace and kernel log with the build identity. Record
core load, content initialization and presented frames separately; a visible
core entry or `.info` name is not proof of loaded executable code. Owner game
paths remain in ignored logs. Keep normal XMB, Vulkan and audio startup working
when no core is selected.
## Native core loader diagnostics
`tests/test_core_loader.py` compiles and executes the real loader against a small
host ELF fixture. It tests relocation/import execution, reference counts,
reloads, once-per-mapping initializers, invalid initializer tables/callbacks,
missing imports, malformed header/table bounds, forbidden segment flags,
TLS, and invalid relocation targets/types. The fixture's OSABI byte is adjusted
only for this host test; shipped cores must pass the actual cross-build checker.
`tests/test_core_recovery.py` runs the upstream core-selection function before
and after the patch and checks rejection without marking the core as selected.
`tools/run-title.sh --no-build --core-test --watch 30` arms an opt-in no-game
console diagnostic. It executes eight FCEUmm load/symbol/API/identity/unload
cycles, rejects a missing core/export, and loads the core again with the frontend
resident. It then attempts missing-core selection/content startup through the
actual task functions and checks that the initialized menu context survives.
The runner collects `core-loader-test.json` and `core-recovery-test.json`, checks
their build identity and pass status, captures logs, and closes the title. Normal
runs clear the control file; diagnostics never run without the flag.
These diagnostics do not establish gameplay. The owner selects the core and
content manually in a separate captured run and confirms video, audio, controls
and return to XMB. Keep user content paths in ignored captures, not evidence.
`tests/test_core_frame.py` executes the actual XRGB8888 upload conversion with
red/green/blue and mixed colours, checks opaque alpha, distinct input/output row
pitches, untouched padding and in-place conversion. Real-core acceptance also
requires a trace with no Vulkan refusals or failed command buffers, rather than
inferring correctness from the core's successful initialization.
For mGBA use `tools/run-title.sh --no-build --core-test=mgba --watch 180`.
`--core-test` without a value retains the FCEUmm diagnostic. Reports contain the
selected core name, checked along with build identity and pass status. Only these
two names are accepted. The owner selects content manually after startup.
`tests/test_core_imports.py` checks union/deduplication across two cores, object
and untyped imports, directory/time adapters, and conflicting or TLS import
rejection. The directory adapter fixture also checks rewind after EOF, rewind of
a partially consumed batch and failed seek without discarding state. The loader
constructor test was observed failing before support was added, then passing;
it also rejects callbacks into writable data without executing them.
Record GB, GBC and GBA gameplay separately when tested. Core compilation and
metadata support for a system do not substitute for console gameplay. RTC,
optional BIOS files, saves/states, sensors, rumble and sustained performance
require their own acceptance; do not infer them from one successful game.
The native memory test covers 16/32 MiB allocation, zeroing, preserved data on
resize, overflow/failure keeping the original buffer, mapped-to-small resize,
foreign libc ownership and concurrent allocation/free. Target acceptance must
also load a GBA archive larger than the native heap could previously allocate;
a host allocation test alone does not prove native content loading.
For core lifecycle acceptance, test coloured content, open Quick Menu with the
core paused, close content to XMB, reload another game, and choose Quit. Inspect
both the live frontend log and kernel capture. Native exit(0) followed by SIGSYS
is a shutdown failure even when the user intentionally chose Quit. Check core
XBGR-to-XRGB conversion and repeated immutable cached-frame uploads on the host;
console colour/flicker confirmation remains required.
For patch 0078, manually exercise game -> Quick Menu -> Close Content -> another
core/game, repeating with FCEUmm and mGBA. Check menu and gameplay for flicker
and stale image regions at each transition, including the RGUI configuration
in the owner's reproduction. Owner observation is required: a recording at a
matching refresh cadence may hide alternating corrupted frames. Preserve the
live frontend log before a relaunch replaces it; do not accept a clean loader
probe alone as visual acceptance. Record hashes of linked driver archives when
the sibling Vulkan repository is being developed concurrently.
### Snes9x
`tests/test_snes9x_video.py` exhaustively checks 65,536 RGB565 values through the
actual core conversion and frontend GPU upload helpers. It also covers padded
input rows, source immutability, repeated cached frames, common/hires/NTSC/4x
sizes, and rejection without writing on bad dimensions/pitch/capacity.
`test_core_loader.py` loads a C++ ELF with this core-local destructor registry:
construct once, defer destruction while another handle exists, destroy in reverse
order on the last close, reload repeatedly, and reject malformed finalizer tables
or non-executable callbacks before any constructor. The host fixture disables
GNU ld's EH frame header because this PS5 layout is not a host unwind layout;
exceptions/unwind registration are outside the loader contract.
Console acceptance uses `tools/run-title.sh --no-build --core-test=snes9x
--watch 180`: eight loader cycles and live-menu recovery, then owner-loaded SNES
archive with correct colours, sound and controls. Open Quick Menu, Close Content
and load another game/core to check persistent colour/flicker regressions. Owner
visual confirmation and matching build identity are required. Core-declared FPS
and audio rate are metadata, not performance measurements. NTSC filters, special
chips, subsystem BIOSes, interlace/hires modes, SRAM/state round trips and long-run
A/V timing require separate coverage beyond an ordinary game acceptance run.
### FBNeo
`tests/test_fbneo_video.py` checks every RGB565 value through the actual native
conversion and frontend upload helpers, immutable cached input, padded rows,
bounds rejection and horizontal/vertical arcade dimensions. The pinned-source
`test_fbneo_upstream.py` checks all 16,777,216 32-bit colours using upstream's
actual HighCol32/HighCol16 functions and the frontend upload helper. It also
compares the original MPEG decoder against the patched decoder at all 4,097
bit limits of each synthetic layer 2 and AMM frame, with address/undefined-behaviour
sanitizers. LeakSanitizer is disabled because the host sandbox cannot provide
its process attachment; bounds and UB checks remain enabled. This source-based
test requires the hash-verified archive downloaded by `make fbneo`; absence is
reported as a skip and cannot establish FBNeo acceptance. The patched metadata
function is also queried 10,000 times under these sanitizers, checking its
name/version, full-path and no-extraction flags, and archive extensions.
After all host gates pass, use `tools/run-title.sh --no-build --core-test=fbneo
--watch 240` for eight load/unload cycles, failed-load/menu recovery and manual
arcade archive loading. Capture frontend/trace/kernel logs and both diagnostic
reports, verify identity, and obtain owner confirmation of gameplay colours,
audio/input, Quick Menu, Close Content and the next loaded game's colours.
Record whether the observed game exercised native 32-bit or converted 16-bit
output. Do not infer both paths or all supported boards from one game. ROM-set
errors, BIOS errors, CHD, samples, rotation, save states and long-run performance
need their own evidence when exercised. Private game names stay in ignored logs.
The FBNeo pinned-source test also exercises the patched catalogue with 30,000
drivers under a simulated 1 MiB small-allocation budget: two bulk allocations,
each allocation failing independently, repeated initialize/exit, double exit,
original pointer restoration and oversized-name rejection. ASan/UBSan runs cover
both narrow and Unicode variants. The shipped console build remains narrow.
### Genesis Plus GX
`tests/test_genesis_plus_gx_video.py` compiles the C output adapter and the actual
frontend RGBA upload helper. It tests all 65,536 RGB565 values packed by the
hash-verified upstream renderer macro; cropped views, 720-pixel input rows,
resolution changes up to 720x576, repeated cached frames, source immutability,
output canaries and invalid viewport rejection. Fetch the pinned source with
`make genesis-plus-gx` first; a missing archive produces an explicit skip, which
cannot establish acceptance. Host geometry coverage does not prove a particular
core option or system on the console.
After all gates pass, `tools/run-title.sh --no-build
--core-test=genesis_plus_gx --watch 240` checks eight load/export/API/name/unload
cycles and failed-load/menu recovery. The owner then loads content manually and
checks colours, audio/input, Quick Menu, Close Content and another game/core.
Capture frontend/trace/kernel logs, match the exact build identity, and check for
Vulkan refusals, GPU API failures, invalid bitmap views and unexpected runtime
errors before committing. Record systems actually tested and preserve user
confirmation separately from machine-readable loader and runtime evidence.
## XMB allocation diagnostics
The opt-in build, host allocator/Vulkan-dispatch tests, raw log fields, ownership
coverage limits and manual reproduction protocol are specified in
[MEMORY_DIAGNOSTICS.md](MEMORY_DIAGNOSTICS.md). Its build inspection is host-only;
it must not be cited as a passing console reproduction or a fixed crash.
-79
View File
@@ -1,79 +0,0 @@
# Thumbnail colours and configuration reset
The owner reported swapped save-state thumbnail colours and loss of input after
Reset to Defaults, then confirmed the uploaded fixes with “Perfect. Commit”.
Build, deployment and acceptance evidence: `evidence/thumbnail-input-defaults/`.
Uploads are authorized; do not launch or interrupt a title without owner intervention.
## Findings and changes
- The asynchronous image task receives `supports_rgba` but initializes its texture
with false. Its completion conversion consequently leaves ARGB words unchanged,
while this port's Vulkan texture uploader consumes RGBA bytes. Patch 0081 passes
the requested flag into the image. PNG files/core framebuffer formats and the
synchronous asset loader are unchanged. Existing correctly saved thumbnails
should display correctly when loaded again; no file rewriting is needed.
- Compiled input defaults already select ps5, but joypad defaults select null.
Patch 0082 selects ps5 for the joypad default as well.
- Reset to Defaults clears auto-binds and device identity without restarting input.
The driver remembers its connected pad and would not announce it again. The reset
command now marks that announcement stale; the next poll re-requests the built-in
controller profile once, even with no fresh native pad sample. This avoids a full
driver teardown or per-frame autoconfiguration tasks.
- No PS5_Vulkan edits or logging changes. Build against the previously accepted
four driver archives in build/xmb-normal-vulkan through the existing override.
No new dependency, toolchain option or source pin is introduced.
## Host checks
`python3 -m unittest tests.test_thumbnail_defaults tests.test_input_ps5 -v`
executes the real task initializer, upload callback and pixel conversion with
stubbed file decode/scheduling. Known red, blue and partially transparent pixels
must match both RGBA and the existing non-RGBA path. It executes the compiled
default selectors and the Reset to Defaults command arm. Native input mocks verify
one reannouncement with no reopen, including no new sample and no active driver.
All three new regressions were observed failing before the fixes: wrong red pixel,
null joypad default, and absent binding refresh. All four focused tests now pass.
Patch inventory grows from 176 to 179 edits, including a fifth configuration.c
edit for the joypad default. The first full run stopped on the old four-edit
configuration inventory assertion; that expected count now includes the added
default. All 74 host tests pass. Full verification command:
```
PS5_MEMORY_DIAGNOSTICS=0 PS5_VULKAN_DIR="$PWD/build/xmb-normal-vulkan" bash tools/verify.sh
```
## Regression checklist
Preserve old logs, live config, and the exact ELF/map/build identity before upload.
Check console idle, upload the verified eboot only, read back runtime identity and
confirm live config unchanged. Start passive klog before the owner launches.
1. Load a game with recognizable red/blue colours; save a state and inspect its
thumbnail. Revisit an existing thumbnail and confirm both match gameplay.
2. Return to XMB; use Configuration File -> Reset to Defaults. Immediately check
buttons and left-stick navigation, then input binding capture.
3. Save the reset defaults and restart manually. Confirm native input still works.
4. Confirm gameplay and Close Content/menu colours remain correct.
Collect retroarch.log/trace/klog for each run and match the uploaded identity.
No console success is claimed by the host tests; no launch is authorized by this plan.
## Verified build and upload
All five gates passed: format, 74 host tests, build (278 frontend sources),
integration (74 tests and manifest) and 30 replayed evidence captures. Normal
identity: `e11018a79a62ce91fa723deaad6939c1e48a47bace21b3527ae84f4452148451`.
Exact ELF/map/title/manifest/logs: klog/thumbnail-input-build/; host-report.json
in `evidence/thumbnail-input-defaults/` records their hashes. The same four driver archives and all three Mesa
utility objects match the previously accepted normal XMB build. The expensive
memory observer is absent and the reset hook is linked into the frontend/title.
Upload: klog/thumbnail-input-upload-20260919-205531/. Console idle checked before
upload; runtime identity read back; only eboot replaced. Live configuration is
byte-identical. Its input backend is ps5 and joypad selection is empty (automatic),
so no stale null selection needed repair. No title launch or kill was sent.
Owner confirmation accepts the reported fixes. Individual checklist results were
not separately reported, and no fresh console logs were captured after acceptance.
Existing logs/settings were preserved.
-108
View File
@@ -1,108 +0,0 @@
# Troubleshooting
Symptom first, then the cause that was actually found, then the fix. Written for
the agent that hits this at 3am with no memory of the last session, so the entry
starts with the text the machine prints, not with the diagnosis.
Append new entries at the end. When a cause stops being true (a version bump, a
platform change), append a short entry that says so rather than deleting the old
one — the old entry is still what someone with an old lockfile will see.
## The title appears in the launcher, but nothing happens when it is selected
**Cause.** The ELF imports `libkernel_sys.sprx`. That library is not present in
the process the console's homebrew loader creates for a payload application, and
the loader refuses the module without printing anything the user can see. A link
against the wrong kernel stub produces this, and so does a core built by a
makefile that picked the `sys` variant on its own.
**Fix.** `readelf -dW dist/<TITLE_ID>/eboot.bin | grep NEEDED` must list
`libkernel_web.sprx` and must not list `libkernel_sys.sprx`. `prospero-clang`
links the `web` variant by default, so a `sys` entry means a flag, a makefile
variable or a prebuilt archive pulled it in: find the archive with
`prospero-nm -D` and rebuild it through `$CC`, not with a hardcoded compiler.
**Tell it apart from.** A missing `sce_sys/param.json` looks the same from the
launcher's side but the console's log shows the title directory being rejected
before any module is read.
## `check-ps5-object: not a PS5 object; the toolchain was bypassed`
**Cause.** The file was compiled by the host compiler. Prospero output and a host
Linux object are both ELF x86-64 with the System V ABI, so nothing else tells
them apart — the import table is the only reliable signal. This happens when a
makefile hardcodes `CC = gcc` or `CC = clang` in a branch that does not read the
environment.
**Fix.** Pass the toolchain as make *variables* (`CC="$CC" CXX="$CXX" AR="$AR"
RANLIB="$RANLIB"` on the command line), because a command-line variable overrides
even an unconditional assignment in the makefile. Then rebuild from clean: a
stale object from the host build survives in the same tree and will be linked
again if the timestamps say it is current.
## `error: <name>_libretro.so does not export retro_api_version`
**Cause.** The core was built with its symbols hidden or stripped from the
dynamic table. RetroArch loads cores with `dlopen` and resolves them with
`dlsym`, so a core whose entry points exist only in the static symbol table links
fine, stages fine, and fails to load on the console.
**Fix.** Build the core with `-fPIC` and without a version script or `--strip-all`
that removes dynamic symbols, then re-check with
`prospero-nm -D --defined-only <core>`. Note that `nm --defined-only` without
`-D` reports "no symbols" on a perfectly good core, because the static table is
gone; that is not a failure.
## A `make` target in `vendor/` cannot find a header the patch was supposed to add
**Cause.** `vendor/retroarch` is a patched tree, and a partial re-fetch, an
interrupted patch or a checkout that skipped `tools/fetch-retroarch.sh` leaves it
half-applied. Nothing in `vendor/` is committed, so the tree on disk is the only
copy.
**Fix.** `rm -rf vendor && tools/fetch-retroarch.sh`. It verifies the pinned
digest before extracting, so a re-fetch is cheap and cannot silently pick up a
different upstream. Never edit a file under `vendor/` to get unblocked: put the
change in `patches/` and re-run the fetcher, or the next fetch discards it.
## The title closes immediately and the log says `PRX_SCE_MODULE_LOAD_ERROR`
**Cause.** The console's loader could not load a module the application needs, and
it names the reason itself:
```text
# exception: 0xa0020102 (PRX_SCE_MODULE_LOAD_ERROR)
# === Lack of a .prx file in /app0/sce_module is detected!!! ===
# Copy the file (e.g. libc.prx) from target/sce_module.
```
`/app0` is the title's own directory as the application sees it, so this means the
file at `sce_module/libc.prx` inside the title folder is missing, misnamed, or
not a module the loader accepts. The size settles which: the signed module is
`1,284,674` bytes, and a copy that is `1,335,962` bytes is the module's raw ELF
rather than its signed container, which the loader refuses — and then reports as
absent.
**Fix.** Put the signed module in place, with that exact lower-case name:
```text
sce_module/libc.prx 1,284,674 bytes sha256 e6ff45d16adf687855cc3b33b0c8a4132b6504360b221e0a34c7e99fb3ba0036
```
`dist/<TITLE_ID>/sce_module/libc.prx` in this repository is that file, and it is
byte-identical to the sibling project's `../PS5_Vulkan/runtime/libc.prx`.
**Tell it apart from.** A null-pointer crash inside a *raw* `eboot.bin` looks
similar from the sofa — the title closes at once — but its log shows `signal: 11
(SIGSEGV)` with `fault address: 0000000000000001` and no `PRX_SCE_MODULE_LOAD_ERROR`
line. That failure is the image, not the module; see `docs/FINDINGS.md`.
## Known benign
Messages that are expected and safe, with the exact text to match. Anything not
on this list is a real failure until it is understood.
| Message | Why it is safe | Seen in |
| --- | --- | --- |
| `Fontconfig error: Cannot load default config file: No such file: (null)` | The payload starts with no font configuration file, so fontconfig falls back to its built-in defaults. Text still renders through the font this build loads directly, which is why the menu is readable. The message is printed once, at startup, before any driver line. | The Option 1 baseline run, `evidence/m1-baseline-loads/` |
| An empty list of captures under `evidence/` | The evidence gate has nothing to replay before the first console run lands. The gate prints `0 captures replayed` and passes. | `tools/verify.sh evidence` during M0 |
-126
View File
@@ -1,126 +0,0 @@
# XMB horizontal-navigation allocation investigation
## Scope
The owner reproduced the crash by holding left/right between XMB tabs. This
investigation uses the existing console capture and the exact configured source;
it does not launch a title, change allocation policy, or modify PS5_Vulkan.
There is no accepted crash fix yet.
## What the capture establishes
See `evidence/xmb-memory-crash/` for the symbolized console failure. Between
5,004 ms and the first failure at 6,429 ms, tracked live native requests grow
from 4,625,679 to 11,616,951 bytes: a 6,991,272-byte increase. The failing
15,488-byte allocation belongs to `xmb_list_insert`, through `xmb_alloc_node`.
The following three logged failures are the same size/caller, with successful
648-byte callback allocations between them. XMB returns from the failed node
allocation without stopping the surrounding entry-building loop.
The last caller breakdown predates the rise. A failed allocation's caller is
not necessarily the owner of the memory already occupying the heap. Therefore
the capture does **not** establish that XMB nodes account for that entire rise,
nor identify the destination tab or its list length. Native accounting excludes
allocations internal to system libraries. Neither total PS5 memory exhaustion
nor the native heap's exact limit, fragmentation or corruption is established.
The later fatal fault is separate: texture unloading enters `vkQueueWaitIdle`,
sync allocation fails, and Mesa's allocation-error logger dereferences NULL.
Image creations minus destructions stay at 131; that rules out growth in the
observed live-image count over these snapshots, not driver-internal leaks or
growth in bytes per image. The allocation-log flood was already fixed before
this capture.
## Why tab switching allocates
1. `xmb_list_cache` optionally copies the visible outgoing entries into one
animation list. `xmb_list_deep_copy` frees that list's previous nodes and
callbacks before replacing it. It does not keep a queue of old lists.
2. `menu_driver_deferred_push_content_list` dispatches the destination tab to
its display-list builder. The inspected main/settings/history/favourites,
contentless-core, playlist-directory and horizontal-playlist cases clear the
previous selection list before repopulating it.
3. `menu_entries_append` allocates an XMB node and a callback object per entry.
Ordinary nodes contain an embedded thumbnail-path structure even when the
entry has no thumbnail. The x86-64 layout matches the console request:
| Component | Bytes |
| --- | ---: |
| Seven path arrays and four name arrays | 15,360 |
| Complete thumbnail-path structure | 15,400 |
| Complete XMB node | 15,488 |
| Menu callback object | 648 |
| Node + callback per entry, excluding strings/list storage | 16,136 |
4. These requests are below the native wrapper's 1 MiB mapping threshold.
Repeated medium-sized list allocations therefore remain on the native heap,
unlike large core buffers. Five hundred entries need 8,068,000 requested
bytes for nodes and callbacks alone. **500 is an illustrative size, not the
measured length of the owner's tab.**
5. Held-button tab repeats have a 99,000-microsecond delay when scroll
acceleration is active. Each accepted switch can repeat the work above.
Speed increases allocation turnover; this alone does not prove retained
memory growth.
## Wallpaper and error-path work
`xmb_update_dynamic_wallpaper` calls `xmb_load_image(..., MENU_IMAGE_NONE)`
when a changed candidate wallpaper is invalid. It then sets `bg_file_path` to
NULL, so a nonempty missing candidate is not remembered as handled. The next
update can take the same branch. `xmb_load_image` destroys the background and
white fallback texture, then creates the white texture again. This happens
even when there was already no background image to replace.
The captured configuration enables dynamic wallpapers. The crash stack reaches
this wallpaper-update/white-texture-unload path; the capture does not include
the resolved wallpaper candidate. Missing-wallpaper churn is thus a confirmed
source behavior and a plausible repeat trigger, not a measured explanation for
the 6,991,272-byte rise. It explains why changing tabs can enter a GPU queue
wait while the native allocator is already failing.
## Local experiment and its limits
`tools/probe-xmb-allocations.py` extracts the actual node allocation/copy/free,
list copying/clearing and file-list function bodies from `build/ra-conf`, and
uses the real structure headers. It compiles a host harness with AddressSanitizer
and UndefinedBehaviorSanitizer. GPU/animation boundaries are inert and nodes
have no textures; list construction and its chosen lengths are synthetic.
This is not a complete frontend, timing, task-queue, native-heap or GPU test.
The experiment alternates list lengths 12, 20, 1, 500 and 6, copying up to eight
visible entries each time. Across 10,000 replacements / 4,566,034 allocation
calls, end-of-cycle live requested bytes stay at 259,288, peak at 8,236,400, and
return to zero after cleanup. The exercised ordinary-node lifecycle therefore
does not accumulate old lists. Native fragmentation remains outside this test.
A separate ownership check finds that `xmb_free_node` does not release its
`console_name` string. This is a real small leak for named nodes; the ordinary
selection nodes in this experiment have NULL names. It does not establish the
cause of the console spike. LeakSanitizer cannot run under the desktop sandbox's
ptrace; explicit allocation accounting verifies cleanup instead. The first
attempt failed on that tooling limitation; rerunning with only leak scanning
disabled passed both other sanitizers and all accounting assertions.
Replay the committed result after preparing the configured source:
```sh
python3 tools/probe-xmb-allocations.py --expect evidence/xmb-allocation-investigation/host-probe.json
bash tools/verify.sh format evidence
```
## Next discriminating measurement
Before changing allocator policy, record the top live allocation callers once
on the **first** failure, plus bounded numeric tab/list counters around clear
and populate: destination kind, list size, live node count, copied node count,
and native bytes before/after. Avoid entry names, content paths and per-entry
logging. This distinguishes a large destination list, retained nodes, other
allocation owners, and failure despite stable live requests. It needs another
owner-authorized console run; no new diagnostic binary was built in this step.
Candidate fixes to evaluate after attribution are lazy/shared thumbnail-path
storage for XMB entries, eliminating redundant white-texture recreation, and
proper allocation-failure handling in the frontend and driver logger. Merely
slowing navigation or increasing the heap would not establish the root cause.
Evidence recorded 2026-09-19 on `codex/xmb-allocation-diagnostics`.
-110
View File
@@ -1,110 +0,0 @@
# XMB list allocation safety
The owner confirmed crash-free navigation with the diagnostic build and then
confirmed the normal build eliminated the five-second hitches. Current logging
is preserved: RetroArch.log, trace and passive klog remain available, while the
expensive memory observer is opt-in. Evidence: `evidence/xmb-safe-list-run/`.
## Changes and rationale
- Patch 0080 changes XMB's inline 15,400-byte thumbnail-path structure into an
optional pointer. Ordinary nodes shrink from 15,488 to 96 bytes. Paths are
initialized on demand for visible icon thumbnails, released outside the visible
range, and independently owned by animation copies. Node destruction also frees
console_name. Image tasks copy their path; they do not retain the path structure.
- `ps5_menu_malloc` allocates XMB nodes and menu callback objects in 64 KiB anonymous
mapped slabs, with 128- and 1,024-byte payload classes. A locked bitmap allocates
slots; the last free unlinks and unmaps the slab. Native pointers are identified
without reading their allocation headers and still return to libc. Ordinary
`free` and `realloc` dispatch by ownership; failure preserves the old allocation.
This avoids one mmap per object and leaves native heap room for strings, list
arrays, playlist parsing, system libraries and the GPU driver. Allocations above
1,024 bytes are rejected by this narrow API. The existing general 1 MiB threshold
is unchanged. Diagnostic mapped counts now include requested slab-object bytes,
excluding slab rounding, headers and size-class padding.
- File-list append/insert check growth and string allocations before publishing an
entry. Menu append/prepend allocate callbacks first and reject missing XMB nodes,
rolling back the newly inserted item. A per-list failure flag suppresses repeated
attempts until clear. The playlist loop stops at the first rejected allocation.
Existing complete entries remain valid; no incomplete callback/node is published.
- Animation copies check capacity, strings, node ownership and callback allocations.
Failure discards the optional copy, preserves the source, and leaves an empty
destination. Empty-list icon loops are bounded. Container allocations initialize
the new flag and are checked. The patch set grows from 150 to 176 edits.
- The build fingerprint now includes patched headers, because file_list_t changes
size. Every frontend consumer must rebuild against the same layout. No new SDK,
dependency, compiler flag or version pin is introduced. PS5_Vulkan is unchanged.
## Host verification
Reproduction commands:
```sh
python3 -m unittest tests.test_menu_memory tests.test_xmb_safe_lists -v
PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh
```
The new tests compile and execute actual patched list/node/menu functions and the
native memory wrapper with AddressSanitizer and UndefinedBehaviorSanitizer. They
exercise 7,384 synthetic entries, every allocation position in append/prepend and
animation copy, failure retry suppression, non-LIFO release, realloc failure,
foreign allocations, concurrent use, and complete mapping/ownership cleanup.
LeakSanitizer is disabled for the sandbox; explicit live-byte/mapping accounting
checks leaks. GPU work, callback binding and menu geometry are stubbed in the
source-extracted test: host success does not establish native navigation/rendering.
The first full gate attempt failed only on the deliberately pinned patch count
(150); it was updated to 176 to cover the 26 new edits. The final run passed all five gates and 71 tests; 278/278 frontend sources
compiled. `evidence/xmb-safe-list-run/diagnostic-build.json` records the exact candidate and source hashes.
The diagnostic image was uploaded with runtime identity readback and unchanged
live config; the owner subsequently ran it manually. No launch was sent by the agent. Exact symbols and logs are in `klog/xmb-safe-lists/`.
## Future console regression checks
Preserve the current console logs and configuration before deployment. Save the
matching ELF, map, build identity, staged manifest and linked archive hashes in
ignored klog/ before another build. Check the console is idle before upload; verify
the uploaded runtime identity. Do not replace live configuration or user playlists.
After the owner authorizes launch, start klog before launch and keep collecting
memory-diagnostics.log, retroarch.log and trace.txt. Ask the owner to enter the same
7,384-entry custom playlist, navigate left/right naturally and rapidly, scroll its
entries, leave/revisit it several times, and verify icons/text/input remain correct.
Also check a small built-in tab, RGUI/XMB transition, and a game/menu transition.
Record whether the owner closes the title or it exits unexpectedly.
Pass requires no allocation failures or dropped diagnostic records, no Vulkan
refusals/API errors/fatal kernel signals, responsive clean navigation confirmed by
the owner, and node/callback bytes attributed to the mapped route. Native usage
must no longer show the old per-entry node/callback burst. Compare repeated returns
to a small tab for retained growth; inspect differences rather than assuming every
cache returns to an identical count. Keep the acceptance evidence with each verified build.
The unsafe PS5_Vulkan logger under genuine allocation failure is a separate known
issue. This frontend fix does not fix that driver code or guarantee recovery
from arbitrary process-wide exhaustion; it prevents this list workload from consuming
its native heap and makes the covered list construction/copy failures safe.
## Five-second stutter comparison
The diagnostic calls ps5::memory::tick before presentation. Every five seconds
it holds the observer mutex, scans 131,072 tracking slots, aggregates callers and
synchronously writes summary/caller rows. This matches the reported hitch cadence.
The owner confirmed the normal build fixed it; exact stall duration was not measured.
```sh
PS5_MEMORY_DIAGNOSTICS=0 PS5_VULKAN_DIR="$PWD/build/xmb-normal-vulkan" bash tools/verify.sh
```
All five gates passed, including 71 tests and compilation of 278 frontend sources.
The ignored mirror uses the same four archived driver libraries as the successful
diagnostic through the existing PS5_VULKAN_DIR override. Mesa utility object
changes were confined to debug compilation-directory information; stripped object
bytes match. No sibling driver sources were changed. Binary inspection confirms
observer symbols/hooks are absent, while ps5_menu_malloc and native wrappers remain.
Upload identity was read back and live configuration preserved. No launch or kill
was issued by the agent. The owner subsequently reported “That fixed it” and
requested committing on main with existing logging. No new normal-run logs were
captured; the normal acceptance is the owner's observation, not a measured FPS.
Exact identities, hashes, commands and scope are in diagnostic-build.json and
normal-build.json under `evidence/xmb-safe-list-run/`.
-68
View File
@@ -1,68 +0,0 @@
# Which source each published release was built from
Each GitHub release of this project carries one ZIP. This page records, for every
release, the commit whose tree the ZIP's `eboot.bin` was built from, the revisions
of what was linked into it, and where the release's tag points. It was established
on 2026-09-29 from the published assets (downloaded and hashed), their upload
times, the repositories' public push events and this repository's history. What
could not be established is said so.
**The tags do not always name the built commit.** The releases were created with
`gh release create` against `main`, and GitHub made each tag from whatever `main`
held on GitHub at that moment. Three times the build was pushed afterwards, so the
tag names an older commit than the one built. Tags are not moved (published history
is not rewritten); the table below is the correction, and docs/RELEASING.md is the
procedure that prevents it.
## The releases
| Release | Asset (sha256) | Tag points to | Built from | Evidence for "built from" |
| --- | --- | --- | --- | --- |
| v0.1.0-alpha.1 | `PS5_RetroArch-v0.1.0-alpha.1.zip` `757a6ab774b990638a71e8dbe80deb6d66e799b53c4b52ac4cb504280c3388c4` | `a18cec6` | `b1dced9` | The ZIP was uploaded at 2026-09-20 01:02 UTC, after the release was created; its `eboot.bin` build identity `e11018a7…` is recorded in `b1dced9` (docs/THUMBNAIL_INPUT_DEFAULTS.md), pushed at 01:01 UTC. |
| v0.2.0-alpha.2 | `…v0.2.0-alpha.2.zip` `5f611cb32e8212fb8a0b63f32c2329bfecd828dfd4434949a9a1366f332b13f2` | `d0a815b` | `d0a815b` | Pushed at 13:49 UTC, release at 13:53 UTC; no later commit before the upload. The build identity `cce8e166…` is not recorded in the repository. |
| v0.3.0-alpha.3 | `…v0.3.0-alpha.3.zip` `d313b30dba97ed82277ee74a495801306a64a24fe365e9aba464c8df25da86eb` | `18dc105` | a tree at or after `c1d9f15`, most likely `6c7f418` | The ZIP's `eboot.bin` contains `src/permissions_ps5.cpp`'s log string ("permissions: /app0 checked=…"), which `c1d9f15` added after the tag. `6c7f418` is the last commit before the upload; the build identity `ad98b960…` is not recorded. |
| v0.4.0-alpha.4 | `…v0.4.0-alpha.4.zip` `093fd883bbff63237486d3f7b9b1cd29c9a0d3142e42bf53367861b72757b144` | `52009a9` | `52009a9` | Build identity `870bd1bb…` is recorded in `054ab98`, an ancestor of the tag; the next commit (`a9452a6`) is documentation. |
| v0.5.0-alpha.5 | `…v0.5.0-alpha.5.zip` `aa85d74e224bab3f0dc414c1176f3aa04ddf02b5d86d522b218d6c9c30e769de` | `1ae4a1a` (2026-09-26) | `72847d6` | The tag's tree cannot link RADV (`tools/build-title.sh` there has no RADV path; it arrived in `f932c82` on 2026-09-28). Build identity `f96bdb0e…` is recorded in `72847d6`, pushed at 12:30 UTC, five minutes after the release. |
Every "built from" commit above is now on the public `main`.
## What was linked in, per release
| Release | RetroArch | Cores (pinned upstream or fork revision) | Graphics driver | SDK |
| --- | --- | --- | --- | --- |
| alpha.1 | 1.22.2 `69a4f0e` + patches | FCEUmm `236ccdf`, mGBA `7a12d6d`, Snes9x `fae2fea`, FBNeo `6bb3167`, Genesis Plus GX `c2838c7` | ps5vk from `../PS5_Vulkan`, revision not recorded; built before PS5_Vulkan's public history begins (2026-09-20) | ps5-payload-dev v0.42 release |
| alpha.2 | same | + PPSSPP v1.20.4 `fa50bb1` | ps5vk, revision not recorded (PS5_Vulkan `085aac6` was pushed 5 minutes before the release) | v0.42 release |
| alpha.3 | same | + Dolphin `c6630001` (libretro/dolphin) | ps5vk, revision not recorded; the latest driver commit before the upload (`81b21d8`) was pushed 2026-09-26 01:19 UTC, after the release | v0.42 release |
| alpha.4 | same | + LRPS2 `6d14775` (PS5_LRPS2) | ps5vk `40e9d30` (release notes); pushed 2026-09-28 04:06 UTC, after the release | PS5_PayloadSDK `a110320`; the fork was published 2026-09-28 |
| alpha.5 | same | same eight | RADV from PS5_Mesa `cedb774`, built by PS5_Vulkan `5d8f37d` | PS5_PayloadSDK `95c08f2` |
The upstream cores' pins did not change between releases. The mGBA and Genesis Plus
GX cores built on 2026-09-29 from these pins are byte-identical to the ones in all
five ZIPs, and FCEUmm's to alpha.2–alpha.5, which confirms those pins.
## Gaps these releases still carry
- **Licence texts.** None of the five ZIPs contains the GPL, the MPL, or the
Snes9x, FBNeo or Genesis Plus GX licences, or the OFL of PPSSPP's fonts. Builds
from now on stage them in `licenses/` (tools/stage-notices.py). The published
ZIPs are unchanged.
- **Source not offered beside the binary.** The release pages link the
repositories but carry no source archives; several cores come from upstream
repositories this project does not control. tools/source-bundle.py makes the
archives; attaching them to a release is the maintainer's decision.
- **alpha.1's driver.** The ps5vk revision linked into alpha.1 predates PS5_Vulkan's
public history, so its source is not published anywhere today.
- **Driver revisions of alpha.2 and alpha.3** are not recorded.
## Proposed "Source" section for each release's notes
Not applied: editing a published release is the maintainer's call.
```markdown
## Source
This ZIP was built from PS5_RetroArch <BUILT-FROM COMMIT> (the tag <TAG> points to
<TAG COMMIT>, an earlier commit; see docs/releases/SOURCE_CORRESPONDENCE.md). Every
component, its licence and its source revision are listed there. The licence texts
for this release are in <link to the licence bundle attached to this release>.
```
-95
View File
@@ -1,95 +0,0 @@
# PS5 RetroArch v0.4.0-alpha.4
Native RetroArch for jailbroken PS5 consoles, rendering through
[PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan). This release adds GPU-rendered
GameCube/Wii and PlayStation 2 emulation, and fixes every crash testers reported
on earlier builds.
> **Alpha.** The games and paths below are the ones I have tested on my console.
> Other games may still fail. Please report them (see *Reporting problems*).
## Highlights
- **Eight cores.** **Dolphin** (GameCube and Wii), **LRPS2** (PlayStation 2) and
**PPSSPP** (PSP) render on the PS5's GPU through PS5_Vulkan, beside FCEUmm, mGBA,
Snes9x, FinalBurn Neo and Genesis Plus GX.
- **PlayStation 2 at 4K.** LRPS2 uses its Vulkan hardware renderer by default at
6x internal resolution, with multi-threaded VU1. The God of War II and
Final Fantasy X demos and GTA San Andreas run at full speed on my console.
**You need your own PS2 BIOS** in `system/pcsx2/bios/`.
- **GameCube and Wii.** Dolphin 2609 with its JIT and fast memory. Wind Waker,
Resident Evil 4, Super Smash Bros. Melee, Mario Kart Wii and Rogue Leader are tested.
Wind Waker ran for an hour and RE4 for 30 minutes with no crash.
- **PSP.** PPSSPP v1.20.4 on its Vulkan renderer with JIT: God of War: Ghost of
Sparta and Yu-Gi-Oh! GX Tag Force are tested at 10× internal resolution.
- **120 Hz output** where the display supports it, with the correct 60 Hz fallback.
- **Faster save states.** A 50 MB PS2 save state is written without stdio, which
was the slow part.
## Fixed from tester reports
| Report | Status in this release |
| --- | --- |
| Crash after closing a game (seen with an SNES game) | **Fixed.** The video driver was being re-created on a RetroArch thread, and the console gives such threads only 64 KiB of stack. It crashed with *Threaded Video* on. RetroArch's threads now get 2 MiB, and the stack-heavy code moved to the heap. I reproduced the crash, then confirmed the fix on the console. |
| PSP game (Ratchet & Clank: Size Matters) crashing after one to two minutes | **Fixed** in the GPU driver: its internal lists were changed from several threads at once without a lock. A new driver test reproduces the corruption on the old driver and passes on the new one. PPSSPP ran a 10-minute soak with the menu opened 25 times and did not crash. |
| Every game at **half speed with V-Sync on** | **Fixed.** On a display that only refreshes at 60 Hz, the console accepted the 120 Hz mode and kept refreshing at 60 Hz, so each frame was shown twice. The driver now measures the real refresh rate and falls back to 60 Hz. My display is 120 Hz, so the 60 Hz path still needs confirming on a 60 Hz TV. |
| Rogue Leader crash after about three minutes | **Fixed** (a race in Dolphin's dual-core GPU FIFO). |
With the half-speed fix, **V-Sync on** is the recommended setting on every display.
## Stability work in this release
- Every thread the title, RetroArch and the driver start now asks for an adequate
stack instead of the console's 64 KiB default.
- Driver state that several threads can reach is atomic or locked. A debug-table
counter could write past its array when two threads recorded commands at once.
- Before release, every core was run with a game: boot, menu open and close, then a
Quick Menu action, with Threaded Video on for half of them and V-Sync on
throughout. There were no crashes and no fatal signals.
## Known issues
- **Shader compilation stutter.** The first time a game needs a new shader, it can
hitch while the shader compiles; later runs load it from the cache. Dolphin's
default is still *Synchronous* compilation, and the driver compiles one pipeline
at a time. Asynchronous compilation is the next piece of work.
- **Rogue Leader** runs at 72–85% for stretches of its attract sequence, even with
every shader cached. The cost is the driver's CPU work, and it is being worked on.
- **PAL PS2 games** (50 fps) on a 120 Hz output alternate two and three refreshes per
frame, which can look uneven. The console offers no 50/100 Hz mode.
- **PS2 games in `.7z` archives** are not unpacked. Use ISO, CHD, CSO or another
supported disc format.
- Mario Party (Wii): black flicker on the world map was reported and has not been
investigated yet.
## Install or update
1. Copy the complete `PPSA99169` folder to your homebrew title location, for
example `/data/homebrew/PPSA99169/`, and launch it from your homebrew launcher.
2. Updating keeps your `config/`, `savefiles/`, `savestates/`, `system/` and
`content/`. Settings already saved there, such as a core's internal resolution,
are kept rather than replaced by new defaults.
3. Put games in `content/` and BIOS files in `system/` (PS2: `system/pcsx2/bios/`).
No games or BIOS files are included.
## Reporting problems
Include the core, game, file format and the settings you changed. After the
problem, and **before relaunching**, copy `trace.txt` and `retroarch.log` from the
title folder: a new launch replaces them.
## Build
Build identity `870bd1bb94d9c35e7ec0efdf32ebfe457500bac3c4387376b7b0ea2e3c42957a`
| Component | Version |
| --- | --- |
| RetroArch | 1.22.2 with this port's patches |
| PS5_Vulkan driver | `40e9d30` |
| PS5 payload SDK fork | `a110320` |
| PPSSPP | v1.20.4 |
| Dolphin | 2609 (libretro/dolphin `c6630001`) |
| LRPS2 | PS5_LRPS2 `6d14775` with the port patch |
All five host gates (format, unit, build, integration, evidence) pass, and so does
the driver's host test suite.
-118
View File
@@ -1,118 +0,0 @@
# PS5 RetroArch v0.5.0-alpha.5
Native RetroArch for jailbroken PS5 consoles. This release changes the graphics
driver: the title now renders through **RADV**, Mesa's Vulkan driver for AMD
GPUs, ported to the PS5 in [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan).
It replaces ps5vk, the driver every earlier release used.
> **Alpha.** The games and paths below are the ones I have tested on my console.
> Other games may still fail. Please report them (see *Reporting problems*).
## Highlights
- **A new graphics driver.** RADV is the driver Linux uses on AMD GPUs, with its
ACO shader compiler. It is built from my Mesa fork
([PS5_Mesa](https://github.com/mihawk-99/PS5_Mesa)) with a PS5 layer for memory,
command submission and display, and it reports Vulkan 1.4. The menu, the five
software cores and the three GPU cores (PPSSPP, Dolphin, LRPS2) all render
through it.
- **PSP anti-aliasing.** PPSSPP's MSAA works. It needs features ps5vk never had
(render pass 2 and depth/stencil resolve), so PPSSPP's MSAA setting was
previously ignored.
- **A shader cache on RADV.** Compiled pipelines are kept in
`radv-shader-cache/` in the title folder. A game's second start reads them back
instead of compiling: with the cache filled, PPSSPP's God of War: Ghost of
Sparta created its pipelines in 29 ms instead of 1.4 s.
- **Faster shader compiles.** Every allocation in the title used to wait on one
lock, and the emulators compile shaders on several threads at once. The title's
heap now gives each thread an arena of its own. PPSSPP compiling from an empty
cache: 2.0 s instead of 5.5 s, with no speed lost after boot.
- **A smaller package.** `sce_module/libvulkan.so.1` is gone: RADV is linked into
`eboot.bin`, and nothing loaded that library (a PS5 title cannot load one).
## Tested on RADV for this release
On my console, with V-Sync on and a 120 Hz display:
- **Every core with a game**: FCEUmm (Super Mario Bros.), Snes9x (A Link to the
Past), mGBA (Mario Pinball Land), Genesis Plus GX (Sonic the Hedgehog), FinalBurn
Neo (Metal Slug), PPSSPP (God of War: Ghost of Sparta), Dolphin (The Wind Waker)
and LRPS2 (the God of War II demo). Each ran through boot, the menu opened and
closed, Close Content from the Quick Menu, and the same game loaded again, with
Threaded Video on for four of them. No crash and no fatal signal, three times
over as the driver changed during the release work.
- **PPSSPP for ten minutes** with Threaded Video and the menu opened 25 times: no
crash, every 10-second audio window at 98–100% speed.
- **The pictures**: God of War: Ghost of Sparta and GTA San Andreas show the same
frames as on ps5vk (the rain and the video move with timing); Wind Waker's title
screen renders correctly.
- **Speed after boot**: PPSSPP and Dolphin (Wind Waker) hold full speed once the
game has booted. LRPS2 (GTA San Andreas) does too, but for one late 10-second
window at 98.9%, where ps5vk had one at 99.4%.
The longer runs listed for v0.4.0-alpha.4 (Wind Waker for an hour, Resident Evil 4
for 30 minutes, Super Smash Bros. Melee, Mario Kart Wii, Rogue Leader, Yu-Gi-Oh!
GX Tag Force, the Final Fantasy X demo) were on ps5vk and have not been repeated
on RADV yet.
## Fixed during the release work
| Problem | Status |
| --- | --- |
| PPSSPP crashed within four seconds of starting a game with MSAA on | **Fixed** in PPSSPP. It took the GPU's Vulkan version as usable although RetroArch opens Vulkan 1.1, so it called a Vulkan 1.2 function RADV correctly withheld. ps5vk never reached that path. |
| Slow first compiles on several threads | **Fixed**: the title heap's arenas (above). |
| A filled shader cache still slow to read from several threads | **Fixed** in the driver: each cache part is opened once, where every read used to reopen and lock its files. |
## Known issues
- **Dolphin's first start of a game.** With an empty shader cache, Dolphin
compiles its ubershaders while the game boots. Wind Waker's first 20 seconds ran
at 83% and 92% speed; ps5vk managed 86% and 99% from an empty cache. Later
starts read the cache and match ps5vk. Making the first start as fast is the
next piece of work.
- **Rogue Leader** ran at 72–85% for stretches of its attract sequence on ps5vk.
It has not been measured on RADV yet.
- **60 Hz displays.** The driver measures the refresh rate after switching to
120 Hz and returns to 60 Hz if the display did not follow, as Alpha 4 did. My
display is 120 Hz, so this path still needs confirming on a 60 Hz TV.
- **PAL PS2 games** (50 fps) on a 120 Hz output alternate two and three refreshes
per frame, which can look uneven. The console offers no 50/100 Hz mode.
- **PS2 games in `.7z` archives** are not unpacked. Use ISO, CHD, CSO or another
supported disc format.
- Mario Party (Wii): black flicker on the world map was reported and has not been
investigated yet.
## Install or update
1. Copy the complete `PPSA99169` folder to your homebrew title location, for
example `/data/homebrew/PPSA99169/`, and launch it from your homebrew launcher.
2. Updating keeps your `config/`, `savefiles/`, `savestates/`, `system/` and
`content/`. Settings already saved there, such as a core's internal
resolution, are kept rather than replaced by new defaults.
3. Put games in `content/` and BIOS files in `system/` (PS2: `system/pcsx2/bios/`).
No games or BIOS files are included.
4. After updating from an earlier release, `sce_module/libvulkan.so.1` and
`ps5vk-shader-cache/` are no longer used and can be deleted. The first start of
each game compiles its shaders again, into `radv-shader-cache/`.
## Reporting problems
Include the core, game, file format and the settings you changed. After the
problem, and **before relaunching**, copy `trace.txt` and `retroarch.log` from the
title folder: a new launch replaces them.
## Build
Build identity `f96bdb0e26dc7e540b51d33e68a0e6933156b11b461058b671544a81c8436482`
| Component | Version |
| --- | --- |
| RetroArch | 1.22.2 with this port's patches |
| Graphics driver | RADV from PS5_Mesa `cedb774` (Mesa 26.2.0), built by PS5_Vulkan `5d8f37d` |
| PS5 payload SDK fork | `95c08f2` |
| PPSSPP | v1.20.4 |
| Dolphin | 2609 (libretro/dolphin `c6630001`) |
| LRPS2 | PS5_LRPS2 `6d14775` with the port patch |
All five host gates (format, unit, build, integration, evidence) pass: 74 Python
tests, and 52 recorded captures replay.
-55
View File
@@ -1,55 +0,0 @@
# Active work
A filled example, for a fictional SaaS project, so the density and the tone are
obvious. It is not part of the read order: copy the shape, not the content.
_Updated: 2026-03-04_
## Now
**The reconciliation worker is merged and proven on staging.** It consumes the
ledger stream, writes the expected-balance table and reports a mismatch instead
of correcting it, which is the behaviour the finance team asked for in
TICKET-412. Evidence: `tools/verify.sh` green at `4c1a9f2`, and the staging run
recorded in `evidence/reconcile/` (12,400 events, 3 seeded mismatches, all three
reported, 0 corrections). What it owes: the retry path when the stream redelivers
an event (it double-counts today; the test that reproduces it is `failing` in
`pnpm test`, so the gate is red until the next step lands).
**The vendor export is parked.** `patches/vendor-export.patch` implements it,
but the vendor's sandbox is the only place it can be verified and the account is
not provisioned yet. Not merged; the patch carries the verification steps.
## Next
1. Deduplicate redelivered stream events by `event_id`, with the test first.
2. Provision the vendor sandbox, then unpark and verify `vendor-export.patch`.
3. Backfill the reconciliation table for February and compare the totals.
4. Trim the raw stream retention from 30 days to 7 once the backfill is done.
## Working notes
- Migrations run before the API boots in staging; `pnpm db:migrate` twice in a
row is safe by design, and that is the check that the down path still exists.
- `pnpm test` needs `DATABASE_URL` pointing at the disposable container
(`compose.dev.yml`); pointing it at staging corrupts the seeded fixtures.
- The stream's consumer group resets its offset on redeploy unless
`STREAM_RESUME=1` is set; the runbook now says so.
## Last verified
| Check | Result |
| --- | --- |
| `tools/verify.sh` (all five gates) at `4c1a9f2` | PASS: lint, 412 unit, build, 31 integration, 6 evidence replays |
| staging smoke, `logs/smoke-2026-03-04.log` | 12,400 events consumed, 3 of 3 mismatches reported, 0 corrections |
| February backfill dry run | 41,209 rows would be inserted; not committed to staging yet |
| vendor sandbox | not provisioned: the export patch is parked |
## Open findings
- Redelivery double-counts: the stream guarantees at-least-once and the worker
assumes exactly-once. The fix is deduplication, not a stronger guarantee.
- `pnpm test` takes 90 s because the integration suite boots Postgres twice;
acceptable for now, revisit if it passes 3 minutes.
- The vendor's sandbox rejects requests faster than 5 per second with a 429 and
no `Retry-After`; the parked patch waits 250 ms between calls because of it.
-38
View File
@@ -1,38 +0,0 @@
# Phase log
A filled example, for the same fictional SaaS project. Two entries: the landed
step and the failure that shaped it. Copy the shape, not the content.
---
## 2026-03-04: The reconciliation worker reports mismatches instead of correcting them
The worker now consumes the ledger stream, writes the expected-balance table and
emits a mismatch record when the two disagree, leaving the ledger untouched. It
exists because finance needs the disagreement to be visible before anything
rewrites a balance (TICKET-412), and it unblocks the February backfill.
**The evidence.** `tools/verify.sh` at `4c1a9f2` returned PASS on all five gates
(412 unit, 31 integration, 6 evidence replays), and the staging run
(`logs/smoke-2026-03-04.log`, 12,400 events) reported 3 of 3 seeded mismatches
with 0 corrections. The distilled capture is committed at
`evidence/reconcile/2026-03-04/`, and `tools/verify.sh evidence` replays it.
**What was tried first.** The first version corrected the balance when the
expected value differed. The staging run then reported 0 mismatches and 3
corrections, which is the opposite of what finance asked for: the correction
erased the evidence. It was reverted in `9a1c4de`, and the worker now only
reports.
**Commit.** `4c1a9f2` — Report ledger mismatches instead of correcting them.
---
## 2026-02-27: The stream offset resets on redeploy
The consumer group was recreated on every deploy, so each deploy replayed the
whole stream and double-counted history. Found by comparing the row count before
and after a no-op deploy: 41,209 rows became 82,418. The fix reads the stored
offset when `STREAM_RESUME=1` (`b7d0e11`); the runbook and
`docs/TROUBLESHOOTING.md` were updated in the same commit, because the next
person to see the doubling will look there first.
-15
View File
@@ -1,15 +0,0 @@
# ProsperoLight UI
Static RmlUi/RCSS assets for the controller-first Sunshine host, application,
and settings views.
## Files
- `main.rml` — launcher structure and runtime-populated view containers.
- `styles/app.rcss` — 10-foot moonlit layout and controller focus states.
Runtime behavior is owned by the C++ application layer.
The runtime palette follows the launcher icon: deep sapphire backgrounds,
light-blue surfaces and borders, and pale moonlight highlights. Semantic
green, amber, and magenta states remain distinct.
@@ -1,7 +0,0 @@
# LVGL Montserrat bitmap fonts
These files are deterministically generated from LVGL's built-in, uncompressed 4-bpp Montserrat Medium C fonts. Each single-page, 32-bit, top-origin TGA declares 8 alpha bits and preserves the original 16 alpha levels; the BMFont XML preserves LVGL's line metrics, offsets, integer advances, and expanded class kerning for ASCII U+0020-U+007E, degree U+00B0, and bullet U+2022.
Regenerate from the repository root with `python tools/generate_lvgl_bitmap_fonts.py`; verify without writing with `python tools/generate_lvgl_bitmap_fonts.py --check`. Font Awesome PUA glyphs present in the LVGL source are intentionally excluded.
Montserrat is Copyright 2011 The Montserrat Project Authors and is distributed under the SIL Open Font License 1.1; see `OFL.txt`. Generated `.fnt` files use the RmlUi `Samples/basic/bitmap_font` XML schema.
-50
View File
@@ -1,50 +0,0 @@
# Controller icon assets
The five controller symbols were generated with OpenAI ImageGen as a single
high-contrast mask, then deterministically cropped, recolored, and exported as
36 x 36 uncompressed, straight-alpha TGA files for exact 1:1 PS5 rendering.
ImageGen prompt:
> Use case: ui-mockup; Asset type controller-button icon mask sprite sheet.
> Create a precise horizontal row of five isolated, crisp white controller
> symbols on pure black: Cross, Circle, Square, Triangle, Options. Use uniform
> optical weight, generous separation, centered geometry, no text, no labels,
> no glow, no shadow, no gradient, no texture, and no perspective.
`controller-imagegen-mask.png` is the generated source. The transparent PNG
files are reusable masters; the corresponding TGA files are the PS5 runtime
assets.
Runtime palette:
- Cross: `#70E1DC`
- Circle: `#FF8793`
- Square: `#D18BE5`
- Triangle: `#69D997`
- Options: `#F2F7F8`
The generated imagery is a project asset. The PlayStation name and controller
symbol conventions remain trademarks of their respective owner.
`play.svg` and `stop.svg` are deterministic project-authored playback symbols,
with matching 18 x 18 straight-alpha TGA runtime assets. They render directly
on the action button without an enclosing icon well and are not PlayStation
button symbols.
`page-up.svg` and `page-down.svg` are matching 18 x 18 paging chevrons shown
beside the available Previous page and Next page actions.
`status-online.svg` and `status-offline.svg` are project-authored 18 x 18
ring-and-core connection indicators. The matching TGA files are deterministic
exact-pixel runtime renders because the accepted PS5 texture loader supports
uncompressed TGA and intentionally does not include an SVG rasterizer.
`brand-medallion.svg`, `host-medallion.svg`, and `moon-crescent.svg` replace
the remaining RmlUi-generated circles and bordered shapes. Their exact-size
TGA renders prevent the horizontal geometry seams seen on the PS5 software
renderer while retaining editable vector masters.
`prosperolight-icon.tga` is a 56 x 56 top-origin runtime miniature generated
directly from `sce_sys/icon0.png`, so the ProsperoLight header and launcher tile
use the same artwork.
-10
View File
@@ -1,10 +0,0 @@
# Explicit opaque blend diagnostic (2026-09-19)
Equivalent ONE/ZERO ADD on the stock shader pass forced explicit blend registers
and FP16 exports without changing menu source-alpha blending. All five gates and
25 tests passed; the full 30-second console run completed and the script closed
it. The owner reported both blue flicker and black triangles unchanged. This is
not a verified fix. Raw capture: `klog/gpu-explicit-blend-run.log` (ignored).
Tested build identity: 8958ff2aee490a89b788269d647223e713483781f588b5e4b3f23452da1ac481.
To retest, apply diagnostic.patch, run tools/verify.sh and run-title.sh --watch 30,
and collect owner feedback. The driver was unchanged for this test.
-22
View File
@@ -1,22 +0,0 @@
# Bounded GPU readback diagnostic (2026-09-19)
This retired diagnostic established the fragment-input compiler defect fixed in
PS5_Vulkan. Apply `diagnostic.patch` to this repository and copy
`vulkan_capture.cpp` into `src/`, then run `tools/verify.sh` and the normal
`tools/run-title.sh --no-build --watch 30` after checking console availability.
It saves menu/target storage on frames 8 and 9 and shader/buffer/table storage on
frame 8 through the driver's existing debug API, after `vkQueueWaitIdle`.
It does not change GPU state or draw pixels on the CPU. Remove the hook and source
again after diagnosis; these captures are not a shipping feature.
Retrieve `/app0/gpu-*.bin` over the configured FTP into ignored `klog/`.
`decode-gpu-captures.py` decodes this run's 320x240 RGBA menu and 3840x2160 BGRA
swapchain storage using the driver's measured tile map. `inspect-gpu-resources.py`
is specific to the captured addresses/layout in `gpu-resources-latest-trace.txt`;
it reads copied binary files, never live GPU memory. Raw files stay ignored.
Before the driver fix, all six menu vertices had white colour and the menu texture
was correct, but no opaque-white sample rendered white. Both fragment varyings
compiled with base 0. After the fix, 909/909 samples are white and frames 8 and 9
are byte-identical. Evidence: `evidence/vulkan-fragment-inputs/` and
`../PS5_Vulkan/evidence/fragment-inputs/`. Owner confirms no flicker or triangles.
-23
View File
@@ -1,23 +0,0 @@
# Retired XMB framebuffer diagnostic
The owner authorized screenshots to diagnose XMB corruption. This hook captured
swapchain frames 120 and 121 after queue idle and shader/buffer/table resources
on frame 120. It is not part of the normal title: the large synchronous writes
pause rendering and contaminate the frontend's refresh estimate.
To repeat: apply `diagnostic.patch`, copy `vulkan_capture.cpp` into `src/`, run
`tools/verify.sh`, then `tools/run-title.sh --no-build --watch 45` on an idle
console. Retrieve `gpu-target-120.bin` and `gpu-target-121.bin` from the title's
FTP directory into ignored `klog/`. The captured resource filenames and byte
counts are listed in that launch's trace. XMB has no RGUI menu image, so the
zero-byte menu capture is expected. Decode the targets with
`python3 parked/xmb-readback/decode-targets.py <capture-directory>`.
Remove the hook/source again after diagnosis. Refresh the generated
`build/ra-conf/gfx/drivers/vulkan.c` from its pristine vendor counterpart, then
reapply `tools/apply-port-patches.py build/ra-conf` before rebuilding: the
patcher adds named edits but does not undo a retired edit automatically.
The driver debug API only reads
storage; the rendered image comes from Vulkan's swapchain, not a CPU renderer.
The original captures remain in ignored `klog/xmb-capture/` and the corrected
ones in `klog/xmb-textures-capture/`. Evidence is in `evidence/xmb-default/`.
-37
View File
@@ -1,37 +0,0 @@
# The generated runtime module (libc.prx)
`sce_module/libc.prx` is the module a title carries where the console's loader
expects a C library companion. It is generated, not copied: `make libc` (or
`tools/rebuild-libc.sh`) builds `tooling/native/libc_builder.cpp` and runs it on the
two manifests in `tooling/native/runtime/`, then signs the result as a fake SELF.
The code is Copyright (C) 2026 BlackBearReloaded (ps5-native-app-boilerplate),
GPL-3.0-or-later.
What the module contains:
- a few dozen bytes of machine code written by the builder: an initializer that hands
the title's `malloc`/`free`/`posix_memalign` to the kernel's heap hook and registers
three thread callbacks, and functions that return zero;
- loader metadata: the NIDs, ELF bindings, types and sizes of the symbols the
console's libc exports (`api-surface.txt`, recorded by observing that module, as
its header says), the imports it declares (`imports.txt`), and a
segment layout that matches what the loader accepts.
It contains no instructions or data copied from a console module. That is
established by the generator's source, and by reproduction: on 2026-09-29 the
module rebuilt from this repository's committed files alone was byte-identical to
the one below, which is also the `libc.prx` in every published PS5_RetroArch release
(v0.1.0-alpha.1 to v0.5.0-alpha.5). The name "clean-room", used for this module
elsewhere, is BlackBearReloaded's description of how it was written; this
repository has no record of that process beyond the source itself.
The expected artifact has SHA-256:
```text
e6ff45d16adf687855cc3b33b0c8a4132b6504360b221e0a34c7e99fb3ba0036
```
Generate it from the repository root with `make libc`, then verify it from this
directory with `sha256sum -c libc.prx.sha256`. The generated file is ignored by
Git; a normal build creates it too. The upstream description of the generator and
its compatibility scope is ps5-native-app-boilerplate's `docs/RUNTIME_SHIM.md`.
-24
View File
@@ -1,24 +0,0 @@
# Native host tooling
All repository-owned format tooling is C or C++. Make/Bash and PowerShell are
thin platform-specific orchestrators. No C# or .NET runtime is required.
The root build performs four native stages:
1. `prospero-clang18` compiles application sources and `native/app_crt.cpp`.
2. LLVM lld resolves objects, static archives, public SDK import stubs,
constructors, TLS, and unwind information into an intermediate PIE.
3. `native/ps5-native-tool` converts that PIE to the PS5 ELF layout and wraps
it in a deterministic development FSELF.
4. The requested folder or optional filesystem image is assembled.
`native/libc_builder.cpp` independently reproduces the generated clean-room
runtime from the two manifests under `native/runtime/`. Run `make libc` on
Linux/WSL or `../tools/rebuild-libc.ps1` on Windows to prove both hashes.
`make deps` fetches the pinned public payload SDK and static zlib archive into
`.deps/native/`; neither is installed globally. The shell and PowerShell
bootstrappers share the same cache layout.
See [`../docs/NATIVE_TOOLING.md`](../docs/NATIVE_TOOLING.md) for the format
boundary and low-level commands.
-22
View File
@@ -1,22 +0,0 @@
# Dolphin acceptance profiles
Core-option files for the Dolphin acceptance runs: each is `key=value` lines the
core reads from `/app0/dolphin-options.txt` for one test run in place of the
frontend's options (the port's test aid, `patches/dolphin/ps5-port.patch`,
`DolphinLibretro/Main.cpp`); `#` lines are ignored. The title deletes that file on
any launch that is not a test run, so a profile never outlives its run.
| File | Profile |
| --- | --- |
| `p0-native.txt` | 0: native baseline -- 1x, no MSAA, JIT64, fastmem, 100% clock, synchronous shaders, EFB copies on the GPU |
| `p1-accurate.txt` | 1: accurate baseline -- safe texture cache, scaled EFB copies, GPU texture decoding, exact depth |
| `p2-ubershaders-1x.txt`, `p2-ubershaders-6x.txt` | 2: synchronous ubershaders at 1x and 6x |
| `p3-torture.txt` | 3: 6x, ubershaders, 16x AF, forced linear filtering, every frame presented |
| `p4-gpu-efb.txt` | 4: 6x EFB torture with copies on the GPU |
| `p5-ram-efb.txt` | 5: profile 4 with EFB copies to RAM (read back through the CPU) |
| `p6-scale-Nx.txt` | 6: the scaling ladder, 1x to 6x on the accurate baseline |
| `p7-msaa-N.txt`, `p7-max-torture.txt` | 7: 2x/4x/8x MSAA on the accurate baseline, and 6x with 8x MSAA and profile 3's settings |
| `p12-ps5-best.txt` | 12: the PS5 defaults -- 6x, asynchronous ubershaders, 16x AF, GPU texture decoding |
Each file names the options its profile is defined by, rather than relying on the
core's defaults for them.